Groovy integreren in Java-applicaties

1. Inleiding

In deze tutorial verkennen we de nieuwste technieken om Groovy te integreren in een Java-applicatie.

2. Een paar woorden over Groovy

De programmeertaal Groovy is een krachtige, optioneel getypte en dynamische taal. Het wordt ondersteund door de Apache Software Foundation en de Groovy-community, met bijdragen van meer dan 200 ontwikkelaars.

Het kan worden gebruikt om een ​​volledige applicatie te bouwen, om een ​​module of een extra bibliotheek te maken die in wisselwerking staat met onze Java-code, of om scripts uit te voeren die ter plekke worden geëvalueerd en gecompileerd.

Lees voor meer informatie Inleiding tot Groovy Language of ga naar de officiële documentatie.

3. Maven afhankelijkheden

Op het moment van schrijven is de laatste stabiele release 2.5.7, terwijl Groovy 2.6 en 3.0 (beide gestart in de herfst '17) zich nog in de alpha-fase bevinden.

Net als bij Spring Boot, we hoeven alleen de groovy-all pom om alle afhankelijkheden toe te voegen we hebben misschien nodig, zonder ons zorgen te hoeven maken over hun versies:

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

4. Gezamenlijke samenstelling

Voordat we ingaan op de details van het configureren van Maven, moeten we begrijpen waar we mee te maken hebben.

Onze code bevat zowel Java- als Groovy-bestanden. Groovy zal geen enkel probleem hebben om de Java-klassen te vinden, maar wat als we willen dat Java Groovy-klassen en -methoden vindt?

Hier komt een gezamenlijke compilatie te hulp!

Gezamenlijke compilatie is een proces dat is ontworpen om zowel Java als Groovy te compileren bestanden in hetzelfde project, in een enkele Maven-opdracht.

Met een gezamenlijke compilatie zal de Groovy-compiler:

  • ontleden de bronbestanden
  • maak, afhankelijk van de implementatie, stubs die compatibel zijn met de Java-compiler
  • roep de Java-compiler aan om de stubs samen met Java-bronnen te compileren - op deze manier kunnen Java-klassen Groovy-afhankelijkheden vinden
  • compileer de Groovy-bronnen - nu kunnen onze Groovy-bronnen hun Java-afhankelijkheden vinden

Afhankelijk van de plug-in die het implementeert, kan het nodig zijn om de bestanden in specifieke mappen te scheiden of om de compiler te vertellen waar hij ze kan vinden.

Zonder gezamenlijke compilatie zouden de Java-bronbestanden worden gecompileerd alsof het Groovy-bronnen waren. Soms werkt dit misschien omdat de meeste Java 1.7-syntaxis compatibel is met Groovy, maar de semantiek zou anders zijn.

5. Maven Compiler-plug-ins

Er zijn een paar compilerplug-ins beschikbaar die gezamenlijke compilatie ondersteunen, elk met zijn sterke en zwakke punten.

De twee meest gebruikte Maven zijn Groovy-Eclipse Maven en GMaven +.

5.1. De Groovy-Eclipse Maven-plug-in

De Groovy-Eclipse Maven-plug-in vereenvoudigt de gezamenlijke compilatie door het genereren van stubs te vermijden, nog steeds een verplichte stap voor andere compilers zoals GMaven+, maar het vertoont enkele configuratie-eigenaardigheden.

Om het ophalen van de nieuwste compilerartefacten mogelijk te maken, moeten we de Maven Bintray-repository toevoegen:

  bintray Groovy Bintray //dl.bintray.com/groovy/maven nooit onwaar 

Vervolgens, in het plug-in-gedeelte, we vertellen de Maven-compiler welke Groovy-compilerversie hij moet gebruiken.

In feite compileert de plug-in die we zullen gebruiken - de Maven-compiler-plug-in - niet echt, maar delegeert in plaats daarvan de taak aan de groovy-eclipse-batch artefact:

 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 

De groovy-all afhankelijkheidsversie moet overeenkomen met de compilerversie.

Ten slotte moeten we onze bronautodiscovery configureren: standaard kijkt de compiler naar mappen zoals src / main / java en src / main / groovy, maar als onze java-map leeg is, zal de compiler niet zoeken naar onze groovy sources.

Hetzelfde mechanisme is geldig voor onze tests.

Om de bestandsdetectie te forceren, kunnen we elk bestand toevoegen aan src / main / java en src / test / java, of voeg gewoon de groovy-eclipse-compiler inpluggen:

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

De sectie is verplicht om de plug-in de extra build-fase en doelen te laten toevoegen, die de twee Groovy-bronmappen bevatten.

5.2. De GMavenPlus-plug-in

De GMavenPlus-plug-in heeft misschien een naam die lijkt op de oude GMaven-plug-in, maar in plaats van alleen een patch te maken, deed de auteur een poging om vereenvoudig en ontkoppel de compiler van een specifieke Groovy-versie.

Om dit te doen, onderscheidt de plug-in zich van de standaardrichtlijnen voor compiler-plug-ins.

De GMavenPlus-compiler voegt ondersteuning toe voor functies die op dat moment nog niet aanwezig waren in andere compilers, zoals invokedynamic, de interactieve shell-console en Android.

Aan de andere kant levert het enkele complicaties op:

  • het wijzigt de bronmappen van Maven om zowel de Java- als de Groovy-broncode te bevatten, maar niet de Java-stubs
  • het vereist dat we stubs beheren als we ze niet verwijderen met de juiste doelen

Om ons project te configureren, moeten we de gmavenplus-plug-in toevoegen:

 org.codehaus.gmavenplus gmavenplus-plugin 1.7.0 uitvoeren addSources addTestSources genererenStubs compileren genererenTestStubs compilerenTests removeStubs removeTestStubs org.codehaus.groovy groovy-all = 1.5.0 zou hier moeten werken -> 2.5.6 runtime pom 

Om het testen van deze plug-in mogelijk te maken, hebben we een tweede pom-bestand gemaakt met de naam gmavenplus-pom.xml in de steekproef.

5.3. Compileren met de Eclipse-Maven-plug-in

Nu alles is geconfigureerd, kunnen we eindelijk onze klassen bouwen.

In het voorbeeld dat we hebben gegeven, hebben we een eenvoudige Java-applicatie gemaakt in de bronmap src / main / java en enkele Groovy-scripts in src / main / groovy, waar we Groovy-klassen en -scripts kunnen maken.

Laten we alles bouwen met de Eclipse-Maven-plug-in:

$ mvn clean compileren ... [INFO] --- maven-compiler-plugin: 3.8.0: compileren (standaard-compileren) @ core-groovy-2 --- [INFO] Veranderingen gedetecteerd - hercompileren van de module! [INFO] Groovy-Eclipse-compiler gebruiken om zowel Java- als Groovy-bestanden te compileren ...

Hier zien we dat Groovy stelt alles samen.

5.4. Compileren met GMavenPlus

GMavenPlus laat enkele verschillen zien:

$ mvn -f gmavenplus-pom.xml schone compilatie ... [INFO] --- gmavenplus-plugin: 1.7.0: GenereerStubs (standaard) @ core-groovy-2 --- [INFO] Groovy 2.5.7 gebruiken om genereerStubs uitvoeren. [INFO] 2 stubs gegenereerd. [INFO] ... [INFO] --- maven-compiler-plugin: 3.8.1: compileren (standaard-compileren) @ core-groovy-2 --- [INFO] Veranderingen gedetecteerd - hercompileren van de module! [INFO] Compileren van 3 bronbestanden naar XXX \ Baeldung \ TutorialsRepo \ core-groovy-2 \ target \ classes [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: compileren (standaard) @ core- groovy-2 --- [INFO] Groovy 2.5.7 gebruiken om te compileren. [INFO] 2 bestanden gecompileerd. [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: removeStubs (standaard) @ core-groovy-2 --- [INFO] ...

We merken meteen dat GMavenPlus de extra stappen doorloopt van:

  1. Stubs genereren, één voor elk groovy bestand
  2. Het compileren van de Java-bestanden - zowel stubs als Java-code
  3. Het compileren van de Groovy-bestanden

Door stubs te genereren, erft GMavenPlus een zwakte die de ontwikkelaars de afgelopen jaren veel hoofdpijn bezorgde bij het werken met gezamenlijke compilatie.

In het ideale scenario zou alles prima werken, maar door meer stappen te introduceren, hebben we ook meer faalpunten: bijvoorbeeld het bouwen kan mislukken voordat de stubs kunnen worden opgeruimd.

Als dit gebeurt, kunnen oude achtergelaten stubs onze IDE verwarren, die compilatiefouten zou laten zien waarvan we weten dat alles correct zou moeten zijn.

Alleen een schone bouw zou dan een pijnlijke en lange heksenjacht voorkomen.

5.5. Verpakkingsafhankelijkheden in het Jar-bestand

Naar voer het programma uit als een jar vanaf de opdrachtregelhebben we de maven-assembly-plugin, die alle Groovy-afhankelijkheden zal bevatten in een "fat jar" genaamd met de postfix gedefinieerd in de eigenschap descriptor Ref:

 org.apache.maven.plugins maven-assembly-plugin 3.1.0 jar-met-afhankelijkheden com.baeldung.MyJointCompilationApp make-assembly pakket enkel 

Zodra de compilatie is voltooid, kunnen we onze code uitvoeren met deze opdracht:

$ java -jar doel / core-groovy-2-1.0-SNAPSHOT-jar-met-afhankelijkheden.jar com.baeldung.MyJointCompilationApp

6. Laden van Groovy Code on the Fly

Met de Maven-compilatie kunnen we Groovy-bestanden in ons project opnemen en verwijzen naar hun klassen en methoden vanuit Java.

Hoewel dit niet voldoende is als we de logica tijdens runtime willen wijzigen: de compilatie draait buiten de runtime-fase, dus we moeten onze applicatie nog steeds herstarten om onze wijzigingen te zien.

Om te profiteren van de dynamische kracht (en risico's) van Groovy, moeten we de beschikbare technieken onderzoeken om onze bestanden te laden wanneer onze applicatie al draait.

6.1. GroovyClassLoader

Om dit te bereiken hebben we de GroovyClassLoader, die broncode in tekst- of bestandsformaat kan parseren en de resulterende klasseobjecten kan genereren.

Als de bron een bestand is, wordt het compilatieresultaat ook in de cache opgeslagen, om overhead te vermijden wanneer we de loader meerdere instanties van dezelfde klasse vragen.

Script dat rechtstreeks afkomstig is van een Draad object wordt in plaats daarvan niet in de cache opgeslagen, dus hetzelfde script meerdere keren aanroepen kan nog steeds geheugenlekken veroorzaken.

GroovyClassLoader is de basis waarop andere integratiesystemen zijn gebouwd.

De implementatie is relatief eenvoudig:

privé laatste GroovyClassLoader-lader; private Double addWithGroovyClassLoader (int x, int y) gooit IllegalAccessException, InstantiationException, IOException {Class calcClass = loader.parseClass (nieuw bestand ("src / main / groovy / com / baeldung /", "CalcMath.groovy")); GroovyObject calc = (GroovyObject) calcClass.newInstance (); return (Double) calc.invokeMethod ("calcSum", new Object [] {x, y}); } openbare MyJointCompilationApp () {loader = nieuwe GroovyClassLoader (this.getClass (). getClassLoader ()); // ...} 

6.2. GroovyShell

De Shell Script Loader ontleden () methode accepteert bronnen in tekst- of bestandsformaat en genereert een instantie van de Script klasse.

Deze instantie erft het rennen() methode van Script, die het volledige bestand van boven naar beneden uitvoert en het resultaat retourneert dat is gegeven door de laatst uitgevoerde regel.

Als we willen, kunnen we ook verlengen Script in onze code, en overschrijf de standaardimplementatie om direct onze interne logica aan te roepen.

De implementatie om te bellen Script.run () het lijkt op dit:

private Double addWithGroovyShellRun (int x, int y) gooit IOException {Script script = shell.parse (nieuw bestand ("src / main / groovy / com / baeldung /", "CalcScript.groovy")); return (dubbel) script.run (); } openbare MyJointCompilationApp () {// ... shell = nieuwe GroovyShell (loader, nieuwe Binding ()); // ...} 

Houd er rekening mee dat de rennen() accepteert geen parameters, dus we zouden aan ons bestand een aantal globale variabelen moeten toevoegen om ze te initialiseren via de Verbindend voorwerp.

Omdat dit object wordt doorgegeven in de GroovyShell initialisatie, worden de variabelen gedeeld met alle Script gevallen.

Als we de voorkeur geven aan een meer gedetailleerde controle, kunnen we gebruik maken van invokeMethod (), die via reflectie toegang hebben tot onze eigen methoden en direct argumenten kunnen doorgeven.

Laten we eens kijken naar deze implementatie:

private finale GroovyShell-shell; private Double addWithGroovyShell (int x, int y) gooit IOException {Script script = shell.parse (nieuw bestand ("src / main / groovy / com / baeldung /", "CalcScript.groovy")); return (Double) script.invokeMethod ("calcSum", new Object [] {x, y}); } openbare MyJointCompilationApp () {// ... shell = nieuwe GroovyShell (loader, nieuwe Binding ()); // ...} 

Onder de dekens, GroovyShell vertrouwt op de GroovyClassLoader voor het compileren en cachen van de resulterende klassen, dus dezelfde regels die eerder zijn uitgelegd, zijn op dezelfde manier van toepassing.

6.3. GroovyScriptEngine

De GroovyScriptEngine class is in het bijzonder voor die toepassingen die vertrouwen op het herladen van een script en zijn afhankelijkheden.

Hoewel we deze extra functies hebben, heeft de implementatie slechts een paar kleine verschillen:

private laatste GroovyScriptEngine-engine; private void addWithGroovyScriptEngine (int x, int y) gooit IllegalAccessException, InstantiationException, ResourceException, ScriptException {Class calcClass = engine.loadScriptByName ("CalcMath.groovy"); GroovyObject calc = calcClass.newInstance (); Object resultaat = calc.invokeMethod ("calcSum", nieuw object [] {x, y}); LOG.info ("Resultaat van de methode CalcMath.calcSum () is {}", resultaat); } openbare MyJointCompilationApp () {... URL url = null; probeer {url = nieuw bestand ("src / main / groovy / com / baeldung /"). toURI (). toURL (); } catch (MalformedURLException e) {LOG.error ("Uitzondering tijdens het aanmaken van url", e); } engine = nieuwe GroovyScriptEngine (nieuwe URL [] {url}, this.getClass (). getClassLoader ()); engineFromFactory = nieuwe GroovyScriptEngineFactory (). getScriptEngine (); }

Deze keer moeten we de bronwortels configureren en verwijzen we naar het script met alleen de naam, die een beetje schoner is.

Binnenkijken loadScriptByName methode, kunnen we meteen de cheque zien isSourceNewer waar de engine controleert of de bron die momenteel in de cache staat nog steeds geldig is.

Elke keer dat ons bestand verandert, GroovyScriptEngine zal automatisch dat specifieke bestand opnieuw laden en alle klassen die ervan afhankelijk zijn.

Hoewel dit een handige en krachtige functie is, kan het een zeer gevaarlijke bijwerking veroorzaken: Het vele malen herladen van een groot aantal bestanden zal zonder waarschuwing resulteren in CPU-overhead.

Als dat gebeurt, moeten we mogelijk ons ​​eigen cachemechanisme implementeren om dit probleem op te lossen.

6.4. GroovyScriptEngineFactory (JSR-223)

JSR-223 biedt een standaard API voor het aanroepen van scriptframeworks sinds Java 6.

De implementatie lijkt op elkaar, hoewel we teruggaan naar het laden via volledige bestandspaden:

privé definitieve ScriptEngine engineFromFactory; private void addWithEngineFactory (int x, int y) gooit IllegalAccessException, InstantiationException, javax.script.ScriptException, FileNotFoundException {Class calcClas = (Class) engineFromFactory.eval (nieuwe FileReader (nieuw bestand ("src / main / groovy / com / baeldung / "," CalcMath.groovy "))); GroovyObject calc = (GroovyObject) calcClas.newInstance (); Object resultaat = calc.invokeMethod ("calcSum", nieuw object [] {x, y}); LOG.info ("Resultaat van de methode CalcMath.calcSum () is {}", resultaat); } openbare MyJointCompilationApp () {// ... engineFromFactory = nieuwe GroovyScriptEngineFactory (). getScriptEngine (); }

Het is geweldig als we onze app integreren met verschillende scripttalen, maar de functieset is beperkter. Bijvoorbeeld, het ondersteunt geen herladen van klassen. Als we dus alleen integreren met Groovy, is het misschien beter om vast te houden aan eerdere benaderingen.

7. Valkuilen van dynamische compilatie

Met behulp van een van de bovenstaande methoden kunnen we een applicatie maken die leest scripts of klassen uit een specifieke map buiten ons jar-bestand.

Dit zou ons de flexibiliteit om nieuwe functies toe te voegen terwijl het systeem draait (tenzij we nieuwe code nodig hebben in het Java-gedeelte), waardoor een soort van Continuous Delivery-ontwikkeling wordt bereikt.

Maar pas op voor dit tweesnijdend zwaard: we moeten ons er nu heel voorzichtig tegen beschermen fouten die zowel tijdens het compileren als tijdens de uitvoering kunnen optreden, de facto ervoor zorgen dat onze code veilig faalt.

8. Valkuilen bij het uitvoeren van Groovy in een Java-project

8.1. Prestatie

We weten allemaal dat wanneer een systeem zeer performant moet zijn, er enkele gouden regels moeten worden gevolgd.

Twee die mogelijk meer wegen op ons project zijn:

  • vermijd reflectie
  • minimaliseer het aantal bytecode-instructies

Met name reflectie is een kostbare operatie vanwege het proces van het controleren van de klasse, de velden, de methoden, de methodeparameters, enzovoort.

Als we de methodeaanroepen van Java naar Groovy analyseren, bijvoorbeeld tijdens het uitvoeren van het voorbeeld addWithCompiledClasses, de stapel operatie tussen .calcSum en de eerste regel van de eigenlijke Groovy-methode ziet er als volgt uit:

calcSum: 4, CalcScript (com.baeldung) addWithCompiledClasses: 43, MyJointCompilationApp (com.baeldung) addWithStaticCompiledClasses: 95, MyJointCompilationApp (com.baeldung) main: 117, App (com.baeldung)

Dat is consistent met Java. Hetzelfde gebeurt wanneer we het object casten dat door de lader is geretourneerd en de methode ervan aanroepen.

Dit is echter wat de invokeMethod call doet:

calcSum: 4, CalcScript (com.baeldung) invoke0: -1, NativeMethodAccessorImpl (sun.reflect) aanroepen: 62, NativeMethodAccessorImpl (sun.reflect) aanroepen: 43, DelegatingMethodAccessorImpl (sun.reflect) aanroepen: 498, Method (java.lang .reflect) aanroepen: 101, CachedMethod (org.codehaus.groovy.reflection) doMethodInvoke: 323, MetaMethod (groovy.lang) invokeMethod: 1217, MetaClassImpl (groovy.lang) invokeMethod: 1041, MetaClassImpl (groovy.lang) aanroepen , MetaClassImpl (groovy.lang) invokeMethod: 44, GroovyObjectSupport (groovy.lang) invokeMethod: 77, Script (groovy.lang) addWithGroovyShell: 52, MyJointCompilationApp (com.baeldung) addWithDynamicCompiledClasses: 99, MyJointApp , MyJointCompilationApp (com.baeldung)

In dit geval kunnen we begrijpen wat er echt achter de kracht van Groovy schuilgaat: de MetaClass.

EEN MetaClass definieert het gedrag van een bepaalde Groovy- of Java-klasse, dus Groovy kijkt ernaar wanneer er een dynamische bewerking moet worden uitgevoerd om de doelmethode of het doelveld te vinden. Eenmaal gevonden, voert de standaard reflectiestroom het uit.

Twee gouden regels gebroken met één aanroepmethode!

Als we met honderden dynamische Groovy-bestanden moeten werken, hoe we onze methoden noemen, zal dan een enorm prestatieverschil maken in ons systeem.

8.2. Methode of eigenschap niet gevonden

Zoals eerder vermeld, als we dat willen implementeer nieuwe versies van Groovy-bestanden in een cd-levenscyclus is dat nodig behandel ze alsof ze een API waren los van ons kernsysteem.

Dit betekent plaatsen meerdere faalveilige controles en code-ontwerpbeperkingen zodat onze nieuw toegetreden ontwikkelaar het productiesysteem niet met een verkeerde druk opblaast.

Voorbeelden van elk zijn: het hebben van een CI-pijplijn en het gebruik van afschaffing van methoden in plaats van verwijdering.

Wat gebeurt er als we dat niet doen? We krijgen vreselijke uitzonderingen vanwege ontbrekende methoden en verkeerde tellingen en typen argumenten.

En als we denken dat compilatie ons zou redden, laten we dan eens kijken naar de methode calcSum2 () van onze Groovy-scripts:

// deze methode zal mislukken in runtime def calcSum2 (x, y) {// GEVAAR! De variabele "log" kan ongedefinieerd zijn. Log.info "Uitvoeren van $ x + $ y" // GEVAAR! Deze methode bestaat niet! calcSum3 () // GEVAAR! De gelogde variabele "z" is niet gedefinieerd! log.info ("Loggen van een ongedefinieerde variabele: $ z")}

Door het hele bestand door te kijken, zien we meteen twee problemen: de methode calcSum3 () en de variabele z worden nergens gedefinieerd.

Toch is het script met succes gecompileerd, zonder ook maar een enkele waarschuwing, zowel statisch in Maven als dynamisch in de GroovyClassLoader.

Het zal alleen mislukken als we het proberen in te roepen.

De statische compilatie van Maven geeft alleen een foutmelding als onze Java-code rechtstreeks verwijst naar calcSum3 (), na het casten van het GroovyObject zoals we doen in de addWithCompiledClasses () methode, maar het is nog steeds niet effectief als we in plaats daarvan reflectie gebruiken.

9. Conclusie

In dit artikel hebben we onderzocht hoe we Groovy kunnen integreren in onze Java-applicatie, waarbij we kijken naar verschillende integratiemethoden en enkele van de problemen die we kunnen tegenkomen met gemengde talen.

Zoals gewoonlijk is de broncode die in de voorbeelden wordt gebruikt, te vinden op GitHub.