Lesson 10 - Java Server - Plugin System
In the previous lesson, Java Server - Event bus, we focused on propagating events across the server using an event bus.
Today we're going to design and implement a system with which we can extend server functionality easily.
Plugins
To begin with, it's a good idea to define what a plugin is. Plugins will represent individual server functionalities. Under the term server functionality you can imagine:
- user management
- database access
- chat
- communication with an external service
- and more...
Designing the Plugin System
Our plugin system will be relatively simple. It's task will be to load all available plugins and initialize them when the server starts. In the future, we'll improve this system with loading plug-ins from external jar files so we don't have to recompile the server every time someone wants to add a new feature.
Implementation
We'll start the implementation by designing an interface that will represent
a plugin. We'll name the interface IPlugin
and put it in a separate
plugins
package. The interface will contain a
getName()
method, which, as its name implies, will return the
plugin's name. Next, there will be an init()
method in which the
plugin will be initialized. Using a registerMessageHandlers()
method, the plugin will be able to register all events to which it'll respond in
the future. In a setupDependencies()
method, it'll be possible to
wire individual plugins together. The whole interface looks like this:
package cz.stechy.chat.plugins; public interface IPlugin { String getName(); void init(); void registerMessageHandlers(IEventBus eventBus); void setupDependencies(Map<String, IPlugin> otherPlugins); }
In the same package, we'll create an enumeration that will include all
plugins embedded in the server. Let's name the enum
simply
Plugin
. The enumeration will contain a constant of the
Class<? extends IPlugin>
type. Remember to add a
constructor:
public enum Plugin { ; public final Class<? extends IPlugin> clazz; Plugin(Class<? extends IPlugin> clazz) { this.clazz = clazz; } }
This constant will refer to the class that implements the plugin. For now,
the enum
is empty, so a semicolon is needed at the beginning.
We'll now register all modules in Google guice to instantiate them
automatically. We'll create a new PluginModule
module inherited
from the AbstractModule
class provided by the guice library and
implement the configure method:
public class PluginModule extends AbstractModule { @Override protected void configure() { MapBinder < String, IPlugin > pluginBinder = MapBinder.newMapBinder(binder(), String.class, IPlugin.class); for (Plugin plugin: Plugin.values()) { pluginBinder.addBinding(plugin.name()).to(plugin.clazz).asEagerSingleton(); } // TODO load external plugins } }
In this method we'll prepare a pluginBinder
variable, which
we'll use to bind all plugin implementations with our IPlugin
interface. More about MapBinder
implementation can be found on wiki guice. We'll
do the binding in a loop in which we iterate through the plugin
enum
embedded directly in the server. It's worth mentioning just to
call the asEagerSingleton()
method, which says that whenever we
request a specific plugin instance, we always get the same one. We'll keep
loading external plugins for later lessons.
Next, we'll register the newly created module in guice. We'll add a new
PluginModul
module in the Server
class, in the
main()
method, where the Injector
class is
instantiated:
final Injector injector = Guice.createInjector(new ServerModule(), new PluginModule());
Finally, we'll edit the Server
class. We'll add constants of the
Map<String, IPlugin>
and IEventBus
types. The
class will accept these constants in the constructor:
@Inject public Server(IParameterFactory parameterFactory, IServerThreadFactory serverThreadFactory, IEventBus eventBus, Map<String, IPlugin> plugins) { this.parameterFactory = parameterFactory; this.serverThreadFactory = serverThreadFactory; this.eventBus = eventBus; this.plugins = plugins; }
Now we have to initialize the plugins. To do this, we'll create a private
initPlugins()
method to take care of the initialization:
private void initPlugins() { for (IPlugin plugin: plugins.values()) { plugin.init(); } for (IPlugin plugin: plugins.values()) { plugin.registerMessageHandlers(messageRegistrator); } for (IPlugin plugin: plugins.values()) { plugin.setupDependencies(plugins); } }
First, we call the init()
method over all plugins and give them
the opportunity to initialize. Next, we register listeners for the corresponding
events and finally let the plugins set their dependencies on each other. We'll
call the initPlugins()
method before the server thread starts.
Test Plugin
For demonstration purposes, we'll create a simple plugin that will do nothing
but write its name to the console during the initialization phase. In the
plugins package, we'll create another package named hello
. In this
package, we'll create a HelloPlugin
class that will implement our
IPlugin
interface. We'll implement the required methods as
follows:
package cz.stechy.chat.plugins.hello; public class HelloPlugin implements IPlugin { @Override public String getName() { return "HelloPlugin"; } @Override public void init() { System.out.println("Plugin initialization: " + getName()); } }
We'll add the plugin to the Plugin
enum
. Keep in
mind that it's not enough to just specify the name, but also the class that
implements the plugin:
public enum Plugin { HELLO(HelloPlugin.class); ... }
That's all. When you start the server, you get a message that the plugin has been initialized.
That would be all for today.
In the next lesson, Java Server - Local Area Network Propagation (Part 1), we'll prepare the basis for making the server visible in the local network.