Matteo Joliveau
Posted on April 9, 2018
Visit https://plugface.matteojoliveau.com for the full documentation
One year ago I came across the need to dynamically load bits of code into my Java applications without having to recompile or repackage my software every time. Basically, I was looking for a plugin system.
There was only one open source plugin system worth noting at the time, PF4J, but I felt it was a bit clunky and cumbersome. I wanted something dead simple, that I could use to be productive in minutes. So I decided to write my own.
Enter PlugFace.
And the first iteration was sh*tty as hell, because at the time I was just an intern at my company, still learning real-world Java and didn't have the knowledge to write a good, usable framework.
The Ugly
The first implementation was not so simple as I wanted it to be.
There were interfaces to implement, reflection to use, configuration files to write, a sandboxed permission system, a half-working/half-broken attempt at dependency injection between plugins, and a lot of sweat and tears from me to both write it and use it.
Not the greatest software in the world.
Here is an example of an old PlugFace plugin:
package org.plugface.demo.plugins.greet;
import org.plugface.Plugin;
import org.plugface.PluginConfiguration;
import org.plugface.PluginStatus;
import org.plugface.demo.app.sdk.Greeter;
import org.plugface.impl.DefaultPluginConfiguration;
import java.util.Collections;
public class GreeterPlugin implements Plugin<String[], String>, Greeter{
private final String name;
private PluginConfiguration configuration;
private PluginStatus status;
private boolean enabled;
public GreeterPlugin () {
name = "greeter";
configuration = new DefaultPluginConfiguration();
status = PluginStatus.READY;
enabled = false;
}
@Override
public void start() {
throw new UnsupportedOperationException("This plugin operates in single mode only");
}
@Override
public void stop() {
throw new UnsupportedOperationException("This plugin operates in single mode only");
}
@Override
public String execute(String[] parameters) {
return greet();
}
@Override
public PluginConfiguration getPluginConfiguration() {
return (PluginConfiguration) Collections.unmodifiableMap(configuration);
}
@Override
public void setPluginConfiguration(PluginConfiguration configuration) {
this.configuration = configuration;
}
@Override
public String getName() {
return name;
}
@Override
public PluginStatus getStatus() {
return status;
}
@Override
public void setStatus(PluginStatus pluginStatus) {
status = pluginStatus;
}
@Override
public void enable() {
this.enabled = true;
}
@Override
public void disable() {
this.enabled = false;
}
@Override
public boolean isEnabled() {
return enabled ;
}
@Override
public String greet() {
return "Hello PlugFace!":
}
}
These things were monsters, the interface was huge and full of debatably useful features (like status and name) and they were not practical to implement.
If you wanted more complex behaviors than 'start', 'stop' and 'execute', you had to add your own interfaces and cast your plugin to that type in order to use it or access the methods via reflection.
Then an idea stroke me. Why bother restricting the way a user interacts with a plugin when all the framework could deal with was the dynamic class loading?
The Good
With version 0.6, the framework revived. It was an (almost) complete rewrite. I ditched the whole interface story for a much cleaner @Plugin("name")
annotation and ZERO opinions on how your plugin shall behave. Simply make it implement some interface your application is aware of and then access it through it.
Here is the same plugin but with the new system:
package org.plugface.demo.plugins.greet;
import org.plugface.core.annotations.Plugin;
import org.plugface.demo.app.sdk.Greeter;
@Plugin("greeter")
public class GreeterPlugin implements Greeter {
@Override
public String greet() {
return "Hello PlugFace!";
}
}
It implements the Greeter
interface provided by my application. Now my code can simply retrieve the plugin an put it in a variable of type Greeter without ever have to be aware of the GreeterPlugin
class. Neat!
Here is my application code.
final PluginManager manager = PluginManagers.defaultPluginManager();
manager.loadPlugins(PluginSources.jarSource("path/to/my/plugin/jars"));
final Greeter greeter = manager.getPlugin(Greeter.class);
greeter.greet(); // => "Hello PlugFace!"
The PluginManager
is a utility class to load plugins from various sources, another improvement over the old version which only featured hard-coded JAR loading, and accesses the PluginContext
to add/get/remove plugins both by name and by type.
I also entirely ditched the Sandbox functionalities, which leveraged the Java SecurityContext features to restrict plugins in what they were allowed to do. As it was a huge pain to deal with, I preferred to remove it entirely to focus on the core features for this new version. I might reintroduce it in a future version if I find a way to implement it in a convenient fashion.
Dependency Injection done right
A feature the old system tried to introduce was the possibility of having plugins injected into each other. It achieved this by putting plugins into the PluginConfiguration
object (which was basically a glorified Map<String, Object>
, it worked but was fairly unpractical to use.
PlugFace 0.6 introduced a full-fledged dependency injection system via constructors, using the standard @javax.inject.Inject
annotation (which will be made optional in the future in case of a single constructor being present) so that plugin classes can be used with any standard DI framework such as Spring or Guice.
It supports complicated dependency graphs using topological sorting, with circular dependencies detection and all that good stuff.
package org.plugface.demo.plugins.math;
import org.plugface.core.annotations.Plugin;
import org.plugface.demo.app.sdk.Mathematics;
import javax.inject.Inject;
@Plugin("math")
public class MathPlugin implements Mathematics {
private final SumPlugin sum;
private final MultPlugin mult;
@Inject
public MathPlugin(SumPlugin sum, MultPlugin mult) {
this.sum = sum;
this.mult = mult;
}
@Override
public Integer sum(int a, int b) {
return sum.sum(a, b);
}
@Override
public Integer mult(int a, int b) {
return mult.mult(a, b);
}
}
Spring Integration
The Spring Framework is a tremendous toolkit in Java. It features pretty much anything, but its core feature is clearly the dependency injection system. Since I use Spring a lot in nearly all my projects, I wanted PlugFace to be well integrated with it.
If you want to use plain vanilla PlugFace you can import the plugface-core
module, but if using Spring you may want to switch to the special plugface-spring
module.
Not only it sports auto-configured beans for both the PluginManager and the PluginContext, but it also adds support for plugin retrieval from the ApplicationContext
(meaning if an object is not found as a plugin, it will be looked up amongst Spring beans) and, perhaps more importantly, it makes Spring beans viable targets for dependency injection in plugins.
That's right, with plugface-spring
you can not only inject other plugins inside your plugins, but also any Spring bean you have registered in your application.
Your plugins can access service classes, repositories, utility singletons, basically anything lives inside the Spring context.
package org.plugface.demo.plugins.user;
import org.plugface.core.annotations.Plugin;
import org.plugface.demo.app.sdk.Greeter;
import org.plugface.demo.app.sdk.TestService;
import javax.inject.Inject;
@Plugin("greeter")
public class UserPlugin implements UserDetails {
private final UserService userService; //this is a Spring service
@Inject
public GreeterPlugin(UserService userService) {
this.userService = userService;
}
@Override
public String getUsername() {
final User user = userService.getUserById(0L);
return user.getUsername();
}
}
From Now On
In the future, I want to integrate with other DI frameworks as well, such as Guice, and expand upon the current feature set if any new use case for plugins in Java comes up.
Until then, thanks for the attention, and happy coding!
Posted on April 9, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.