Search code examples
javaclassloaderdrools

How do I give Drools access to dynamically loaded classes?


I am trying to use a ClassLoader to load classes from .class files at runtime and use them in Drools rules (Drools 7.52.0). I am using this custom ClassLoader which reads from a file and uses ClassLoader.defineClass() to load a class. It's similar to a URLClassLoader:

package example;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class DynamicClassLoader extends ClassLoader {
    public DynamicClassLoader(ClassLoader parent) {
        super(parent);
    }

    /**
     * Define a class from a .class file and return it
     * @param filepath path to a .class file
     * @return new Class or null if error
     */
    public Class<?> classFromFile(String filepath) {
        try {
            // Read .class file and write bytes to buffer
            BufferedInputStream input = new BufferedInputStream(new FileInputStream(filepath));
            
            // Write file contents to buffer
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int data = input.read();
            while(data != -1){
                buffer.write(data);
                data = input.read();
            }
            byte[] classData = buffer.toByteArray(); // contents of .class file
            
            input.close();

            return defineClass(null, classData, 0, classData.length);
        } catch (IOException | ClassFormatError e) {
            e.printStackTrace();
        }

        return null;
    }
}

I can use the ClassLoader to load a class, construct an instance, and access its methods. However, even though I pass the ClassLoader to Drools through the methods KieServices.newKieBuilder and KieServices.newKieContainer, Drools can't compile the rule. Here is my Main.java:

package example;

import java.io.IOException;

import org.kie.api.builder.model.KieBaseModel;
import org.kie.api.builder.model.KieModuleModel;
import org.kie.api.runtime.KieSession;

import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.Message;

public class Main {
    private static KieServices kServices = KieServices.Factory.get();
    private static KieFileSystem kFileSys;
    private static KieSession kSession;
    private static DynamicClassLoader classLoader = new DynamicClassLoader(Main.class.getClassLoader());
    
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // Load Person class
        classLoader.classFromFile("Person.class");

        // Instantiate Person
        Class<?> personClass = classLoader.loadClass("facts.Person");
        Object person = null;
        try {
            // Instantiate person and access methods using reflection
            person = personClass.getConstructor(String.class, Integer.class).newInstance("Alice", 49);
            System.out.println(person.getClass() + ": name = " +
                    (String) personClass.getMethod("getName").invoke(person));
        } catch (Exception e) {
            System.out.println("Error instantiating person");
            e.printStackTrace();
        }
        
        // Create a KieSession with a rule that uses the Person class
        String drl = String.join("\n",
                "import facts.Person;",
                "rule \"person\"",
                "    when",
                "        $p : Person()",
                "    then",
                "        System.out.println($p.getName());",
                "end"
            );
        initializeKieSession(drl);

        kSession.insert(person);
        kSession.fireAllRules();
    }
    
    /**
     * Create a KieSession using the given ruleset
     * @param drl a ruleset string in Drools Rule Language
     */
    private static void initializeKieSession(String drl) {
        // Create module model
        KieModuleModel kModMod = kServices.newKieModuleModel();
        KieBaseModel kBaseMod = kModMod.newKieBaseModel("KBase_std").setDefault(true);
        kBaseMod.newKieSessionModel("KSession_std").setDefault(true);

        // Create file system with module model
        kFileSys = kServices.newKieFileSystem();
        kFileSys.writeKModuleXML(kModMod.toXML());
        
        // Write rules
        kFileSys.write("src/main/resources/person.drl", drl);

        KieBuilder kBuilder = kServices.newKieBuilder(kFileSys, classLoader).buildAll();
        
        boolean errors = kBuilder.getResults().hasMessages(Message.Level.ERROR);
        if (errors) {
            for (Message message : kBuilder.getResults().getMessages())
                System.out.println(message.getText());
        }
        
        // new KieSession
        kSession = kServices.newKieContainer(
                kServices.getRepository().getDefaultReleaseId(), classLoader).getKieBase().newKieSession();
    }
}

Compiling this rule (using KieBuilder.buildAll()) gives the error Rule Compilation error Only a type can be imported. facts.Person resolves to a package. facts.Person cannot be resolved to a type.

If I don't pass the ClassLoader to the KieBuilder, I get two additional errors: Unable to resolve ObjectType 'Person'. $p cannot be resolved.

So my ClassLoader is doing something, but not giving Drools full access to any classes it has loaded. How can I fix this? I have spent days on this problem, and I can't find anything that helps.


Solution

  • It looks like the contents of any loaded class files also need to be written to the KieFileSystem before compiling.

    So to give Drools full access to a class, the following is required:

    ClassLoader classLoader;
    // Use classLoader to load external classes
    ...
    
    // Copy class definitions to the KieFileSystem
    for (/*each loaded class*/) {
        String filename = packageName + '/' + className + ".class";
        kFileSys.write(filename, byteArrayOfClassFileContents);
    }
    
    // Pass classLoader to both newKieBuilder and newKieContainer
    KieServices kServices = KieServices.Factory.get();
    KieBuilder kBuilder = kServices.newKieBuilder(kFileSys, classLoader).buildAll();
    KieContainer kContainer = kServices.newKieContainer(
            kServices.getRepository().getDefaultReleaseId(), classLoader);
    

    Note that it is important to write class files inside their packages in the root of the KieFileSystem. For example, the definition for class foo.bar.Baz should be written to "foo/bar/Baz.class"