hrs
hrs

Reputation: 417

java CDI Extension with dynamic class loading

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

Answers (1)

Siliarus
Siliarus

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

Related Questions