I call multiple Groovy scripts from Java, they both contain long-lived Groovy objects.
I would like my Groovy scripts to make some changes to a Java meta-class for a Java class (that have about 100 instances). However, the scripts should be able to make different changes, and changes in one of the scripts should not be reflected in the other scripts.
The problem: The meta-class for the Java class is shared across all the scripts.
This question is similar to How do I undo meta class changes after executing GroovyShell? but in this case I want two scripts to execute simultaneously, so it is not possible to reset after script execution.
SameTest.java
public interface SameTest {
void print();
void addMyMeta(String name);
void addJavaMeta(String name);
void callMyMeta(String name);
void callJavaMeta(String name);
}
SameSame.java
import groovy.lang.Binding;
import groovy.util.GroovyScriptEngine;
public class SameSame {
public SameTest launchNew() {
try {
GroovyScriptEngine scriptEngine = new GroovyScriptEngine(new String[]{""});
Binding binding = new Binding();
binding.setVariable("objJava", this);
SameTest script = (SameTest) scriptEngine.run("test.groovy", binding);
return script;
} catch (Exception | AssertionError e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
SameSame obj = new SameSame();
SameTest a = obj.launchNew();
SameTest b = obj.launchNew();
a.addMyMeta("a");
a.callMyMeta("a");
try {
b.callMyMeta("a");
throw new AssertionError("Should never happen");
} catch (Exception ex) {
System.out.println("Exception caught: " + ex);
}
a.addJavaMeta("q");
b.callJavaMeta("q");
a.print();
b.print();
}
}
test.groovy
ExpandoMetaClass.enableGlobally()
class Test implements SameTest {
SameSame objJava
void print() {
println 'My meta class is ' + Test.metaClass
println 'Java meta is ' + SameSame.metaClass
}
void addMyMeta(String name) {
println "Adding to Groovy: $this $name"
this.metaClass."$name" << {
"$name works!"
}
}
void addJavaMeta(String name) {
println "Adding to Java: $this $name"
objJava.metaClass."$name" << {
"$name works!"
}
}
void callMyMeta(String name) {
println "Calling Groovy: $this $name..."
"$name"()
println "Calling Groovy: $this $name...DONE!"
}
void callJavaMeta(String name) {
println "Calling Java: $this $name..."
objJava."$name"()
println "Calling Java: $this $name...DONE!"
}
}
new Test(objJava: objJava)
Output
Adding to Groovy: Test@7ee955a8 a
Calling Groovy: Test@7ee955a8 a...
Calling Groovy: Test@7ee955a8 a...DONE!
Calling Groovy: Test@4a22f9e2 a...
Exception caught: groovy.lang.MissingMethodException: No signature of method: Test.a() is applicable for argument types: () values: []
Possible solutions: any(), any(groovy.lang.Closure), is(java.lang.Object), wait(), wait(long), each(groovy.lang.Closure)
Adding to Java: Test@7ee955a8 q
Calling Java: Test@4a22f9e2 q...
Calling Java: Test@4a22f9e2 q...DONE!
My meta class is groovy.lang.ExpandoMetaClass@2145b572[class Test]
Java meta is groovy.lang.ExpandoMetaClass@39529185[class SameSame]
My meta class is groovy.lang.ExpandoMetaClass@72f926e6[class Test]
Java meta is groovy.lang.ExpandoMetaClass@39529185[class SameSame]
The two lines showing information about the Java meta should be different.
This should crash:
a.addJavaMeta("q");
b.callJavaMeta("q");
Is it possible somehow to use different MetaClassRegistry
's in the different GroovyScriptEngine
instances?
Or is there any other way to make the desired result as shown above happen?
The feature you are looking for is one I had planed for Groovy 3. But since I will no longer be able to work full time on Groovy and since nobody else dares a big change to the MOP this is no option at the moment.
So is it possible to use different MetaClassRegistry's in the different GroovyScriptEngine instances?
No, since you cannot use different MetaClassRegistry's. The implementation is somewhat abstracted, but the usage of MetaClassRegistryImpl is hardcoded and allows for only one global version.
Or is there any other way to make the desired result as shown above happen?
That depends on your requirements.
setMetaClassCreationHandle
in MetaClassRegistry
). Then you would have to of course capture a call like ExpandoMetaClass.enableGlobally()
. You could use ExpandoMetaClass
with a custom invoker (set someClass.metaClass.invokeMethod = ...
) or of course directly extend the class. You would then somehow need a way to recognize that you are coming from one script or the other (there is something called origin
or caller
in the bigger invokemethod signature, but the information is not always reliable. Same thing for get/setProperty). As for how to reliably and efficiently transport that information... well.. that's something I have no answer for. You have to experiment if what ExpandoMetaClass provides is good enough for you. Maybe you could use a ThreadLocal to store the information... though then you would have to write a transform, which will rewrite all method and property calls and most probably cause a performance disaster.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With