Introduction
A framework for plug-in loading and a way to load plug-ins in a development environment that doesn't require redeployment of the plug-ins after each change.
Background
A plug-in is a component (jar) that doesn't compile with the host application, meaning that the host application operates independently of the plug-ins, making it possible to add and update plug-ins dynamically without needing to make changes to the host application.
This architecture complicates the development process when you develop both the plug-in and the host application, since whenever you make a change in the plug-in, it requires to redeploy it on the host application.
The DNA of working in TDD/BDD methodologies is running tests all the time, which means redeploy for each test run.
Therefore, we are loading the plug-ins from their target path in development, instead of loading them from their deployment location.
This way, whenever a change is compiled in the plug-in, on the next run of the host application, it will contain the change, like a regular dependency.
Using the Code
The following code demonstrates how to add libraries to classpath and load plug-ins in runtime in production and development.
Starting with the Tests
public class TestPluginLoader extends TestPluginLoaderBase {
@Test
public void testLoad() throws InstantiationException, IllegalAccessException {
testLoad(new PluginLoader());
}
}
public class TestBinPluginLoader extends TestPluginLoaderBase {
@Test
public void testLoad() throws InstantiationException, IllegalAccessException {
testLoad(new BinPluginLoader());
}
}
public abstract class TestPluginLoaderBase {
private final ClasspathCaretaker _classpathCaretaker = new ClasspathCaretaker();
@Before
public void setUp() {
_classpathCaretaker.save();
}
@After
public void teardown() {
_classpathCaretaker.restore();
}
protected void testLoad(PluginLoaderBase loader) throws InstantiationException,
IllegalAccessException {
Set<Class<? extends Plugin>> plugins = loader.load(Paths.get("plugins"));
Collection<String> expected = new ArrayList<>();
expected.add("plugin-helicopter");
expected.add("plugin-rollerblade");
ClasspathUtils.print();
Assert.assertEquals(plugins.toString(), expected.size(), plugins.size());
for (String currName : expected) {
Assert.assertTrue(isExist(plugins, currName));
}
}
private boolean isExist(Set<Class<? extends Plugin>> plugins, String name)
throws InstantiationException, IllegalAccessException {
boolean ret = false;
for (Class<? extends Plugin> currPlugin : plugins) {
if (currPlugin.newInstance().getId().equals(name)) {
ret = true;
break;
}
}
return ret;
}
}
Implementation
public class PluginLoader extends PluginLoaderBase {
@Override
public Set<Class<? extends Plugin>> load(Path pluginsDir) {
Path pluginDir = getPluginDir(pluginsDir);
new ClasspathAppender().add(pluginDir, ".jar");
return getPlugins();
}
private Path getPluginDir(Path pluginsDir) {
Path ret = DirectoryUtils.getDirectory(pluginsDir.toString());
if (ret == null) {
throw new RuntimeException(String.format(
"Failed to load plugins, directory not found on: %s",
pluginsDir));
}
return ret;
}
}
public class BinPluginLoader extends PluginLoaderBase {
@Override
public Set<Class<? extends Plugin>> load(Path pluginsDir) {
final String FILTER = "plugin-*";
Path root = DirectoryUtils.getDirectory("inspector-gadget-src");
Path suffix = Paths.get("bin");
new ClasspathAppender().add(root, FILTER, suffix);
return getPlugins();
}
}
public abstract class PluginLoaderBase {
public abstract Set<Class<? extends Plugin>> load(Path pluginsDir);
protected Set<Class<? extends Plugin>> getPlugins() {
Set<Class<? extends Plugin>> ret = null;
ResourceFinder finder = new ResourceFinder("META-INF/services/");
try {
ret = RuntimeUtils.cast(new HashSet<>(finder.findAllImplementations(Plugin.class)));
} catch (Throwable thrown) {
throw new RuntimeException(thrown);
}
return ret;
}
}