Reputation: 417
The goal is to load a bunch of jar files as plugins from a remote location, initialize them inside CDI context.
Then a servlet can fire events like like so:
testEvent.fire(new EventTest("some message"));
Which the plugin will be able to observe. An example plugin will look something like this:
public class Plugin{
public void respond (@Observes EventTest e){
//does something with the even object
}
}
Here is the code that supposedly loads the plugin. Taken and reworked from https://jaxenter.com/tips-for-writing-pluggable-java-ee-applications-105281.html This class sits in the same package as the servlet class. It has the necessary META-INF/services directory with the javax.enterprise.inject.spi.Extension file that has a single line - the fully qualified name of the extension class: main.initplugins.InitPlugins .
package main.initplugins;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.Statement;
import java.util.jar.JarInputStream;
import java.util.jar.JarEntry;
import java.lang.ClassLoader;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.BeanManager;
public class InitPlugins implements javax.enterprise.inject.spi.Extension{
Logger log = Logger.getLogger("");
private java.util.Set<Class<?>> classes;
public void beforeBeanDiscovery(@Observes BeforeBeanDiscovery bbd, BeanManager bm){
log.log(Level.INFO, "LOAD PLUGINS HERE");
loadFromFiles();
try{
for (Class<?> cl: classes){
final javax.enterprise.inject.spi.AnnotatedType<?> at = bm.createAnnotatedType(cl);
bbd.addAnnotatedType(at);
log.log(Level.INFO, "ADD ANNOTATED TYPE FOR: " + cl.getName());
}
log.log(Level.INFO, "ANNOTATED TYPE CREATION COMPLETE");
} catch (Exception ex){
log.log(Level.INFO, "FAIL TO CREATE ANNOTATED TYPE: " + ex.getMessage());
}
}
public void loadFromFiles() {
classes = new java.util.LinkedHashSet<Class<?>>();
try{
//connect to a remote location. In this case it will be a database that holds the bytes of the .jar files
Connection dbConnection = java.sql.DriverManager.getConnection("jdbc:mysql://localhost/testdb?user=user&password=passwd");
Statement statement = dbConnection.createStatement();
java.sql.ResultSet plugins = statement.executeQuery("select * from plugins"); //the plugins table contain 2 columns: 1) fileName as primary key, 2) longblob that hold raw byte of the jar file
while (plugins.next()){
JarInputStream js = new JarInputStream(new java.io.ByteArrayInputStream(plugins.getBytes(2))); //load them as jar files, 2 is the index for the raw byte column that holds the jar file
JarEntry je;
while((je = js.getNextJarEntry()) != null){
//open each jar file, scan through file contents and find the .class files, then extract those bytes and pass them in the ClassLoader's defineClass method
if(!je.isDirectory() && je.getName().endsWith(".class")){
String className = je.getName().substring(0, je.getName().length() - 6).replace("/", ".");
log.log(Level.INFO, "class name is: " + className);
java.io.ByteArrayOutputStream classBytes = new java.io.ByteArrayOutputStream();
byte[] bytes;
try{
byte[] buffer = new byte[2048];
int read = 0;
while(js.available() > 0){
read = js.read(buffer, 0, buffer.length);
if(read > 0){
classBytes.write(buffer, 0, read);
}
}
bytes = classBytes.toByteArray();
//code below taken from: https://jaxenter.com/tips-for-writing-pluggable-java-ee-applications-105281.html
java.security.ProtectionDomain protDomain = getClass().getProtectionDomain();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Method tempDefineClassMethod = null;
for (Method tempMethod : ClassLoader.class.getDeclaredMethods()){
if(tempMethod.getName().equals("defineClass") && tempMethod.getParameterCount() == 5){
tempDefineClassMethod = tempMethod;
break;
}
}
final Method defineClassMethod = tempDefineClassMethod;
try{
java.security.AccessController.doPrivileged(new java.security.PrivilegedExceptionAction(){
@Override
public java.lang.Object run() throws Exception{
if (!defineClassMethod.isAccessible()){
defineClassMethod.setAccessible(true);
}
return null;
}
});
log.log(Level.INFO, "Attempting load class: " + className + " with lenght of: " + bytes.length);
defineClassMethod.invoke(cl, className, bytes, 0, bytes.length, protDomain);
classes.add(cl.loadClass(className));
log.log(Level.INFO, "Loaded class: " + je.getName());
} catch (Exception ex){
log.log(Level.INFO, "Error loading class: " + ex.getMessage());
ex.printStackTrace();
}
} catch (Exception ex){
log.log(Level.INFO, "Error loading bytes: " + ex.getMessage());
}
}
}
}
} catch (SQLException ex){
log.log(Level.SEVERE, "Fail to get db connection or create statement in plugin ejb: ".concat(ex.getMessage()));
} catch (Exception ex){
log.log(Level.SEVERE, "Fail to get db connection or create statement in plugin ejb: ".concat(ex.getMessage()));
}
}
}
And it doesn't work for some reason. No errors are thrown at any stage. When i fire the event from the servlet, the loaded plugin doesn't pick it up. What am i doing wrong?
Upvotes: 0
Views: 798
Reputation: 6753
From CDI standpoint, your approach should work just fine.
The problem here is class loading, especially when considering any non-flat deployment (anything else than pure SE).
You chose to use TCCL, e.g. you did:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Which, in certain application servers/servlets, might give you a different class loader than the one that loaded the extension itself (InitPlugin
).
Instead, you should use the same CL which loaded the extension as that is going to be the one handling CDI beans. So, just do this:
ClassLoader cl = InitPlugins.class.getClassLoader()
NOTE: Be aware that you are sailing undefined waters. This behavior/fix will likely not be portable.
Upvotes: 2