Groovy in Java-Anwendungen integrieren

1. Einleitung

In diesem Tutorial werden die neuesten Techniken zur Integration von Groovy in eine Java-Anwendung erläutert.

2. Ein paar Worte über Groovy

Die Programmiersprache Groovy ist eine leistungsstarke, optional typisierte und dynamische Sprache . Es wird von der Apache Software Foundation und der Groovy-Community mit Beiträgen von mehr als 200 Entwicklern unterstützt.

Es kann verwendet werden, um eine gesamte Anwendung zu erstellen, ein Modul oder eine zusätzliche Bibliothek zu erstellen, die mit unserem Java-Code interagiert, oder um Skripts auszuführen, die im laufenden Betrieb ausgewertet und kompiliert werden.

Weitere Informationen finden Sie unter Einführung in die Groovy-Sprache oder in der offiziellen Dokumentation.

3. Maven-Abhängigkeiten

Zum Zeitpunkt des Schreibens ist die neueste stabile Version 2.5.7, während sich Groovy 2.6 und 3.0 (beide im Herbst '17 gestartet) noch im Alpha-Stadium befinden.

Ähnlich wie bei Spring Boot müssen wir nur den Groovy-All- Pom einfügen, um alle Abhängigkeiten hinzuzufügen, die wir möglicherweise benötigen, ohne uns um ihre Versionen kümmern zu müssen:

 org.codehaus.groovy groovy-all ${groovy.version} pom 

4. Gemeinsame Zusammenstellung

Bevor wir uns mit der Konfiguration von Maven befassen, müssen wir verstehen, womit wir es zu tun haben.

Unser Code enthält sowohl Java- als auch Groovy-Dateien . Groovy hat überhaupt kein Problem damit, die Java-Klassen zu finden, aber was ist, wenn Java Groovy-Klassen und -Methoden finden soll?

Hier kommt die gemeinsame Zusammenstellung zur Rettung!

Die gemeinsame Kompilierung ist ein Prozess, mit dem sowohl Java- als auch Groovy- Dateien im selben Projekt mit einem einzigen Maven-Befehl kompiliert werden können .

Bei der gemeinsamen Kompilierung wird der Groovy-Compiler:

  • Analysieren Sie die Quelldateien
  • Erstellen Sie je nach Implementierung Stubs, die mit dem Java-Compiler kompatibel sind
  • Rufen Sie den Java-Compiler auf, um die Stubs zusammen mit Java-Quellen zu kompilieren. Auf diese Weise können Java-Klassen Groovy-Abhängigkeiten finden
  • Kompilieren Sie die Groovy-Quellen - jetzt können unsere Groovy-Quellen ihre Java-Abhängigkeiten finden

Abhängig vom implementierten Plugin müssen wir die Dateien möglicherweise in bestimmte Ordner unterteilen oder dem Compiler mitteilen, wo sie zu finden sind.

Ohne gemeinsame Kompilierung würden die Java-Quelldateien so kompiliert, als wären sie Groovy-Quellen. Manchmal funktioniert dies, da der größte Teil der Java 1.7-Syntax mit Groovy kompatibel ist, die Semantik jedoch anders ist.

5. Maven Compiler Plugins

Es gibt einige Compiler-Plugins, die das gemeinsame Kompilieren unterstützen , jedes mit seinen Stärken und Schwächen.

Die beiden am häufigsten bei Maven verwendeten sind Groovy-Eclipse Maven und GMaven +.

5.1. Das Groovy-Eclipse Maven Plugin

Das Groovy-Eclipse Maven-Plugin vereinfacht die gemeinsame Kompilierung, indem die Erzeugung von Stubs vermieden wird. Dies ist für andere Compiler wie GMaven + immer noch ein obligatorischer Schritt, weist jedoch einige Konfigurationsprobleme auf.

Um das Abrufen der neuesten Compiler-Artefakte zu ermöglichen, müssen wir das Maven Bintray-Repository hinzufügen:

  bintray Groovy Bintray //dl.bintray.com/groovy/maven   never   false   

Dann teilen wir dem Maven-Compiler im Plugin-Bereich mit, welche Groovy-Compiler-Version er verwenden muss.

Tatsächlich wird das von uns verwendete Plugin - das Maven-Compiler-Plugin - nicht kompiliert, sondern delegiert den Job an das Groovy-Eclipse-Batch- Artefakt:

 maven-compiler-plugin 3.8.0  groovy-eclipse-compiler ${java.version} ${java.version}    org.codehaus.groovy groovy-eclipse-compiler 3.3.0-01   org.codehaus.groovy groovy-eclipse-batch ${groovy.version}-01   

Die Groovy-All- Abhängigkeitsversion sollte mit der Compilerversion übereinstimmen.

Schließlich müssen wir unsere Quelle automatische Erkennung konfigurieren: standardmäßig der Compiler in Ordnern wie aussehen würde src / main / java und src / main / stark, aber wenn unser Java - Ordner leer ist, sucht der Compiler nicht für unsere groovy Quellen .

Der gleiche Mechanismus gilt für unsere Tests.

Um die Dateierkennung zu erzwingen, können Sie eine beliebige Datei in src / main / java und src / test / java hinzufügen oder einfach das Groovy-Eclipse-Compiler- Plugin hinzufügen :

 org.codehaus.groovy groovy-eclipse-compiler 3.3.0-01 true 

Das Der Abschnitt ist obligatorisch, damit das Plugin die zusätzliche Erstellungsphase und die zusätzlichen Ziele hinzufügen kann, die die beiden Groovy-Quellordner enthalten.

5.2. Das GMavenPlus Plugin

Das GMavenPlus-Plugin hat möglicherweise einen ähnlichen Namen wie das alte GMaven-Plugin, aber anstatt nur einen Patch zu erstellen, bemühte sich der Autor, den Compiler zu vereinfachen und von einer bestimmten Groovy-Version zu entkoppeln .

Dazu trennt sich das Plugin von den Standardrichtlinien für Compiler-Plugins.

Der GMavenPlus-Compiler bietet Unterstützung für Funktionen, die zu diesem Zeitpunkt in anderen Compilern noch nicht vorhanden waren , z. B. invokedynamic, die interaktive Shell-Konsole und Android.

Auf der anderen Seite gibt es einige Komplikationen:

  • 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:

 org.codehaus.gmavenplus gmavenplus-plugin 1.7.0    execute addSources addTestSources generateStubs compile generateTestStubs compileTests removeStubs removeTestStubs      org.codehaus.groovy groovy-all = 1.5.0 should work here --> 2.5.6 runtime pom   

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:

  1. Generating stubs, one for each groovy file
  2. Compiling the Java files – stubs and Java code alike
  3. 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:

 org.apache.maven.plugins maven-assembly-plugin 3.1.0    jar-with-dependencies     com.baeldung.MyJointCompilationApp      make-assembly  package  single    

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 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.

Trotzdem wird das Skript erfolgreich und ohne eine einzige Warnung kompiliert, sowohl statisch in Maven als auch dynamisch im GroovyClassLoader.

Es wird nur fehlschlagen, wenn wir versuchen, es aufzurufen.

Die statische Kompilierung von Maven zeigt nur dann einen Fehler an, wenn unser Java-Code direkt auf calcSum3 () verweist , nachdem das GroovyObject wie in der Methode addWithCompiledClasses () umgewandelt wurde. Es ist jedoch immer noch unwirksam, wenn wir stattdessen Reflection verwenden.

9. Fazit

In diesem Artikel haben wir untersucht, wie wir Groovy in unsere Java-Anwendung integrieren können, und dabei verschiedene Integrationsmethoden und einige der Probleme untersucht, die bei gemischten Sprachen auftreten können.

Der in den Beispielen verwendete Quellcode befindet sich wie gewohnt auf GitHub.