vendredi 9 juin 2023

Trouble with groovy/java, classpaths, and reflection/dynamic method access

TL;DR: I am having trouble with classpaths and a java/groovy execution environment. Java is openjdk version "17.0.7" 2023-04-18. Groovy is groovy-4.0.12.jar.

The Background

I have Java program that loads groovy scripts and runs them. The script can access a number of resource in the parent via an ExecutionContext class that is setup upon initialization. The design uses a base class to standardize the interface, leaving task-specific codes to the subclass. A quick example:

BaseClass.1.groovy:

import org.bson.*;
class BaseClass {
    ExecutionContext exe; 

    public void setContext(ExecutionContext executingEnvironment) {
        this.exe = executingEnvironment
    }
              
    Document fetchSection(String name) {
    return exe.fetchSection(name);
    }
}

MyClass.groovy:
import BaseClass;

import org.bson.*;

class MyClass extends BaseClass {
    void do_something(Document inputs) {
    Document s1 = fetchSection("S1"); // use BaseClass method
...
     }    
}

ExecutionContext is defined in a simple class and placed into a jar ExecutionContext.jar:

import org.bson.*;

public interface ExecutionContext {
    Document fetchSection(String sectionName);
}

The parent program contains a concrete implementation of ExecutionContext:

    class ExecutionContextImpl implements ExecutionContext {
    public Document fetchSection(String sectionName) {
        // Actual work here...
    }
    }

The parent first loads the BaseClass, then MyClass, then creates an instance of MyClass, then creates an instance of ExecutionContext and using reflection sets it into MyClass:

GroovyClassLoader cls = new GroovyClassLoader();

String fwscript = get BaseClass.1.groovy from somewhere;
String myscript = get MyClass.groovy from somewhere;

Class fwclazz = cls.parseClass(fwscript);
Class clazz = cls.parseClass(myscript);
Object oo = clazz.newInstance(); 

ExecutionContext qqq = new ExecutionContextImpl();

Class[] argTypes = new Class[1];
argTypes[0] = ExecutionContext.class;
java.lang.reflect.Method m = fwclazz.getDeclaredMethod("setContext", argTypes);
Object[] margs = new Object[1];
margs[0] = qqq;
Object oo2 = m.invoke(oo, margs);

This works, and later use of dynamic reflection to invoke methods such as do_something also works:

Class[] argTypes = new Class[1];
argTypes[0] = Document.class;
java.lang.reflect.Method m = clazz.getDeclaredMethod( "do_something", argTypes);
Object[] margs = new Object[1];
margs[0] = new Document("adj", 6.3);
Object oo2 = m.invoke(oo, margs);

The Problem

I wish to isolate the groovy classpath environment from the parent. This can be done as follows; note the null in arg #2 to prevent the new classloader from inheriting the environment from the parent program:

URL[] groovyClasspath = new URL[] {
    new File("/path/to/ExecutionContext.jar").toURI().toURL(),
    new File("/path/to/groovy-4.0.12.jar").toURI().toURL(),
    new File("/path/to/guava-17.0.jar").toURI().toURL(),
    new File("/path/to/bson-4.9.0.jar").toURI().toURL()
};
URLClassLoader cpl = new URLClassLoader(groovyClasspath, null);

This almost works. The scripts parse and in particular, resources referenced in the scripts like imports and bson and Table objects are resolved. Commenting out the URL in the classpath produces an expected unable to resolve class runtime exception so it is definitely finding the intended codes. The problem is that with an explicit classpath, the groovy environment cannot seem to find methods in the loaded groovy scripts. This call:

java.lang.reflect.Method m = fwclazz.getDeclaredMethod("setContext", argTypes);

throws an exception java.lang.NoSuchMethodException: BaseClass.setContext(ExecutionContext). Any call to getDeclaredMethod for either the base class fwclazz or the subclass MyClass has this problem. And what is weird is that if I list the methods with something like this:

for (Method method : fwclazz.getDeclaredMethods()) {
   System.out.println("  " + method.toString());
}

Then I clearly see public void BaseClass.setContext(ExecutionContext):

  public groovy.lang.MetaClass BaseClass.getMetaClass()
  public void BaseClass.setMetaClass(groovy.lang.MetaClass)
  protected groovy.lang.MetaClass BaseClass.$getStaticMetaClass()
  public org.bson.Document BaseClass.fetchSection(java.lang.String)
  public void BaseClass.setContext(ExecutionContext)
  public static java.lang.invoke.MethodHandles$Lookup BaseClass.$getLookup()
  public ExecutionContext BaseClass.getExe()
  public void BaseClass.setExe(ExecutionContext)

I am sure there is a very simple addition to the GroovyClassLoader URL list to make this work. Any clues?





Aucun commentaire:

Enregistrer un commentaire