Original creation date: 30th September 2019
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.
Implementing a plugin library for a Java application
Currently I’m working on a trading system and there I need a web crawler to extract some information. The problem is that the crawling of the page itself depends on the page that is visited because the HTML code of every page in the internet is different. So it is not possible to embed this into the trading system itself. Instead it has to be provided by the user of the trading system. I came to the consideration that I would have to do this with a plugin system.
For security reasons, the trading platform should not accept every class that is given by the user. Instead it only shall accept classes that extend a specified super class or implement a specified interface. The most flexible way to achieve this goal is to let the user provide either a compiled class file or a Java source code file (which then would be compiled to a class file by the trading system) and dynamically load the given class when it is needed.
I created the plugin handler class and defined two load functions. The first one takes a String and the class the plugin has to extend/implement. The second one takes a path to a class file and the class the plugin has to extend/implement:
public class PluginLoader {
public static class LoadingException extends Exception{
public LoadingException(String message) {
super(message);
}
public LoadingException(Throwable cause) {
super(cause);
}
}
public <OBJECT> OBJECT load(String plugin, Class<OBJECT> pluginInterface) throws LoadingException {
}
public <OBJECT> OBJECT load(Class<OBJECT> pluginInterface, File plugin) throws LoadingException {
}
}
OBJECT is a generic parameter, so the caller of the load function knows of which type the result is. You also could use Object as type instead, but then the caller would have to cast the result to the class provided by the pluginInterface parameter.
Add maven dependencies to pom.xml:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>3.14.159265359</version>
</dependency>
First parse class name from the given Java source code string:
private String parseClassName(String plugin) throws LoadingException {
JavaParser javaParser = new JavaParser();
ParseResult<CompilationUnit> parseResult = javaParser.parse(plugin);
if (!parseResult.isSuccessful()){
StringBuilder message = new StringBuilder();
message.append("Parsing source file failed:\n");
parseResult.getProblems().forEach(problem -> message.append(String.format("%s\n", problem.getVerboseMessage())));
throw new LoadingException(message.toString());
}
assert parseResult.getResult().isPresent();
final Set<TypeDeclaration<?>> publicTopLevelClasses = parseResult.getResult().get().getTypes().stream()
.filter(TypeDeclaration::isTopLevelType)
.filter(typeDeclaration -> !typeDeclaration.getModifiers().contains(Modifier.privateModifier()))
.filter(typeDeclaration -> !typeDeclaration.getModifiers().contains(Modifier.protectedModifier()))
.collect(Collectors.toSet());
if (publicTopLevelClasses.isEmpty()){
throw new LoadingException("Did not found a public top level class in the source code");
}
if (publicTopLevelClasses.size() > 1){
throw new LoadingException("Found multiple public top level classes in the source code");
}
return publicTopLevelClasses.iterator().next().getName().asString();
}
In the first load function we now take the class name and write the content of the given source code string into a Java file:
String className = parseClassName(plugin);
File sourceFile = writeSourceCodeToFile(plugin, className);
private File writeSourceCodeToFile(String sourceCode, String className) throws LoadingException {
File tempDir = new File(Long.toString(System.nanoTime())).getAbsoluteFile();
File sourceFile = new File(tempDir, String.format("%s.java", className)).getAbsoluteFile();
try {
Files.createDirectory(tempDir.toPath());
if (!tempDir.exists()) {
throw new LoadingException("Could not create temp directory: " + tempDir.getAbsolutePath());
}
} catch (IOException e) {
throw new LoadingException("Could not create temp directory: " + tempDir.getAbsolutePath());
}
try{
Files.createFile(sourceFile.toPath());
if (!sourceFile.exists()){
throw new LoadingException(String.format("Could not create temp file: %s", sourceFile.getAbsolutePath()));
}
try(final DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(sourceFile))){
outputStream.writeBytes(sourceCode);
}
} catch (IOException e) {
throw new LoadingException(String.format("Could not create temp file: %s", sourceFile.getAbsolutePath()));
}
return sourceFile;
}
Now compile the class:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, sourceFile.getAbsolutePath());
File classFile = sourceFile.toPath().getParent().resolve(String.format("%s.class", className)).toFile();
Call second load function, delete the created Java source file and class file and return the instance to the caller:
try{
return load(pluginInterface, classFile);
} finally {
deleteTempFolder(sourceFile.getParentFile());
}
private void deleteTempFolder(File tempFolder) throws LoadingException{
try{
FileUtils.deleteDirectory(tempFolder);
} catch (IOException e) {
throw new LoadingException(String.format("Could not delete temp directory: %s", tempFolder.getAbsolutePath()));
}
}
The second load function extracts the class name of the file name:
String className = getClassName(plugin);
private String getClassName(File plugin) {
//first part of the file name is the name of the class
return plugin.getName().substring(0, plugin.getName().indexOf("."));
}
Loads the class:
URL classUrl = plugin.getParentFile().toURI().toURL();
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{classUrl});
classLoader.loadClass(className);
Class<?> compiledClass = Class.forName(className, false, classLoader);
Checks if the class implements the given interface or extends the given class:
if (!pluginInterface.isAssignableFrom(compiledClass)){
throw new LoadingException(String.format("Public class %s in the source code can not be assigned to %s", className, pluginInterface.getCanonicalName()));
}
Instantiates the class and returns it to the caller:
if (Arrays.asList(compiledClass.getConstructors()).parallelStream().noneMatch(constructor -> constructor.getParameterCount() == 0)){
throw new LoadingException(String.format("Public class %s in the source code has no constructor without arguments", className));
}
final Object result = compiledClass.getConstructor().newInstance();
return (OBJECT) result;
You can find the complete source code at https://gitlab.com/marvinh/plugin-system-for-java