Making the plugin loader ready for being used with Spring Boot and Docker

in plugin •  4 years ago 

Original creation date: 4th August 2020
Note: Originally I posted this on my Wordpress Blog (https://1337codersblog.wordpress.com), but I decided to switch to steemit, so I copied it from there.

Making the plugin loader ready for being used with Spring Boot and Docker

In the last weeks I refactored my trading system to use Spring Boot and splitted it into Microservices. I used the Spring Boot Maven Plugin to create a JAR file containing all dependencies including Spring Boot, so that the service can be run by simply executing java -jar <path-to-jar-file>. While this I found out that everything works fine when I execute the application from within Intellij, but when run from the command line the compiler fails to compile plugins, because it is not able to find imported classes that are contained in Maven dependencies.

In the debugging process I found out that when run from Intellij all Maven dependency packages are mentioned in the class path. When run from the command line the class path only contains the location of the applications JAR file. Another part of the problem is that the Spring Boot Maven Plugin does not unpackage the Maven dependencies but places the artifacts JAR files into BOOT-INF/lib/ within the created JAR.

First I tried to solve this by looking up the byte code of the needed classes by their class instances from within the application. But it turned out to be really hard to find a way for providing the byte codes to the compiler, so I decided that this approach takes too much effort for my task.

In the next step I thought about using the ServiceLoader API, so I created a JAR file for the test plugin with the correct file structure for the ServiceLoader. But when I tried to run the application from the command line I found out that it failed to find the plugin, because when running a JAR file from command line the -classpath argument is ignored. Adding the plugins JAR file to the class path from within the application also failed.

So I decided to make the plugin loader capable of loading such plugin JAR files and this solved my problem.

First I added a function to the plugin loader that takes a JarInputStream and the interface class that the plugin must implement and returns the instance of the loaded plugin:

public <OBJECT> OBJECT load(JarInputStream jarInputStream, Class<OBJECT> pluginInterface) throws LoadingException{

I use a JarInputStream, because it allows me the provide the JAR from a memory location, for example after having been loaded from a database.

JarInputStream does not allow random access to the content of the JAR file, so first the complete stream has to be read and all files have to be cached:

Map<String, byte[]> entries = new HashMap<>();

try {
    JarEntry jarEntry = jarInputStream.getNextJarEntry();
    while (jarEntry != null){
        if (!jarEntry.isDirectory()){
            entries.put(jarEntry.getRealName(), jarInputStream.readAllBytes());
        }
        jarEntry = jarInputStream.getNextJarEntry();
    }
} catch (IOException exception) {
    throw new LoadingException(exception);
}

Then the loader looks for a file within META-INF/services named equal to the full qualified class name of the interface the plugin has to implement. This file specifies the full qualified class name of the plugin that is to be loaded:

String pluginClassSpecificationFile = String.format("META-INF/services/%s", pluginInterface.getCanonicalName());
if (!entries.containsKey(pluginClassSpecificationFile)){
    throw new WrongFileFormatException(String.format("Expected JAR to contain file '%s'", pluginClassSpecificationFile));
}

Now the class name of the plugin to be loaded is read from the file:

List<String> pluginClassSpecificationFileLines = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(entries.get(pluginClassSpecificationFile)))).lines().collect(Collectors.toList());
if (pluginClassSpecificationFileLines.isEmpty()){
    throw new WrongFileFormatException(String.format("File is empty: %s", pluginClassSpecificationFile));
}
if (pluginClassSpecificationFileLines.size() > 1){
    throw new WrongFileFormatException(String.format("File contains more than one lines: %s", pluginClassSpecificationFile));
}
String pluginClassName = pluginClassSpecificationFileLines.get(0);

Now the byte code of all classes in the loaded JAR can be added to the plugin loaders classloader so that they can be used by the application. This also includes the byte code of the plugin to be loaded:

PluginClassLoader pluginClassLoader = new PluginClassLoader();
entries.entrySet().stream()
        .filter(file -> file.getKey().endsWith(".class"))
        .forEach(file -> {
            String className = file.getKey()
                    .replace(".class", "")
                    .replace("/", ".");
            pluginClassLoader.putClassCode(className, file.getValue());
        });

And now the loader creates an instance of the plugin class and returns it to the caller:

try {
        Class<?> pluginClass = pluginClassLoader.findClass(pluginClassName);
        return createInstance(pluginInterface, pluginClass);
    } catch (ClassNotFoundException e) {
        throw new LoadingException(String.format("Did not find class to load: '%s'\nPlease check if it is contained in the provided JAR", pluginClassName));
    }
}

Now you just have to call the load function of the plugin loader and you are ready to use the plugin. You can find the complete source code in my Gitlab repository.

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!