Mengintegrasikan Groovy ke Aplikasi Java

1. Pengenalan

Dalam tutorial ini, kita akan meneroka teknik terkini untuk mengintegrasikan Groovy ke dalam Aplikasi Java.

2. Beberapa Perkataan Tentang Groovy

Bahasa pengaturcaraan Groovy adalah bahasa yang kuat, ditaip pilihan dan dinamik . Ia disokong oleh Apache Software Foundation dan komuniti Groovy, dengan sumbangan daripada lebih daripada 200 pemaju.

Ini dapat digunakan untuk membangun keseluruhan aplikasi, untuk membuat modul atau perpustakaan tambahan yang berinteraksi dengan kod Java kami, atau untuk menjalankan skrip yang dinilai dan disusun dengan cepat.

Untuk maklumat lebih lanjut, sila baca Pengantar Bahasa Groovy atau pergi ke dokumentasi rasmi.

3. Pergantungan Maven

Pada masa penulisan, rilis stabil terbaru ialah 2.5.7, sementara Groovy 2.6 dan 3.0 (keduanya bermula pada musim gugur '17) masih dalam tahap alpha.

Sama seperti Spring Boot, kita hanya perlu memasukkan pom groovy-all untuk menambahkan semua pergantungan yang mungkin kita perlukan, tanpa perlu risau tentang versi mereka:

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

4. Penyusunan Bersama

Sebelum mengetahui perincian cara mengkonfigurasi Maven, kita perlu memahami apa yang sedang kita hadapi.

Kod kami akan mengandungi fail Java dan Groovy . Groovy sama sekali tidak akan menghadapi masalah untuk mencari kelas Java, tetapi bagaimana jika kita mahu Java mencari kelas dan kaedah Groovy?

Inilah kompilasi bersama untuk menyelamatkan!

Penyusunan bersama adalah proses yang dirancang untuk menyusun fail Java dan Groovy dalam projek yang sama, dalam satu perintah Maven.

Dengan penyusunan bersama, penyusun Groovy akan:

  • menghuraikan fail sumber
  • bergantung pada pelaksanaannya, buat stub yang sesuai dengan penyusun Java
  • memanggil penyusun Java untuk menyusun rintisan bersama dengan sumber Java - dengan cara ini kelas Java dapat mencari pergantungan Groovy
  • menyusun sumber Groovy - sekarang sumber Groovy kami dapat mencari kebergantungan Java mereka

Bergantung pada plugin yang melaksanakannya, kami mungkin diminta untuk memisahkan fail ke dalam folder tertentu atau untuk memberitahu penyusun di mana mencarinya.

Tanpa penyusunan bersama, fail sumber Java akan disusun seolah-olah itu adalah sumber Groovy. Kadang-kadang ini mungkin berfungsi kerana kebanyakan sintaks Java 1.7 serasi dengan Groovy, tetapi semantiknya akan berbeza.

5. Plugin Penyusun Maven

Terdapat beberapa plugin penyusun yang menyokong penyusunan bersama , masing-masing mempunyai kelebihan dan kekurangannya.

Dua yang paling biasa digunakan dengan Maven adalah Groovy-Eclipse Maven dan GMaven +.

5.1. Plugin Groovy-Eclipse Maven

Plugin Groovy-Eclipse Maven mempermudah penyusunan bersama dengan mengelakkan penghasilan rintisan , masih merupakan langkah wajib bagi penyusun lain seperti GMaven + , tetapi ia menyajikan beberapa kebiasaan konfigurasi.

Untuk membolehkan pengambilan artifak penyusun terbaru, kita harus menambahkan repositori Maven Bintray:

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

Kemudian, di bahagian pemalam, kami memberitahu penyusun Maven versi penyusun Groovy yang harus digunakan.

Sebenarnya, plugin yang akan kita gunakan - plugin penyusun Maven - sebenarnya tidak menyusun, sebaliknya menyerahkan tugas itu ke artifak groovy-eclipse-batch :

 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   

Yang manis-semua versi pergantungan perlu sepadan dengan versi pengkompil.

Akhirnya, kita perlu mengkonfigurasi autodiscovery sumber kita: secara lalai, penyusun akan melihat folder seperti src / main / java dan src / main / groovy, tetapi jika folder java kita kosong, penyusun tidak akan mencari alur kami sumber .

Mekanisme yang sama berlaku untuk ujian kami.

Untuk memaksa penemuan fail, kami dapat menambahkan fail apa pun dalam src / main / java dan src / test / java , atau hanya menambahkan plugin groovy-eclipse-compiler :

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

The bahagian adalah wajib untuk membiarkan pemalam menambah fasa dan matlamat tambahan, yang mengandungi dua folder sumber Groovy.

5.2. Pemalam GMavenPlus

Plugin GMavenPlus mungkin memiliki nama yang serupa dengan plugin GMaven yang lama, tetapi alih-alih membuat tambalan, penulis berusaha untuk mempermudah dan mencabut penyusun dari versi Groovy tertentu .

Untuk melakukannya, pemalam memisahkan dirinya dari garis panduan standard untuk pemalam penyusun.

Pengkompilasi GMavenPlus menambah sokongan untuk ciri yang masih belum ada pada penyusun lain pada masa itu , seperti invokedynamic, konsol shell interaktif, dan Android.

Di sisi lain, ia menunjukkan beberapa komplikasi:

  • ia memodifikasi direktori sumber Maven untuk memuat sumber Java dan Groovy, tetapi bukan rintisan Java
  • ia memerlukan kita menguruskan rintisan jika kita tidak menghapusnya dengan tujuan yang tepat

Untuk mengkonfigurasi projek kami, kami perlu menambahkan plugin gmavenplus:

 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   

Untuk membolehkan pengujian plugin ini, kami membuat fail pom kedua yang disebut gmavenplus-pom.xml dalam sampel.

5.3. Menyusun Dengan Plugin Eclipse-Maven

Setelah semuanya dikonfigurasi, akhirnya kita dapat membina kelas kita.

Dalam contoh yang kami sediakan, kami membuat aplikasi Java sederhana di folder sumber src / main / java dan beberapa skrip Groovy dalam src / main / groovy , di mana kami dapat membuat kelas dan skrip Groovy.

Mari membina semuanya dengan pemalam Eclipse-Maven:

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

Di sini kita melihat bahawa Groovy menyusun semuanya .

5.4. Menyusun Dengan GMavenPlus

GMavenPlus menunjukkan beberapa perbezaan:

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

Kami dapati dengan segera bahawa GMavenPlus melalui langkah-langkah tambahan:

  1. Menghasilkan stub, satu untuk setiap fail groovy
  2. Menyusun fail Java - stub dan kod Java sama
  3. Menyusun fail Groovy

Dengan menghasilkan stub, GMavenPlus mewarisi kelemahan yang menyebabkan banyak pening kepala kepada pembangun dalam beberapa tahun terakhir, ketika bekerja dengan penyusunan bersama.

Dalam senario yang ideal, semuanya akan berjalan lancar, tetapi dengan memperkenalkan lebih banyak langkah, kita juga mempunyai lebih banyak titik kegagalan: sebagai contoh, binaan mungkin gagal sebelum dapat membersihkan rintisan.

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.

Walaupun begitu, skrip berjaya disusun, tanpa satu pun amaran, baik secara statik di Maven dan secara dinamis di GroovyClassLoader.

Ia akan gagal hanya apabila kita berusaha menggunakannya.

Penyusunan statik Maven akan menunjukkan ralat hanya jika kod Java kita merujuk terus ke calcSum3 () , setelah membuang GroovyObject seperti yang kita lakukan dalam kaedah addWithCompiledClasses () , tetapi masih tidak berkesan jika kita menggunakan refleksi.

9. Kesimpulannya

Dalam artikel ini, kami meneroka bagaimana kami dapat mengintegrasikan Groovy dalam aplikasi Java kami, melihat kaedah penyatuan yang berbeza dan beberapa masalah yang mungkin kami hadapi dengan bahasa campuran.

Seperti biasa, kod sumber yang digunakan dalam contoh boleh didapati di GitHub.