Java Extensions: in-depth overview
In this article we're going to dive deeper into the details and options available when developing server side Extension. If you have skipped the first two chapters in this section we highly recommend to start from there before continuing.
Deployment overview
As seen in the quick start guide, Java Extensions are deployed as .jar file(s) to a folder which represents the name of the Extension itself. Below is the main extensions/ folder which acts as the root for all Extensions in the server. Our MyFirstExtension folder contains one jar file with all our code.
There are a few more things to notice:
- The name of the .jar file does not need to match the name of its containing folder. Any name will do.
- You can deploy multiple jar files under the same Extension folder, for example dependencies or other libraries. They will all be loaded under the same Class Loader.
- There is a special folder called _shared/ where you can put dependencies as well. This way you can choose which libraries are shared across multiple Extensions and which are local to a specific Extension.
Both approaches can be useful depending on what you need to do:
- Libraries deployed under your Extension folder will be loaded in the Extension's own Class Loader. This means that dependencies are local to your Extension and won't affect other Extensions in the server.
- Libraries deployed under the _shared folder are loaded in the Extension's parent Class Loader. This means that dependencies are globally available to all Extensions. If you change any of these, it will affect all Extensions that use them, but you will save memory as each library is loaded only once.
As a general rule of thumb we recommend deploying Extension's dependencies under _shared. This will save a significant amount of memory. For more details on how class loading works in SmartFoxServer you can take a look at this article.
Custom Configuration
An Extension can auto-load a .properties file with custom settings that are immediately available in your code. By default the server attempts to load a file called config.properties. You can also specify a different file name, if necessary. This can be useful when attaching the same Extension to multiple Zones (or Rooms) and you need to pass different settings to each one.
Here is an example of how an Extension is configured via the AdminTool (see the Zone Extension tab or Room Extension tab in the Zone Configurator module documentation for additional information):

