Интегриране на Groovy в Java приложения

1. Въведение

В този урок ще изследваме най-новите техники за интегриране на Groovy в Java приложение.

2. Няколко думи за Groovy

Езикът за програмиране Groovy е мощен, въведен по избор и динамичен език . Поддържа се от Софтуерната фондация Apache и общността Groovy, с принос от повече от 200 разработчици.

Може да се използва за изграждане на цяло приложение, за създаване на модул или допълнителна библиотека, взаимодействаща с нашия Java код, или за стартиране на скриптове, оценени и компилирани в движение.

За повече информация, моля, прочетете Въведение в Groovy Language или отидете на официалната документация.

3. Зависимости на Maven

По време на писането най-новата стабилна версия е 2.5.7, докато Groovy 2.6 и 3.0 (и двете започнаха през есента на 17) все още са на алфа етап.

Подобно на Spring Boot, просто трябва да включим groovy-all pom, за да добавим всички зависимости, от които може да се нуждаем, без да се притесняваме за техните версии:

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

4. Компилация на ставите

Преди да влезем в подробности как да конфигурираме Maven, трябва да разберем с какво си имаме работа.

Нашият код ще съдържа файлове Java и Groovy . Groovy изобщо няма да има проблем с намирането на Java класове, но какво, ако искаме Java да намери Groovy класове и методи?

Тук идва съвместната компилация на помощ!

Съвместната компилация е процес, предназначен да компилира Java и Groovy файлове в един и същ проект, в една команда на Maven.

Със съвместна компилация, компилаторът на Groovy ще:

  • анализирайте изходните файлове
  • в зависимост от изпълнението, създайте заглушки, които са съвместими с Java компилатора
  • извикайте Java компилатора, за да компилирате заглушенията заедно с Java източници - по този начин Java класовете могат да намерят зависимости Groovy
  • компилирайте Groovy източниците - сега нашите Groovy източници могат да намерят своите зависимости Java

В зависимост от приставката, която го изпълнява, може да се наложи да отделим файловете в определени папки или да кажем на компилатора къде да ги намери.

Без съвместна компилация, изходните файлове на Java ще бъдат компилирани така, сякаш са Groovy източници. Понякога това може да работи, тъй като по-голямата част от синтаксиса на Java 1.7 е съвместима с Groovy, но семантиката би била различна.

5. Приставки за компилатор Maven

Налични са няколко приставки за компилатор, които поддържат съвместна компилация , всеки със своите силни и слаби страни.

Двете най-често използвани с Maven са Groovy-Eclipse Maven и GMaven +.

5.1. Приставката Groovy-Eclipse Maven

Приставката Groovy-Eclipse Maven опростява съвместната компилация, като избягва генерирането на заглушки, все още задължителна стъпка за други компилатори като GMaven + , но представя някои странни конфигурации.

За да позволим извличането на най-новите артефакти на компилатора, трябва да добавим хранилището Maven Bintray:

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

След това в раздела за приставки казваме на компилатора на Maven коя версия на компилатора на Groovy трябва да използва.

Всъщност приставката, която ще използваме - приставката на компилатора Maven - всъщност не се компилира, а вместо това делегира работата на артефакта 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   

Версията на зависимостта groovy-all трябва да съвпада с версията на компилатора.

И накрая, трябва да конфигурираме нашето автоматично откриване на източника: по подразбиране компилаторът ще разгледа папки като src / main / java и src / main / groovy, но ако нашата папка java е празна, компилаторът няма да търси нашата groovy източници .

Същият механизъм е валиден и за нашите тестове.

За да принудим откриването на файла, бихме могли да добавим всеки файл в src / main / java и src / test / java или просто да добавим приставката groovy-eclipse-compiler :

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

The Разделът е задължителен, за да позволи на приставката да добави допълнителната фаза на изграждане и цели, съдържащи двете изходни папки Groovy.

5.2. Приставката GMavenPlus

Плъгинът GMavenPlus може да има име, подобно на стария плъгин GMaven, но вместо да създаде обикновена кръпка, авторът направи усилие да опрости и отдели компилатора от конкретна версия на Groovy .

За целта приставката се отделя от стандартните насоки за приставки на компилатора.

Компилаторът GMavenPlus добавя поддръжка за функции, които все още не са присъствали в други компилатори по това време , като invokedynamic, интерактивната конзола на черупката и Android.

От друга страна, това представлява някои усложнения:

  • той модифицира директориите на Maven, за да съдържа както Java, така и Groovy източници, но не и Java скриптове
  • тя изисква от нас да управлява мъничета , ако не ги изтриете с правилните цели

За да конфигурираме нашия проект, трябва да добавим приставката 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   

За да позволим тестване на този плъгин, създадохме втори pom файл, наречен gmavenplus-pom.xml в пробата.

5.3. Компилиране с приставката Eclipse-Maven

Сега, когато всичко е конфигурирано, най-накрая можем да изградим нашите класове.

В примера, който предоставихме, създадохме просто Java приложение в изходната папка src / main / java и някои скриптове Groovy в src / main / groovy , където можем да създадем Groovy класове и скриптове.

Нека да изградим всичко с приставката 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 ...

Тук виждаме, че Groovy съставя всичко .

5.4. Компилиране с GMavenPlus

GMavenPlus показва някои разлики:

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

Веднага забелязваме, че GMavenPlus преминава през допълнителните стъпки на:

  1. Генериране на мъничета, по един за всеки грув файл
  2. Компилиране на Java файлове - еднакво мъничета и Java код
  3. Компилиране на файловете на Groovy

Чрез генериране на заглушки, GMavenPlus наследява слабост, която причинява много главоболия на разработчиците през последните години, когато се работи със съвместна компилация.

В идеалния сценарий всичко би работило добре, но въвеждайки повече стъпки, имаме и повече точки на отказ: например компилацията може да се провали, преди да може да почисти мъничките.

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.

Въпреки това скриптът се компилира успешно, без дори едно предупреждение, както статично в Maven, така и динамично в GroovyClassLoader.

Ще се провали само когато се опитаме да го извикаме.

Статичната компилация на Maven ще покаже грешка само ако нашият код на Java се отнася директно към calcSum3 () , след преместването на GroovyObject, както правим в метода addWithCompiledClasses () , но все пак е неефективен, ако вместо това използваме отражение.

9. Заключение

В тази статия разгледахме как можем да интегрираме Groovy в нашето Java приложение, разглеждайки различни методи за интеграция и някои от проблемите, които можем да срещнем при смесените езици.

Както обикновено, изходният код, използван в примерите, може да бъде намерен в GitHub.