Integrating Groovy into Java Applications
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked
on 2026-10-14). The host code is Java, compiled with javac against groovy-6.0.0.jar (and groovy-jsr223-6.0.0.jar for JSR 223);
the Groovy runtime is an ordinary library, so a Java program can compile and run Groovy source at runtime without any Groovy-specific
build step. Groovy ships several integration mechanisms, from a one-line expression evaluator to a script engine that reloads files
as they change. They differ in what they accept as input, whether they cache compiled code, whether they notice changed sources, and
how much you can restrict what the script is allowed to do. The scenario is the usual greenhouse: the Java application owns the
sensor readings, and a Groovy rule decides what to do about them.
The same mechanisms are used by tools that embed Groovy as a scripting layer. The JSR 223 elements of Apache JMeter are an example and are described on Scripting with JSR223 & Groovy. Restricting what an embedded script can do is covered in depth on Compilation Customizers & Secure Scripting; the DSL techniques that make an embedded script pleasant to write are on Domain Specific Languages.
Choosing a mechanism
The arrows read as "when this is not enough, move here". Two further classes sit outside the chain. JavaShell compiles Java source
in memory, and CompilationUnit is the compiler itself, for hosts that need to control the compilation phases. The sections below
take the mechanisms in that order.
Eval
groovy.util.Eval is the shortest path: static methods that compile and run a string. me takes no variables, and x, xy and
xyz bind one to three values to the names x, y and z. The overload me(symbol, value, expression) binds one value to a name
of your choice.
import groovy.util.Eval;
public class EvalDemo {
public static void main(String[] args) {
Object a = Eval.me("33 * 3");
Object b = Eval.x(30, "x * 1.5");
Object c = Eval.me("t", 31, "t > 28 ? 'vent' : 'hold'");
Object d = Eval.xy(21, 55, "x + y / 100");
System.out.println(a + " " + a.getClass().getSimpleName());
System.out.println(b);
System.out.println(c);
System.out.println(d);
}
}
99 Integer
45.0
vent
21.55
Eval keeps nothing: each call compiles a new script, and there is no way to pass a compiler configuration. It is a fit for a
one-off expression such as a computed field in a configuration file, and the wrong tool for anything that runs repeatedly or that
comes from an untrusted source.
GroovyShell
groovy.lang.GroovyShell is the usual entry point. It evaluates a script from a String, Reader, File, InputStream or URI
(evaluate), or compiles it without running it (parse), which returns a groovy.lang.Script you can keep and run later.
Sharing data with a Binding
A groovy.lang.Binding is a map of variables that the script sees as undeclared names. The application puts inputs in, and reads
results out of the same object. A variable that the script declares (def x, String x) is local to the script and never
reaches the binding, so a result variable must be assigned without a type.
Binding shared = new Binding();
GroovyShell shell = new GroovyShell(shared);
shared.setProperty("temperature", 31);
Object r = shell.evaluate("action = temperature > 28 ? 'vent' : 'hold'; temperature - 28");
System.out.println("result=" + r + " action=" + shared.getProperty("action"));
shell.evaluate("String local = 'x'");
System.out.println("has local: " + shared.hasVariable("local"));
result=3 action=vent
has local: false
Reusing a parsed script
parse pays the compilation cost once. The returned Script can be run many times, each time with a different binding assigned
with setBinding.
Script script = shell.parse("humidity = humidity + 5");
for (int h : new int[] {40, 70}) {
Binding b = new Binding();
b.setVariable("humidity", h);
script.setBinding(b);
script.run();
System.out.println("humidity -> " + b.getVariable("humidity"));
}
humidity -> 45
humidity -> 75
A Script instance is not thread safe, and neither is a Binding passed to the GroovyShell constructor, because it is shared by
every script the shell runs. For concurrent use, give each thread its own Script instance, obtained from its own parse call
(compile the source once as a class with GroovyClassLoader and instantiate it per thread when parsing on every request is too
slow), and its own binding.
Script s1 = shell.parse("n = n * 2");
Script s2 = shell.parse("n = n * 2");
Binding b1 = new Binding(); b1.setVariable("n", 3);
Binding b2 = new Binding(); b2.setVariable("n", 4);
s1.setBinding(b1); s2.setBinding(b2);
Thread t1 = new Thread(s1::run), t2 = new Thread(s2::run);
t1.start(); t2.start(); t1.join(); t2.join();
System.out.println(b1.getVariable("n") + " " + b2.getVariable("n") + " distinct=" + (s1 != s2));
6 8 distinct=true
A custom script class
By default a parsed script extends groovy.lang.Script. A CompilerConfiguration can name another base class, which gives every
script extra properties and methods. This is the simplest way to hand the script a small vocabulary of host functions. The base class
must be abstract and extend Script.
public static abstract class GreenhouseScript extends Script {
public String zone = "unset";
public String describe() { return "zone " + zone; }
}
CompilerConfiguration cc = new CompilerConfiguration();
cc.setScriptBaseClass(GreenhouseScript.class.getName());
GroovyShell custom = new GroovyShell(ShellDemo.class.getClassLoader(), new Binding(), cc);
Script s = custom.parse("describe()");
System.out.println(s instanceof GreenhouseScript);
((GreenhouseScript) s).zone = "north";
System.out.println(s.run());
true
zone north
The class loader passed as the first argument is the parent of the loader the shell creates, so it must be able to see the base
class. The same CompilerConfiguration is also where compilation customizers are registered, which is the sandboxing hook shown
in Sandboxing: what a script may do.
GroovyClassLoader
GroovyShell is built for scripts. When the source defines classes, for example an implementation of an interface that the Java
application declares, use groovy.lang.GroovyClassLoader (the shell uses one internally). parseClass compiles source and returns
a Class, which the host instantiates and calls through the Java interface, so the rest of the application never touches Groovy
types.
public interface Rule { String decide(double temperature); }
String src = "class VentRule implements LoaderDemo.Rule {\n"
+ " String decide(double t) { t > 28 ? 'vent' : 'hold' }\n}";
try (GroovyClassLoader gcl = new GroovyClassLoader(LoaderDemo.class.getClassLoader())) {
Class<?> c1 = gcl.parseClass(src);
Class<?> c2 = gcl.parseClass(src);
System.out.println(c1.getName() + " same=" + (c1 == c2));
Rule rule = (Rule) c1.getDeclaredConstructor().newInstance();
System.out.println(rule.decide(31.5) + " " + rule.decide(20));
File f = Files.createTempFile("VentRule", ".groovy").toFile();
Files.writeString(f.toPath(), "class FileRule { }");
Class<?> f1 = gcl.parseClass(f);
Class<?> f2 = gcl.parseClass(new File(f.getAbsolutePath()));
System.out.println("file source same=" + (f1 == f2));
System.out.println("loaded classes=" + gcl.getLoadedClasses().length);
}
VentRule same=false
vent hold
file source same=true
loaded classes=2
The second line shows the caching rule that matters. A class loader remembers the classes it defined and never lets go of them, and
it does not remember the text of a String source, so compiling the same string twice defines two different classes with the
same name. A host that calls parseClass(String) per request therefore grows without bound. With a File source the loader does
cache by file, so two File objects for the same path give the same class. The practical rules are to compile each rule once and
keep the resulting Class or instance, to key your own cache on the source text or its hash, and to close the loader (it is
Closeable) when the whole generation of classes is replaced.
GroovyScriptEngine
groovy.util.GroovyScriptEngine adds, on top of a class loader, the two things a long-running host usually wants: it finds scripts
by name under one or more root URLs, and it recompiles a script when the file changes, including when only a script that it
depends on changed. Scripts are run with run(name, binding).
The example below writes a rule script and a dependency into a temporary directory, runs the rule, then edits the dependency and the rule in turn. The engine checks file timestamps, so the example waits past the file system’s timestamp resolution between edits.
Path dir = Files.createTempDirectory("rules");
Path rule = dir.resolve("Rule.groovy");
Path dep = dir.resolve("Threshold.groovy");
Files.writeString(dep, "class Threshold { static double limit = 28 }");
Files.writeString(rule, "binding.temperature > Threshold.limit ? 'vent' : 'hold'");
GroovyScriptEngine engine = new GroovyScriptEngine(new java.net.URL[] { dir.toUri().toURL() });
Binding b = new Binding();
b.setVariable("temperature", 30);
System.out.println("v1: " + engine.run("Rule.groovy", b));
Thread.sleep(1100);
Files.writeString(dep, "class Threshold { static double limit = 35 }");
System.out.println("dependency changed: " + engine.run("Rule.groovy", b));
Thread.sleep(1100);
Files.writeString(rule, "'always hold'");
System.out.println("script changed: " + engine.run("Rule.groovy", b));
v1: vent
dependency changed: hold
script changed: always hold
Rule.groovy never changed in the second step, yet its result did, because the engine tracks the files a script imports or
refers to and recompiles it when any of them is newer. This is the mechanism for rule files that operators edit on a running
system. A compiler configuration can be supplied with setConfig, so the sandboxing options of GroovyShell apply here too. What the
engine does not do is watch the directory: it checks timestamps when a script is run, so a rule that is never run is never
reloaded, and the host chooses how often to run (and therefore check).
JavaShell
org.apache.groovy.util.JavaShell goes the other way: it compiles Java source in memory through the platform
javax.tools.JavaCompiler and returns loaded classes. It is useful in a Groovy or polyglot host that generates Java code or
loads Java plug-ins from text. It needs a JDK at runtime, because a plain JRE has no system compiler. A compilation failure throws
JavaShellCompilationException carrying the javac diagnostics. compileAll returns every class of a source unit keyed by binary
name, and compileAllTo also writes the class files to a directory (it is marked incubating, so its signature may change in a
later 6.x release).
JavaShell js = new JavaShell();
String src = "package demo;\n"
+ "public class Dose { public static String plan(int minutes) { return minutes + \" min\"; } }\n";
Class<?> dose = js.compile("demo.Dose", src);
System.out.println(dose.getDeclaredMethod("plan", int.class).invoke(null, 12));
try {
js.compile("demo.Broken", "package demo; public class Broken { int x = \"s\"; }");
} catch (Exception e) {
System.out.println(e.getClass().getSimpleName());
}
12 min
JavaShellCompilationException
CompilationUnit
org.codehaus.groovy.control.CompilationUnit is the compiler’s own driver: it owns the compilation phases (parsing, conversion,
semantic analysis, canonicalization, class generation and so on) and lets a host add operations to a phase or stop after one. The
joint compiler uses it to produce Java stubs before javac runs. The documentation advises against subclassing or driving it
directly unless no standard option fits, since compilation customizers and CompilerConfiguration cover most needs without tying
the host to compiler internals. Joint compilation is described on
Compiling with groovyc & Joint Compilation.
JSR 223
JSR 223 (javax.script) is the standard Java API for scripting languages. groovy-jsr223 registers a Groovy engine that
ScriptEngineManager finds by the name groovy, so a host written against javax.script can switch languages by name. It is
the only mechanism here that does not depend on Groovy classes at compile time, at the price of a narrower API: no access to the
Script instance, the compiler configuration or the class loader.
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");
System.out.println(engine.getFactory().getEngineName() + " " + engine.getFactory().getEngineVersion());
System.out.println(engine.eval("(1..10).sum()"));
engine.put("first", "HELLO");
engine.put("second", "world");
System.out.println(engine.eval("first.toLowerCase() + ' ' + second.toUpperCase()"));
engine.eval("def decide(t) { t > 28 ? 'vent' : 'hold' }");
Invocable inv = (Invocable) engine;
System.out.println(inv.invokeFunction("decide", 30));
Rule rule = inv.getInterface(Rule.class);
System.out.println(rule.decide(12.0));
CompiledScript cs = ((Compilable) engine).compile("t > 28 ? 'vent' : 'hold'");
for (int t : new int[] {20, 33}) {
Bindings b = engine.createBindings();
b.put("t", t);
System.out.println(t + " -> " + cs.eval(b));
}
Groovy Scripting Engine 2.0
55
hello WORLD
vent
hold
20 -> hold
33 -> vent
engine.put shares variables, Invocable calls a function defined by an earlier eval (and getInterface wraps the engine’s
functions in a Java interface, as the Rule call shows), and Compilable returns a CompiledScript that is compiled once and
evaluated with a fresh Bindings per call, which is the right shape for a hot path. The version string 2.0 is the version of the
JSR 223 engine adapter, not of Groovy. By default the engine keeps hard references to the functions it has defined; the engine-level
attribute #jsr223.groovy.engine.keep.globals set to weak, soft or phantom (case ignored) switches to that reference kind, and any other
value keeps hard references. Groovy’s documentation recommends its own mechanisms over JSR 223 when you will only ever use Groovy.
Sandboxing: what a script may do
Every mechanism above runs the script with the full privileges of the host JVM. The exceptions are the ones that accept a
CompilerConfiguration (GroovyShell, GroovyClassLoader, GroovyScriptEngine), because a configuration can carry compilation
customizers that reject code at compile time. This is a syntactic restriction, not an operating-system sandbox, and the Java
Security Manager that older material suggests is no longer available as a fallback on current JDKs. The example restricts a shell so that
method calls are only allowed on an explicit list of receiver types.
SecureASTCustomizer sec = new SecureASTCustomizer();
sec.setAllowedReceiversClasses(List.of(Integer.class, Object.class));
sec.setIndirectImportCheckEnabled(true);
CompilerConfiguration cc = new CompilerConfiguration();
cc.addCompilationCustomizers(sec);
GroovyShell shell = new GroovyShell(cc);
System.out.println(shell.evaluate("1 + 2"));
try {
shell.evaluate("System.exit(1)");
} catch (MultipleCompilationErrorsException e) {
System.out.println("rejected: " + e.getMessage().lines().skip(1).findFirst().get().trim());
}
3
rejected: General error during semantic analysis: Method calls not allowed on [java.lang.System]
A whitelist like this is a guard against mistakes and casual misuse by trusted authors. Treat anything that runs scripts supplied
by untrusted users as needing process-level isolation (a separate process or container with limited rights) as well. The
customizers, ImportCustomizer, SecureASTCustomizer and the type-checking based approach are covered on
Compilation Customizers & Secure Scripting.
Releasing class loaders in managed environments
Hosts that load and discard Groovy repeatedly (a web container redeploying applications, a plug-in system, a long-running service
that replaces rule generations) must make sure the old Groovy class loader can be garbage collected, or metaspace grows with
every redeployment. Groovy keeps runtime metadata (ClassInfo) for every class it dispatches on, including JDK classes such as
String. By default that metadata sits in a java.lang.ClassValue, which lives as long as its key class, so an entry on a
long-lived class such as String holds a strong chain to the Groovy class loader of the application that created it. The
documentation lists three measures, any one of which is sufficient.
-
Start the JVM with
-Dgroovy.use.classvalue=soft. Groovy keeps the fast per-class lookup but holds each entry through aSoftReference, so the long-lived JDK classes no longer keep the loader alive. Entries that cannot be rebuilt, such as installed metaclasses, stay strongly referenced while in use. -
Start the JVM with
-Dgroovy.use.classvalue=false. Entries move to a weak-key map. The trade-off is a hash lookup instead of the per-class fast path: compilation speed is unaffected, but dispatch-heavy dynamic code can be a few percent slower overall, and some idioms on JDK-class receivers are reported up to about 1.4 times slower, so measure with your own workload. -
Clean up explicitly when the application is discarded, for example from
ServletContextListener#contextDestroyed.
for (ClassInfo ci : ClassInfo.getAllClassInfo()) {
Class<?> c = ci.getTheClass();
if (c != null) ClassInfo.remove(c);
}
Groovy itself starts no background threads that would pin the loader. Threads started by script code (including executors a script
creates) capture their creating context and must be shut down by the application as usual. The loop above compiled and ran against
groovy-6.0.0.jar with -Dgroovy.use.classvalue=soft; whether a particular container then releases its loader is a property of that
container and was not measured here.
Where older material is outdated
-
JavaShell, thegroovy.use.classvalueoptions and the class loader release guidance are recent additions that books written for Groovy 2.x do not cover. The Groovy 6.0 documentation describes them in the integration chapter. -
Older sandboxing advice relies on the Java Security Manager. It was deprecated for removal in JDK 17 and permanently disabled in JDK 24, so on Groovy 6.0 the in-process tool is the set of compilation customizers shown above, together with process-level isolation.
-
The compiler classes (
CompilerConfiguration,CompilationUnit, the customizers) still live inorg.codehaus.groovy.controlin Groovy 6.0, while the core embedding classes are ingroovy.langandgroovy.util. Code copied from older articles usually still compiles; the point to re-check is the dependency coordinates, which areorg.apache.groovyfor current releases. -
Version and JDK baselines: Groovy 6.0 needs JDK 17 or later.