Where:
- Name refers to the name of the folder containing the code;
- Type indicates the type of Extension in use;
- Main class is the fully qualified name of the main Extension class (or script file name, in case of Groovy, for example), the one implementing the init() method.
- Use naming convention is not an Extension setting: it just activates/deactivates a filter on the Main class field to show only those classes whose name ends with the suffix "Extension";
- Properties file is an optional name of a .properties file deployed in the Extension folder containing custom Extension settings (default is config.properties);
- Reload mode indicates if the Extension is monitored and auto-reloaded (AUTO), reloaded manually (MANUAL) or not reloadable (NONE).
Extension reloading
SmartFoxServer provides Extension hot-reloads, to assist in speeding up the dev and test phases. When this feature is turned on (see the Custom configuration paragraph above), the server will monitor your Extension folders and reload the code when a jar or script file is re-deployed.
Logging and extra dependencies
You can log messages directly to the main SmartFox log files (located under server/logs/smartfox.log) using the trace() method, as in this example:
Also you can use the Extension's internal logger:
With getLogger() you can choose between various log levels, and more advanced formatting options.
If you need to further customize the logging format and behavior you can work with the SmartFox logging API based on SLF4J and LogBack 2.x by importing a few extra libraries provided in the installation.
For more information check how to customize the logging system in SmartFoxServer.
Extension interoperability
Sometimes Extensions may need to talk to each other. The simplest way to achieve this is by implementing and calling the handleInternalMessage() method, available from the parent class. This is what the method looks like:
This is pretty flexible as we can pass any command name and parameter object and obtain any object in return. Here is an implementation example at Zone level:
This is how a Room Extension can call the test command on the Zone Extension:
Note:
Since objects are cast from the top level java.lang.Object type to whatever types are used in these calls, it is important to add extra care in making sure that parameters are cast correctly on both ends of the communication. Cast errors can't be detected at compile time, so accurate testing is important.
The destroy() method
Finally we need to take a look at the destroy() method which is called by the server when an Extension is shutting down. This happens every time an Extension is re-deployed at Zone Level, or when a Room is removed from the system and the relative Extension needs to be shut down.
Technically it is not mandatory to implement the destroy() method, in fact you can deploy an Extension without implementing it and everything will work just fine. However there are several exceptions where overriding destroy() is required: if you're working with files, databases or any other resource that has a long life-cycle you will need to keep track of those and release them in the overridden destroy().
When doing so, always remember to also call the parent destroy() method, since it's responsible for a lot of internal cleaning as well.
Here's a typical use case for a custom destroy() method:
public class SchedulerTestExtension extends SFSExtension
{
private class TaskRunner implements Runnable
{
private int runningCycles = 0;
public void run()
{
try
{
runningCycles++;
trace("Inside the running task. Cycle: " + runningCycles);
}
catch (Exception e)
{
// Handle exceptions here
}
}
}
ScheduledFuture<?> taskHandle;
@Override
public void init()
{
// Schedule the task to run every second, with no initial delay
taskHandle = getApi().getScheduler().scheduleAtFixedRate(new TaskRunner(), 0, 1, TimeUnit.SECONDS);
}
@Override
public void destroy()
{
super.destroy();
if (taskHandle != null);
taskHandle.cancel();
}
}
In our init() we create a repeating task running every second. If we don't cancel it manually, the task will keep running even after the Extension has been destroyed, causing a memory leak. To avoid this we make sure to release it in our overridden destroy().
Server side API
In addition to responding to requests and events a server Extension can access a wide variety of API ranging from handling User or Room states, to setting server Variables, managing Buddies, accessing databases. It is a very long list and you can consult it via the server-side API Javadoc for all the details. In particular see the SFSApi class, as a starting point.
We also talk about the server side API in this article.
Game state and thread safety
Every Extension has usually the need to keep some application state between calls. Since the server uses non-blocking I/O and virtual thread to scale with your hardware, concurrency is a constant in SmartFoxServer.
This means is that multiple requests from clients can access your game state from different threads and you'll need to make sure that it remains consistent and doesn't get corrupted. This is particularly true for fast real-time games where the server can handle tens of thousands of requests per second.
The Java language and its "cousins" (Kotlin, Scala, Groovy etc...) provide a rich set of tools to manage concurrency. In particular
- The volatile keyword
- Locks
- Atomic variables
- Concurrent collections
We recommend checking these topics if you're unfamiliar with them. In any case you'll see these tools in action in some of the more advanced examples we provide.
Multi handlers
In order to properly organize request names in large application we provide a convention similar to that of namespaces in Java or C#, using a dot syntax. Suppose an Extension can handle a number of games and other operations such as user registration and profile editing. We could organize these requests in various groups, such as:
register.submitForm
register.passwordLost
register.changeEmail
register. ...
profile.changeAvatarType
profile.changeNick
profile.getAvatar
profile. ...
checkers.sendMove
checkers.getMyScore
checkers.leaveGame
checkers. ...
The Extension API provides a @MultiHandler annotation that can be added to your handler classes. This will register the class for all requests starting with a certain prefix. Let's see a multi-handler example for the register requests we just talked about:
@MultiHandler
public class RegisterMultiHandler extends BaseClientRequestHandler
{
@Override
public void handleClientRequest(User sender, ISFSObject params, TransportType txType)
{
// Obtain the request custom name
String command = params.getString(SFSExtension.MULTIHANDLER_REQUEST_ID);
if (command.equals("submitForm"))
handleSubmitForm(sender, params);
else if (command.equals("changeEmail"))
handleChangeEmail(sender, params);
// ... etc ...
}
private void handleSubmitForm(User sender, ISFSObject params)
{
// ... app logic here
}
private void handleChangeEmail(User sender, ISFSObject params)
{
// ... app logic here
}
}
At the line highlighted above we obtain the sub-command (e.g. register.submitForm) that was sent and proceed with dispatching the call to the appropriate method.
Now we register the class in the init method of the Extension as we have already seen before.
The only difference here is that the handler class is marked as @MultiHandler. When this is done, the Extension dispatcher invokes the handler on any request id starting with the register prefix. In other words it will handle any register.[something] request.
Note:
You are not limited to a single "dot" in the request name. You could have multiple nested levels, such as: games.spacewars.fireBullet or user.profile.avatar.getHairColor, etc. The only recommendation is to keep these request names reasonably short since they will be transmitted with the request/response objects.
Instantiation annotations
We provide several useful annotations that can be used to specify how your handler classes should be instantiated. There are two main behaviors that can be specified using the @Instantiation annotation on your classes:
- @Instantiation(NEW_INSTANCE): creates a new instance on every call
- @Instantiation(SINGLE_INSTANCE): uses the same (cached) instance for all calls
The default behavior is SINGLE_INSTANCE which means that an event/request handler is instantiated once and cached locally to be reused as many times as necessay, thus having minimal memory impact.
Extension Filters
If you are familiar with the Java Servlet API this will probably sound familiar. Extension Filters in SmartFoxServer are inspired by servlet filters and they serve a similar purpose: they are executed in a chain and they can be used to log, filter, or handle specific requests or events before they get to the Extension itself.
The advantage of pluggable Filters is that they don't get in the way of your Extension code, their execution order can be altered and they can even stop the execution flow, if necessary. An example of this could be a custom ban filter where user credentials are checked against a black list before the request is passed to your Login Handler.
This is an example of a simple Extension filter:
public class CustomFilter extends SFSExtensionFilter
{
@Override
public void init(SFSExtension ext)
{
super.init(ext);
trace("Filter inited!");
}
@Override
public void destroy()
{
trace("Filter destroyed!");
}
@Override
public FilterAction handleClientRequest(String cmd, User sender, ISFSObject params)
{
// If something goes wrong you can stop the execution chain here!
if (cmd.equals("BadRequest"))
return FilterAction.HALT;
else
return FilterAction.CONTINUE;
}
@Override
public FilterAction handleServerEvent(ISFSEvent event)
{
return FilterAction.CONTINUE;
}
}
Filters can be easily added to any Extension at configuration time or dynamically, at runtime:
When a new request or event is sent to your Extension it will first traverse the Filters Chain in the order in which filters were added. In the example above it will be: customLoginFilter » pubMessageFilter » privMessageFilter » Extension.