Skip to content

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.

Answer

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"