Creating Server Modules with Java

Server modules add functionality to the entire server and are not associated with a specific room (unlike room modules, which are bound to a specific room's life cycle). For example, a server module might send an email to an administrator if the server's CPU usage exceeds 75%.

Server modules are active for as long as the server is running. When the server starts, all server modules are also started; when the server shuts down, all server modules are destroyed.

To create a server module, first write the module code and then deploy the module. The following steps describe the process in detail.

Step 1: Create a the Module Source File

On your local hard drive, create a directory named /module_src/net/user1/union/servermodule/.

In /module_src/net/user1/union/servermodule/, create a text file named ServerStatsServerModule.java.

Step 2: Implement the Module Interface

The following Java class, ServerStatsServerModule, shows the bare minimum code for a valid server module. Add the source code shown below to ServerStatsServerModule.java from Step 1. To test deployment before continuing with the rest of this tutorial, optionally skip to Step 6.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package net.user1.union.servermodule;

import net.user1.union.api.Module;
import net.user1.union.core.context.ModuleContext;

/**
 * This module writes server stats to the log file at a given interval.  
 */

public class ServerStatsServerModule implements Module {
    /**
     * This method is called by the server when the module is initialized.  It returns true
     * if the module was able to initialize correctly.  Otherwise it returns false.
     */

    public boolean init(ModuleContext ctx) {
        return true;
    }

    /**
     * This method is called by the server when the module is shutdown.
     */

    public void shutdown() {   
    }
}

The ServerStatsServerModule class's init() and shutdown() methods are both required. The init() method is invoked by Union Server when the module is instantiated, and should contain the module's initialization code. The init() method is passed a ModuleContext object that provides access to the public objects on the server, such as Room objects and Client objects.

The init() method's return value tells Union Server whether the module should be allowed to start. If init() returns true, Union Server will activate the module. If init() returns false, Union Server logs an error and does not activate the module. For example, if a module fails to load a required local asset, it might choose to abort startup by returning false from init().

Once a server module has started, it remains active until the server shuts down, at which time Union Server automatically invokes the module's shutdown() method. The shutdown() method should contain cleanup code that frees all resources in use by the module.

Let's expand the preceding ServerStatsServerModule class to create a "stats logger" module that logs information about the server to the server's log.txt file.

Step 3: Initialize Resources

Modules initalize resources in the init() method. In the ServerStatsServerModule's init() method, we'll use the ModuleContext to retrieve a module attribute, "interval" that determines how often, in seconds, the module writes statistics to the log file. The "interval" attribute is set via union.xml, as shown later under "Step 7: Deploy the Module". The ServerStatsServerModule uses a thread to periodically log server information, so it implements Runnable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package net.user1.union.servermodule;

import net.user1.union.api.Module;
import net.user1.union.core.context.ModuleContext;
import org.apache.log4j.Logger;

/**
 * This module writes server stats to the log file at a given interval.  
 */

public class ServerStatsServerModule implements Module, Runnable {
    private static Logger s_log = Logger.getLogger(ServerStatsServerModule.class);
    private int m_interval; // in seconds
    private Thread m_thrThis;
   
    /**
     * This method is called by the server when the module is initialized.  It returns true
     * if the module was able to initialize correctly.  Otherwise it returns false.
     */

    public boolean init(ModuleContext ctx) {
        // --- get the give interval from the attribute provided
        try {
            m_interval = Integer.parseInt((String)ctx.getAttributes().get("interval"));
        } catch (NumberFormatException e) {
            s_log.error("interval for ServerStatsServerModule was not an integer.");
            return false;
        }
       
        // --- create and start our thread
        m_thrThis = new Thread(this);
        m_thrThis.start();
       
        // --- we got here everything must be OK
        return true;
    }

    /**
     * This method is called by the server when the module is shutdown.
     */

    public void shutdown() {   
    }
}

Step 4: Clean Up Resources

Next, in the shutdown() method, we clean up any resources used by the module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package net.user1.union.servermodule;

import net.user1.union.api.Module;
import net.user1.union.core.context.ModuleContext;
import org.apache.log4j.Logger;

/**
 * This module writes server stats to the log file at a given interval.  
 */

public class ServerStatsServerModule implements Module, Runnable {
    private static Logger s_log = Logger.getLogger(ServerStatsServerModule.class);
    private int m_interval; // in seconds
    private Thread m_thrThis;
   
    /**
     * This method is called by the server when the module is initialized.  It returns true
     * if the module was able to initialize correctly.  Otherwise it returns false.
     */

    public boolean init(ModuleContext ctx) {
        // --- get the give interval from the attribute provided
        try {
            m_interval = Integer.parseInt((String)ctx.getAttributes().get("interval"));
        } catch (NumberFormatException e) {
            s_log.error("interval for ServerStatsServerModule was not an integer.");
            return false;
        }
       
        // --- create and start our thread
        m_thrThis = new Thread(this);
        m_thrThis.start();
       
        // --- we got here everything must be OK
        return true;
    }

    /**
     * This method is called by the server when the module is shutdown.
     */

    public void shutdown() {   
            // --- set the thread to null
        m_thrThis = null;
    }
}

Step 5: Add Logic

Finally, we create the run() method, which contains the logic of this server module. In this case run() logs selected stats to a file. Replace all existing content in ServerStatsServerModule.java (from Step 1) with the code below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package net.user1.union.servermodule;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.ThreadMXBean;
import net.user1.union.api.Module;
import net.user1.union.core.context.ModuleContext;
import org.apache.log4j.Logger;

/**
 * This module writes server stats to the log file at a given interval.  
 */

public class ServerStatsServerModule implements Module, Runnable {
    private static Logger s_log = Logger.getLogger(ServerStatsServerModule.class);
    private int m_interval; // in seconds
    private Thread m_thrThis;
   
    /**
     * This method is called by the server when the module is initialized.  It returns true
     * if the module was able to initialize correctly.  Otherwise it returns false.
     */

    public boolean init(ModuleContext ctx) {
        // --- get the give interval from the attribute provided
        try {
            m_interval = Integer.parseInt((String)ctx.getAttributes().get("interval"));
        } catch (NumberFormatException e) {
            s_log.error("interval for ServerStatsServerModule was not an integer.");
            return false;
        }
       
        // --- create and start our thread
        m_thrThis = new Thread(this);
        m_thrThis.start();
       
        // --- we got here everything must be OK
        return true;
    }

    public void run() {
        // --- setup our MXBeans for gathering stats
        MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
        ThreadMXBean tbean = ManagementFactory.getThreadMXBean();
        String newline = System.getProperty("line.separator");
        StringBuilder stats = new StringBuilder(128);
       
        // --- while the thread should be running
        while (m_thrThis != null) {
            // --- create the stats
            stats.append(newline).append("-----------------").append(newline);
            stats.append("SERVER STATS").append(newline);
            stats.append("-------------------------------------------------------------------------").append(newline);
           
            stats.append("Memory Usage [" + mbean.getHeapMemoryUsage() + "]").append(newline);
            stats.append("Thread Usage [" + tbean.getThreadCount() + "]").append(newline);
            stats.append("-------------------------------------------------------------------------").append(newline);
           
            // --- log the stats at WARN level
            s_log.warn(stats.toString());
           
            // --- delete the contents from the StringBuilder so we can reuse it
            stats.delete(0, stats.length());
           
            // --- sleep for the interval given
            try {
                Thread.sleep(m_interval*1000);
            } catch (InterruptedException e) {
                s_log.error("Thread for ServerStatsServerModule could not sleep.  Ending Thread.");
                m_thrThis = null;
            }
        }
    }

    /**
     * This method is called by the server when the module is shutdown.
     */

    public void shutdown() {   
        // --- set the thread to null
            m_thrThis = null;
    }
}

Step 6: Give Union Server Access to the Module

First, compile the module class by running the following command from the /module_src/ directory. Replace "UNION_HOME" with your Union Server's installation directory:

Windows

javac -cp UNION_HOME\lib\union.jar net\user1\union\servermodule\ServerStatsServerModule.java

Mac/Unix

javac -cp UNION_HOME/lib/union.jar net/user1/union/servermodule/ServerStatsServerModule.java

Next, create a .jar containing the compiled class by running the following command from the /module_src/ directory:

Windows

jar -cf stats.jar net\user1\union\servermodule\ServerStatsServerModule.class

Mac/Unix

jar -cf stats.jar net/user1/union/servermodule/ServerStatsServerModule.class

Finally, place the stats.jar file in UNION_HOME/modules directory.

For a more convenient workflow during testing, you may wish to skip the creation of stats.jar and instead place the entire /net/user1/union/example/servermodule/ folder directly into the UNION_HOME/modules directory. Union Server will locate the module's .class file whether it resides in a .jar file or in a regular directory. However, as a best practice for production, module source code should be stored in a version control repository, and not included in the UNION_HOME/modules directory.

Now that the module's jar file is in the /modules/ directory, the module can be deployed.

Step 7: Deploy the Module

Server modules can be deployed at server-startup time via union.xml or at runtime via server-side code.

To deploy the ServerStatsServerModule module using union.xml, edit union.xml and add the following <module> element under the <modules> element.  (If the <modules> element does not exist, first add it under the root element <union>.)

<module>
        <id>serverStatsServerModule</id>
        <source type="class">net.user1.union.servermodule.ServerStatsServerModule</source>
        <attributes>
            <attribute name="interval">300</attribute>
        </attributes>
</module>

Alternatively, to deploy the ServerStatsServerModule module at runtime via server-side code, create a ModuleDef instance, and pass it to the Server instance's createModule() method.

To check whether the module deployed successfully, search log.txt for ServerStatsServerModule. If the module is deployed, log.txt will include the following message:

INFO - Loaded module type [class] with code [net.user1.union.servermodule.ServerStatsServerModule]