1. Introduction
In this tutorial, we’ll explore the latest techniques to integrate Groovy into a Java Application.
2. A Few Words About Groovy
The Groovy programming language is a powerful, optionally-typed and dynamic language. It’s supported by the Apache Software Foundation and the Groovy community, with contributions from more than 200 developers.
It can be used to build an entire application, to create a module or an additional library interacting with our Java code, or to run scripts evaluated and compiled on the fly.
For more information, please read Introduction to Groovy Language or go to the official documentation.
3. Maven Dependencies
At the time of writing, the latest stable release is 2.5.7, while Groovy 2.6 and 3.0 (both started in fall ’17) are still in alpha stage.
Similar to Spring Boot, we just need to include the groovy-all pom to add all the dependencies we may need, without worrying about their versions:
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>${groovy.version}</version> <type>pom</type> </dependency>
4. Joint Compilation
Before going into the details of how to configure Maven, we need to understand what we are dealing with.
Our code will contain both Java and Groovy files. Groovy won’t have any problem at all finding the Java classes, but what if we want Java to find Groovy classes and methods?
Here comes joint compilation to the rescue!
Joint compilation is a process designed to compile both Java and Groovy files in the same project, in a single Maven command.
With joint compilation, the Groovy compiler will:
- parse the source files
- depending on the implementation, create stubs that are compatible with the Java compiler
- invoke the Java compiler to compile the stubs along with Java sources – this way Java classes can find Groovy dependencies
- compile the Groovy sources – now our Groovy sources can find their Java dependencies
Depending on the plugin implementing it, we may be required to separate the files into specific folders or to tell the compiler where to find them.
Without joint compilation, the Java source files would be compiled as if they were Groovy sources. Sometimes this might work since most of the Java 1.7 syntax is compatible with Groovy, but the semantics would be different.
5. Maven Compiler Plugins
There are a few compiler plugins available that support joint compilation, each with its strengths and weaknesses.
The two most commonly used with Maven are Groovy-Eclipse Maven and GMaven+.
5.1. The Groovy-Eclipse Maven Plugin
The Groovy-Eclipse Maven plugin simplifies the joint compilation by avoiding stubs generation, still a mandatory step for other compilers like GMaven+, but it presents some configuration quirks.
To enable retrieval of the newest compiler artifacts, we have to add the Maven Bintray repository:
<pluginRepositories> <pluginRepository> <id>bintray</id> <name>Groovy Bintray</name> <url>https://dl.bintray.com/groovy/maven</url> <releases> <!-- avoid automatic updates --> <updatePolicy>never</updatePolicy> </releases> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories>
Then, in the plugin section, we tell the Maven compiler which Groovy compiler version it has to use.
In fact, the plugin we’ll use – the Maven compiler plugin – doesn’t actually compile, but instead delegates the job to the groovy-eclipse-batch artifact:
<plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <compilerId>groovy-eclipse-compiler</compilerId> <source>${java.version}</source> <target>${java.version}</target> </configuration> <dependencies> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-eclipse-compiler</artifactId> <version>3.3.0-01</version> </dependency> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-eclipse-batch</artifactId> <version>${groovy.version}-01</version> </dependency> </dependencies> </plugin>
The groovy-all dependency version should match the compiler version.
Finally, we need to configure our source autodiscovery: by default, the compiler would look into folders such as src/main/java and src/main/groovy, but if our java folder is empty, the compiler won’t look for our groovy sources.
The same mechanism is valid for our tests.
To force the file discovery, we could add any file in src/main/java and src/test/java, or simply add the groovy-eclipse-compiler plugin:
<plugin> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-eclipse-compiler</artifactId> <version>3.3.0-01</version> <extensions>true</extensions> </plugin>
The <extension> section is mandatory to let the plugin add the extra build phase and goals, containing the two Groovy source folders.
5.2. The GMavenPlus Plugin
The GMavenPlus plugin may have a name similar to the old GMaven plugin, but instead of creating a mere patch, the author made an effort to simplify and decouple the compiler from a specific Groovy version.
To do so, the plugin separates itself from the standard guidelines for compiler plugins.
The GMavenPlus compiler adds support for features that were still not present in other compilers at the time, such as invokedynamic, the interactive shell console, and Android.
On the other side, it presents some complications:
- it modifies Maven’s source directories to contain both the Java and the Groovy sources, but not the Java stubs
- it requires us to manage stubs if we don’t delete them with the proper goals
To configure our project, we need to add the gmavenplus-plugin:
<plugin> <groupId>org.codehaus.gmavenplus</groupId> <artifactId>gmavenplus-plugin</artifactId> <version>1.7.0</version> <executions> <execution> <goals> <goal>execute</goal> <goal>addSources</goal> <goal>addTestSources</goal> <goal>generateStubs</goal> <goal>compile</goal> <goal>generateTestStubs</goal> <goal>compileTests</goal> <goal>removeStubs</goal> <goal>removeTestStubs</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <!-- any version of Groovy \>= 1.5.0 should work here --> <version>2.5.6</version> <scope>runtime</scope> <type>pom</type> </dependency> </dependencies> </plugin>
To allow testing of this plugin, we created a second pom file called gmavenplus-pom.xml in the sample.
5.3. Compiling With the Eclipse-Maven Plugin
Now that everything is configured, we can finally build our classes.
In the example we provided, we created a simple Java application in the source folder src/main/java and some Groovy scripts in src/main/groovy, where we can create Groovy classes and scripts.
Let’s build everything with the Eclipse-Maven plugin:
$ mvn clean compile ... [INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ core-groovy-2 --- [INFO] Changes detected - recompiling the module! [INFO] Using Groovy-Eclipse compiler to compile both Java and Groovy files ...
Here we see that Groovy is compiling everything.
5.4. Compiling With GMavenPlus
GMavenPlus shows some differences:
$ mvn -f gmavenplus-pom.xml clean compile ... [INFO] --- gmavenplus-plugin:1.7.0:generateStubs (default) @ core-groovy-2 --- [INFO] Using Groovy 2.5.7 to perform generateStubs. [INFO] Generated 2 stubs. [INFO] ... [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ core-groovy-2 --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 3 source files to XXX\Baeldung\TutorialsRepo\core-groovy-2\target\classes [INFO] ... [INFO] --- gmavenplus-plugin:1.7.0:compile (default) @ core-groovy-2 --- [INFO] Using Groovy 2.5.7 to perform compile. [INFO] Compiled 2 files. [INFO] ... [INFO] --- gmavenplus-plugin:1.7.0:removeStubs (default) @ core-groovy-2 --- [INFO] ...
We notice right away that GMavenPlus goes through the additional steps of:
- Generating stubs, one for each groovy file
- Compiling the Java files – stubs and Java code alike
- Compiling the Groovy files
By generating stubs, GMavenPlus inherits a weakness that caused many headaches to developers in the past years, when working with joint compilation.
In the ideal scenario, everything would work just fine, but introducing more steps we have also more points of failure: for example, the build may fail before being able to clean up the stubs.
If this happens, old stubs left around may confuse our IDE, which would then show compilation errors where we know everything should be correct.
Only a clean build would then avoid a painful and long witch hunt.
5.5. Packaging Dependencies in the Jar File
To run the program as a jar from the command line, we added the maven-assembly-plugin, which will include all the Groovy dependencies in a “fat jar” named with the postfix defined in the property descriptorRef:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.1.0</version> <configuration> <!-- get all project dependencies --> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <!-- MainClass in mainfest make a executable jar --> <archive> <manifest> <mainClass>com.baeldung.MyJointCompilationApp</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <!-- bind to the packaging phase --> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>
Once the compilation is complete we can run our code with this command:
$ java -jar target/core-groovy-2-1.0-SNAPSHOT-jar-with-dependencies.jar com.baeldung.MyJointCompilationApp
6. Loading Groovy Code on the Fly
The Maven compilation let us include Groovy files in our project and reference their classes and methods from Java.
Although, this is not enough if we want to change the logic at runtime: the compilation runs outside the runtime stage, so we still have to restart our application in order to see our changes.
To take advantage of the dynamic power (and risks) of Groovy, we need to explore the techniques available to load our files when our application is already running.
6.1. GroovyClassLoader
To achieve this, we need the GroovyClassLoader, which can parse source code in text or file format and generate the resulting class objects.
When the source is a file, the compilation result is also cached, to avoid overhead when we ask the loader multiple instances of the same class.
Script coming directly from a String object, instead, won’t be cached, hence calling the same script multiple times could still cause memory leaks.
GroovyClassLoader is the foundation other integration systems are built on.
The implementation is relatively simple:
private final GroovyClassLoader loader; private Double addWithGroovyClassLoader(int x, int y) throws IllegalAccessException, InstantiationException, IOException { Class calcClass = loader.parseClass( new File("src/main/groovy/com/baeldung/", "CalcMath.groovy")); GroovyObject calc = (GroovyObject) calcClass.newInstance(); return (Double) calc.invokeMethod("calcSum", new Object[] { x, y }); } public MyJointCompilationApp() { loader = new GroovyClassLoader(this.getClass().getClassLoader()); // ... }
6.2. GroovyShell
The Shell Script Loader parse() method accepts sources in text or file format and generates an instance of the Script class.
This instance inherits the run() method from Script, which executes the entire file top to bottom and returns the result given by the last line executed.
If we want to, we can also extend Script in our code, and override the default implementation to call directly our internal logic.
The implementation to call Script.run() looks like this:
private Double addWithGroovyShellRun(int x, int y) throws IOException { Script script = shell.parse(new File("src/main/groovy/com/baeldung/", "CalcScript.groovy")); return (Double) script.run(); } public MyJointCompilationApp() { // ... shell = new GroovyShell(loader, new Binding()); // ... }
Please note that the run() doesn’t accept parameters, so we would need to add to our file some global variables initialize them through the Binding object.
As this object is passed in the GroovyShell initialization, the variables are shared with all the Script instances.
If we prefer a more granular control, we can use invokeMethod(), which can access our own methods through reflection and pass arguments directly.
Let’s look at this implementation:
private final GroovyShell shell; private Double addWithGroovyShell(int x, int y) throws IOException { Script script = shell.parse(new File("src/main/groovy/com/baeldung/", "CalcScript.groovy")); return (Double) script.invokeMethod("calcSum", new Object[] { x, y }); } public MyJointCompilationApp() { // ... shell = new GroovyShell(loader, new Binding()); // ... }
Under the covers, GroovyShell relies on the GroovyClassLoader for compiling and caching the resulting classes, so the same rules explained earlier apply in the same way.
6.3. GroovyScriptEngine
The GroovyScriptEngine class is particularly for those applications which rely on the reloading of a script and its dependencies.
Although we have these additional features, the implementation has only a few small differences:
private final GroovyScriptEngine engine; private void addWithGroovyScriptEngine(int x, int y) throws IllegalAccessException, InstantiationException, ResourceException, ScriptException { Class<GroovyObject> calcClass = engine.loadScriptByName("CalcMath.groovy"); GroovyObject calc = calcClass.newInstance(); Object result = calc.invokeMethod("calcSum", new Object[] { x, y }); LOG.info("Result of CalcMath.calcSum() method is {}", result); } public MyJointCompilationApp() { ... URL url = null; try { url = new File("src/main/groovy/com/baeldung/").toURI().toURL(); } catch (MalformedURLException e) { LOG.error("Exception while creating url", e); } engine = new GroovyScriptEngine(new URL[] {url}, this.getClass().getClassLoader()); engineFromFactory = new GroovyScriptEngineFactory().getScriptEngine(); }
This time we have to configure source roots, and we refer to the script with just its name, which is a bit cleaner.
Looking inside the loadScriptByName method, we can see right away the check isSourceNewer where the engine checks if the source currently in cache is still valid.
Every time our file changes, GroovyScriptEngine will automatically reload that particular file and all the classes depending on it.
Although this is a handy and powerful feature, it could cause a very dangerous side effect: reloading many times a huge number of files will result in CPU overhead without warning.
If that happens, we may need to implement our own caching mechanism to deal with this issue.
6.4. GroovyScriptEngineFactory (JSR-223)
JSR-223 provides a standard API for calling scripting frameworks since Java 6.
The implementation looks similar, although we go back to loading via full file paths:
private final ScriptEngine engineFromFactory; private void addWithEngineFactory(int x, int y) throws IllegalAccessException, InstantiationException, javax.script.ScriptException, FileNotFoundException { Class calcClas = (Class) engineFromFactory.eval( new FileReader(new File("src/main/groovy/com/baeldung/", "CalcMath.groovy"))); GroovyObject calc = (GroovyObject) calcClas.newInstance(); Object result = calc.invokeMethod("calcSum", new Object[] { x, y }); LOG.info("Result of CalcMath.calcSum() method is {}", result); } public MyJointCompilationApp() { // ... engineFromFactory = new GroovyScriptEngineFactory().getScriptEngine(); }
It’s great if we are integrating our app with several scripting languages, but its feature set is more restricted. For example, it doesn’t support class reloading. As such, if we are only integrating with Groovy, then it may be better to stick with earlier approaches.
7. Pitfalls of Dynamic Compilation
Using any of the methods above, we could create an application that reads scripts or classes from a specific folder outside our jar file.
This would give us the flexibility to add new features while the system is running (unless we require new code in the Java part), thus achieving some sort of Continuous Delivery development.
But beware this double-edged sword: we now need to protect ourselves very carefully from failures that could happen both at compile time and runtime, de facto ensuring that our code fails safely.
8. Pitfalls of Running Groovy in a Java Project
8.1. Performance
We all know that when a system needs to be very performant, there are some golden rules to follow.
Two that may weigh more on our project are:
- avoid reflection
- minimize the number of bytecode instructions
Reflection, in particular, is a costly operation due to the process of checking the class, the fields, the methods, the method parameters, and so on.
If we analyze the method calls from Java to Groovy, for example, when running the example addWithCompiledClasses, the stack of operation between .calcSum and the first line of the actual Groovy method looks like:
calcSum:4, CalcScript (com.baeldung) addWithCompiledClasses:43, MyJointCompilationApp (com.baeldung) addWithStaticCompiledClasses:95, MyJointCompilationApp (com.baeldung) main:117, App (com.baeldung)
Which is consistent with Java. The same happens when we cast the object returned by the loader and call its method.
However, this is what the invokeMethod call does:
calcSum:4, CalcScript (com.baeldung) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invoke:101, CachedMethod (org.codehaus.groovy.reflection) doMethodInvoke:323, MetaMethod (groovy.lang) invokeMethod:1217, MetaClassImpl (groovy.lang) invokeMethod:1041, MetaClassImpl (groovy.lang) invokeMethod:821, MetaClassImpl (groovy.lang) invokeMethod:44, GroovyObjectSupport (groovy.lang) invokeMethod:77, Script (groovy.lang) addWithGroovyShell:52, MyJointCompilationApp (com.baeldung) addWithDynamicCompiledClasses:99, MyJointCompilationApp (com.baeldung) main:118, MyJointCompilationApp (com.baeldung)
In this case, we can appreciate what’s really behind Groovy’s power: the MetaClass.
A MetaClass defines the behavior of any given Groovy or Java class, so Groovy looks into it whenever there’s a dynamic operation to execute in order to find the target method or field. Once found, the standard reflection flow executes it.
Two golden rules broken with one invoke method!
If we need to work with hundreds of dynamic Groovy files, how we call our methods will then make a huge performance difference in our system.
8.2. Method or Property Not Found
As mentioned earlier, if we want to deploy new versions of Groovy files in a CD life cycle, we need to treat them like they were an API separate from our core system.
This means putting in place multiple fail-safe checks and code design restrictions so our newly joined developer doesn’t blow up the production system with a wrong push.
Examples of each are: having a CI pipeline and using method deprecation instead of deletion.
What happens if we don’t? We get dreadful exceptions due to missing methods and wrong argument counts and types.
And if we think that compilation would save us, let’s look at the method calcSum2() of our Groovy scripts:
// this method will fail in runtime def calcSum2(x, y) { // DANGER! The variable "log" may be undefined log.info "Executing $x + $y" // DANGER! This method doesn't exist! calcSum3() // DANGER! The logged variable "z" is undefined! log.info("Logging an undefined variable: $z") }
By looking through the entire file, we immediately see two problems: the method calcSum3() and the variable z are not defined anywhere.
Even so, the script is compiled successfully, without even a single warning, both statically in Maven and dynamically in the GroovyClassLoader.
It’ll fail only when we try to invoke it.
Maven’s static compilation will show an error only if our Java code refers directly to calcSum3(), after casting the GroovyObject like we do in the addWithCompiledClasses() method, but it’s still ineffective if we use reflection instead.
9. Conclusion
In this article, we explored how we can integrate Groovy in our Java application, looking at different integration methods and some of the problems we may encounter with mixed languages.
As usual, the source code used in the examples can be found on GitHub.