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

flowchart LR subgraph EV["Eval"] direction TB EV1["input: one expression string"] EV2["caching: none"] EV3["reloading: none"] EV4["sandboxing: none"] end subgraph GS["GroovyShell"] direction TB GS1["input: String, Reader, File, InputStream, URI"] GS2["caching: parse returns a reusable Script"] GS3["reloading: none"] GS4["sandboxing: CompilerConfiguration with customizers"] end subgraph GC["GroovyClassLoader"] direction TB GC1["input: class source as String, File or URL"] GC2["caching: by file only, a String compiles anew"] GC3["reloading: none by itself"] GC4["sandboxing: CompilerConfiguration with customizers"] end subgraph SE["GroovyScriptEngine"] direction TB SE1["input: script names under root URLs"] SE2["caching: yes, per script"] SE3["reloading: yes, including dependencies"] SE4["sandboxing: CompilerConfiguration with customizers"] end subgraph JX["JSR 223"] direction TB JX1["input: String or Reader"] JX2["caching: Compilable gives a CompiledScript"] JX3["reloading: none"] JX4["sandboxing: none in the standard API"] end EV --> GS --> GC --> SE GS --> JX

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 a SoftReference, 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, the groovy.use.classvalue options 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 in org.codehaus.groovy.control in Groovy 6.0, while the core embedding classes are in groovy.lang and groovy.util. Code copied from older articles usually still compiles; the point to re-check is the dependency coordinates, which are org.apache.groovy for current releases.

  • Version and JDK baselines: Groovy 6.0 needs JDK 17 or later.