Integrering af Groovy i Java-applikationer

1. Introduktion

I denne vejledning undersøger vi de nyeste teknikker til at integrere Groovy i en Java-applikation.

2. Et par ord om groovy

Groovy-programmeringssproget er et stærkt, valgfrit-skrevet og dynamisk sprog. Det understøttes af Apache Software Foundation og Groovy-samfundet med bidrag fra mere end 200 udviklere.

Det kan bruges til at oprette en hel applikation, til at oprette et modul eller et ekstra bibliotek, der interagerer med vores Java-kode, eller til at køre scripts, der er evalueret og kompileret i farten.

For mere information, se Introduktion til Groovy Language eller gå til den officielle dokumentation.

3. Maven-afhængigheder

I skrivende stund er den seneste stabile udgivelse 2.5.7, mens Groovy 2.6 og 3.0 (begge startede i efteråret '17) stadig er i alfa-fase.

Svarende til Spring Boot, vi skal bare medtage groovy-all pom for at tilføje alle afhængigheder vi har muligvis brug for uden at bekymre os om deres versioner:

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

4. Fælles kompilering

Før vi går i detaljer om, hvordan man konfigurerer Maven, skal vi forstå, hvad vi har at gøre med.

Vores kode indeholder både Java- og Groovy-filer. Groovy har slet ikke noget problem med at finde Java-klasser, men hvad hvis vi vil have Java til at finde Groovy-klasser og -metoder?

Her kommer fælles samling til undsætning!

Fælles kompilering er en proces designet til at kompilere både Java og Groovy filer i det samme projekt i en enkelt Maven-kommando.

Med fælles kompilering vil Groovy-kompilatoren:

  • parse kildefilerne
  • afhængigt af implementeringen skal du oprette stubs, der er kompatible med Java-kompilatoren
  • påkald Java-kompilatoren for at kompilere stubberne sammen med Java-kilder - på denne måde kan Java-klasser finde Groovy-afhængigheder
  • kompiler Groovy-kilderne - nu kan vores Groovy-kilder finde deres Java-afhængigheder

Afhængigt af det plugin, der implementerer det, kan det være nødvendigt, at vi adskiller filerne i bestemte mapper eller fortæller compileren, hvor de skal findes.

Uden fælles kompilering ville Java-kildefiler blive kompileret som om de var Groovy-kilder. Nogle gange fungerer dette muligvis, da det meste af Java 1.7-syntaksen er kompatibel med Groovy, men semantikken ville være anderledes.

5. Maven-kompilator-plugins

Der er et par compiler-plugins til rådighed, der understøtter fælles kompilering, hver med sine styrker og svagheder.

De to mest anvendte med Maven er Groovy-Eclipse Maven og GMaven +.

5.1. Groovy-Eclipse Maven-pluginet

Groovy-Eclipse Maven-pluginet forenkler den fælles samling ved at undgå generering af stubbe, stadig et obligatorisk trin for andre compilere som GMaven+, men det præsenterer nogle konfigurationsspørgsmål.

For at muliggøre hentning af de nyeste compiler-artefakter skal vi tilføje Maven Bintray-arkivet:

  bintray Groovy Bintray //dl.bintray.com/groovy/maven aldrig falsk 

Derefter, i pluginsektionen, vi fortæller Maven-compileren, hvilken Groovy-compilerversion den skal bruge.

Faktisk kompilerer det plugin, vi vil bruge - Maven-compiler-pluginet - faktisk ikke, men i stedet delegerer jobbet til 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 

Det groovy-all afhængighedsversion skal matche compilerversionen.

Endelig er vi nødt til at konfigurere vores kilde autodiscovery: som standard ville compileren se i mapper som f.eks src / main / java og src / main / groovy, men hvis vores java-mappe er tom, vil kompilatoren ikke se efter vores groovy kilder.

Den samme mekanisme gælder for vores tests.

For at tvinge filopdagelsen kunne vi tilføje enhver fil i src / main / java og src / test / java, eller blot tilføj groovy-eclipse-compiler plugin:

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

Det sektion er obligatorisk for at lade pluginet tilføje den ekstra build-fase og mål, der indeholder de to Groovy-kildemapper.

5.2. GMavenPlus-pluginet

GMavenPlus-pluginnet kan have et navn, der ligner det gamle GMaven-plugin, men i stedet for at oprette et simpelt program, bestræbte forfatteren sig på at forenkle og afkoble compileren fra en bestemt Groovy-version.

For at gøre dette adskiller pluginet sig fra standardretningslinjerne for compiler-plugins.

GMavenPlus-kompilatoren tilføjer support til funktioner, der på det tidspunkt stadig ikke var til stede i andre compilere, såsom invokedynamic, den interaktive shell-konsol og Android.

På den anden side præsenterer det nogle komplikationer:

  • det ændrer Mavens kildekataloger at indeholde både Java- og Groovy-kilderne, men ikke Java-stubberne
  • det kræver, at vi administrerer stubbe hvis vi ikke sletter dem med de rette mål

For at konfigurere vores projekt skal vi tilføje gmavenplus-pluginet:

 org.codehaus.gmavenplus gmavenplus-plugin 1.7.0 udfør addSources addTestSources generereStubs kompilerere generereTestStubs compileTests removeStubs removeTestStubs org.codehaus.groovy groovy-all = 1.5.0 skal fungere her -> 2.5.6 runtime pom 

For at tillade test af dette plugin oprettede vi en anden pom-fil kaldet gmavenplus-pom.xml i prøven.

5.3. Kompilering med Eclipse-Maven Plugin

Nu hvor alt er konfigureret, kan vi endelig bygge vores klasser.

I det eksempel, vi gav, oprettede vi en simpel Java-applikation i kildemappen src / main / java og nogle Groovy-scripts i src / main / groovy, hvor vi kan oprette Groovy-klasser og scripts.

Lad os bygge alt med Eclipse-Maven-pluginet:

$ mvn clean compile ... [INFO] --- maven-compiler-plugin: 3.8.0: compile (default-compile) @ core-groovy-2 --- [INFO] Registrerede ændringer - genkompilering af modulet! [INFO] Brug af Groovy-Eclipse-compiler til at kompilere både Java- og Groovy-filer ...

Her ser vi det Groovy kompilerer alt.

5.4. Kompilering med GMavenPlus

GMavenPlus viser nogle forskelle:

$ mvn -f gmavenplus-pom.xml ren kompilering ... [INFO] --- gmavenplus-plugin: 1.7.0: generereStubs (standard) @ core-groovy-2 --- [INFO] Brug af Groovy 2.5.7 til udfør generereStubs. [INFO] Genererede 2 stubbe. [INFO] ... [INFO] --- maven-compiler-plugin: 3.8.1: kompilering (standardkompilering) @ core-groovy-2 --- [INFO] Registrerede ændringer - kompilering af modulet igen! [INFO] Kompilering af 3 kildefiler til XXX \ Baeldung \ TutorialsRepo \ core-groovy-2 \ target \ classes [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: kompilering (standard) @ core- groovy-2 --- [INFO] Brug af Groovy 2.5.7 til at udføre kompilering. [INFO] Kompilerede 2 filer. [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: removeStubs (standard) @ core-groovy-2 --- [INFO] ...

Vi bemærker med det samme, at GMavenPlus gennemgår de yderligere trin:

  1. Generere stubber, en til hver groovy fil
  2. Kompilering af Java-filer - stubs og Java-kode ens
  3. Kompilering af Groovy-filer

Ved at generere stubber arver GMavenPlus en svaghed, der har forårsaget mange hovedpine for udviklere i de sidste år, når de arbejder med fælles kompilering.

I det ideelle scenario ville alt fungere fint, men ved at introducere flere trin har vi også flere fejlpunkter: for eksempel bygningen mislykkes muligvis, før den er i stand til at rense stubberne.

Hvis dette sker, kan gamle stubber, der er tilbage, forvirre vores IDE, som derefter viser kompileringsfejl, hvor vi ved, at alt skal være korrekt.

Kun en ren bygning ville så undgå en smertefuld og lang heksejagt.

5.5. Emballageafhængigheder i Jar-filen

Til kør programmet som en krukke fra kommandolinjen, vi tilføjede maven-assemblage-plugin, som vil omfatte alle de Groovy afhængigheder i en "fedt krukke" navngivet med postfixet defineret i ejendommen deskriptorRef:

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

Når kompileringen er færdig, kan vi køre vores kode med denne kommando:

$ java -jar target / core-groovy-2-1.0-SNAPSHOT-jar-with-dependencies.jar com.baeldung.MyJointCompilationApp

6. Indlæsning af Groovy-kode i farten

Maven-kompilationen lad os inkludere Groovy-filer i vores projekt og henvise til deres klasser og metoder fra Java.

Selv om dette ikke er nok, hvis vi vil ændre logikken ved runtime: kompilering kører uden for runtime-scenen, så Vi er stadig nødt til at genstarte vores applikation for at se vores ændringer.

For at udnytte Groovys dynamiske kraft (og risici) er vi nødt til at undersøge de tilgængelige teknikker til at indlæse vores filer, når vores applikation allerede kører.

6.1. GroovyClassLoader

For at opnå dette har vi brug for GroovyClassLoader, som kan analysere kildekoden i tekst eller filformat og generere de resulterende klasseobjekter.

Når kilden er en fil, gemmes resultatet af kompileringen også, for at undgå overhead, når vi spørger læsseren om flere forekomster af samme klasse.

Script kommer direkte fra en Snor objekt vil i stedet ikke blive cachelagretDerfor kan det stadig forårsage hukommelseslækage at kalde det samme script flere gange.

GroovyClassLoader er grundlaget for andre integrationssystemer.

Implementeringen er relativt enkel:

privat endelig GroovyClassLoader loader; privat dobbelt addWithGroovyClassLoader (int x, int y) kaster IllegalAccessException, InstantiationException, IOException {Class calcClass = loader.parseClass (ny fil ("src / main / groovy / com / baeldung /", "CalcMath.groovy")); GroovyObject calc = (GroovyObject) calcClass.newInstance (); returner (Double) calc.invokeMethod ("calcSum", nyt objekt [] {x, y}); } offentlig MyJointCompilationApp () {loader = ny GroovyClassLoader (this.getClass (). getClassLoader ()); // ...} 

6.2. GroovyShell

Shell Script Loader parse () metoden accepterer kilder i tekst eller filformat og genererer en forekomst af Manuskript klasse.

Denne instans arver løb() metode fra Manuskript, som udfører hele filen fra top til bund og returnerer resultatet givet af den sidst udførte linje.

Hvis vi vil, kan vi også udvide Manuskript i vores kode og tilsidesætter standardimplementeringen for direkte at kalde vores interne logik.

Implementeringen at ringe til Script.run () ser sådan ud:

privat dobbelt addWithGroovyShellRun (int x, int y) kaster IOException {Script script = shell.parse (ny fil ("src / main / groovy / com / baeldung /", "CalcScript.groovy")); returner (dobbelt) script.run (); } offentlig MyJointCompilationApp () {// ... shell = ny GroovyShell (loader, ny Binding ()); // ...} 

Bemærk, at løb() accepterer ikke parametre, så vi bliver nødt til at føje til vores fil nogle globale variabler, initialisere dem gennem Bindende objekt.

Da dette objekt overføres i GroovyShell initialisering deles variablerne med alle Manuskript tilfælde.

Hvis vi foretrækker en mere detaljeret kontrol, kan vi bruge påkaldeMethod (), som kan få adgang til vores egne metoder gennem refleksion og videregive argumenter direkte.

Lad os se på denne implementering:

privat endelig GroovyShell skal; privat dobbelt addWithGroovyShell (int x, int y) kaster IOException {Script script = shell.parse (ny fil ("src / main / groovy / com / baeldung /", "CalcScript.groovy")); returner (dobbelt) script.invokeMethod ("calcSum", nyt objekt [] {x, y}); } offentlig MyJointCompilationApp () {// ... shell = ny GroovyShell (loader, ny Binding ()); // ...} 

Under dækkene, GroovyShell stoler på GroovyClassLoader til kompilering og caching af de resulterende klasser, så de samme regler, der er forklaret tidligere, gælder på samme måde.

6.3. GroovyScriptEngine

Det GroovyScriptEngine klasse er især til de applikationer, der stole på genindlæsning af et script og dets afhængighed.

Selvom vi har disse ekstra funktioner, har implementeringen kun nogle få små forskelle:

privat endelig GroovyScriptEngine-motor; privat ugyldigt addWithGroovyScriptEngine (int x, int y) kaster IllegalAccessException, InstantiationException, ResourceException, ScriptException {Class calcClass = engine.loadScriptByName ("CalcMath.groovy"); GroovyObject calc = calcClass.newInstance (); Objektresultat = calc.invokeMethod ("calcSum", nyt objekt [] {x, y}); LOG.info ("Resultatet af CalcMath.calcSum () -metoden er {}", resultat); } offentlig MyJointCompilationApp () {... URL url = null; prøv {url = ny fil ("src / main / groovy / com / baeldung /"). toURI (). toURL (); } fange (MalformedURLException e) {LOG.error ("Undtagelse under oprettelse af url", e); } motor = ny GroovyScriptEngine (ny URL [] {url}, this.getClass (). getClassLoader ()); engineFromFactory = ny GroovyScriptEngineFactory (). getScriptEngine (); }

Denne gang er vi nødt til at konfigurere kilderødder, og vi henviser til scriptet med bare dets navn, som er lidt renere.

Kigger ind i loadScriptByName metode, kan vi se med det samme checken isSourceNewer hvor motoren kontrollerer, om kilden i øjeblikket i cache stadig er gyldig.

Hver gang vores fil ændres, GroovyScriptEngine vil automatisk genindlæse den pågældende fil og alle klasser afhængigt af den.

Selvom dette er en praktisk og kraftfuld funktion, kan det medføre en meget farlig bivirkning: genindlæsning mange gange resulterer et stort antal filer i CPU-omkostninger uden advarsel.

Hvis det sker, er vi muligvis nødt til at implementere vores egen cachemekanisme for at håndtere dette problem.

6.4. GroovyScriptEngineFactory (JSR-223)

JSR-223 giver en standard API til at kalde scripting-rammer siden Java 6.

Implementeringen ser ens ud, selvom vi går tilbage til indlæsning via fulde filstier:

privat endelig ScriptEngine engineFromFactory; privat ugyldigt addWithEngineFactory (int x, int y) kaster IllegalAccessException, InstantiationException, javax.script.ScriptException, FileNotFoundException {Class calcClas = (Class) engineFromFactory.eval (new FileReader (new File ("src / main / groovy / com / ba) "," CalcMath.groovy "))); GroovyObject calc = (GroovyObject) calcClas.newInstance (); Objektresultat = calc.invokeMethod ("calcSum", nyt objekt [] {x, y}); LOG.info ("Resultatet af CalcMath.calcSum () -metoden er {}", resultat); } offentlig MyJointCompilationApp () {// ... engineFromFactory = ny GroovyScriptEngineFactory (). getScriptEngine (); }

Det er dejligt, hvis vi integrerer vores app med flere script-sprog, men dets funktionssæt er mere begrænset. For eksempel, det understøtter ikke klasselæsning. Som sådan, hvis vi kun integrerer med Groovy, kan det være bedre at holde fast ved tidligere tilgange.

7. Fallgruber ved dynamisk kompilering

Ved hjælp af en af ​​ovenstående metoder kunne vi oprette en applikation, der læser scripts eller klasser fra en bestemt mappe uden for vores jar-fil.

Dette ville give os fleksibilitet til at tilføje nye funktioner, mens systemet kører (medmindre vi har brug for ny kode i Java-delen), hvorved der opnås en form for kontinuerlig levering af udvikling.

Men pas på dette dobbeltkantede sværd: vi er nu nødt til at beskytte os meget omhyggeligt mod fejl, der kan opstå både ved kompileringstid og runtime, de facto at sikre, at vores kode mislykkes sikkert.

8. Fallgruber ved at køre Groovy i et Java-projekt

8.1. Ydeevne

Vi ved alle sammen, at når et system skal være meget performant, er der nogle gyldne regler at følge.

To, der kan veje mere på vores projekt, er:

  • undgå refleksion
  • minimer antallet af bytecode-instruktioner

Især refleksion er en kostbar handling på grund af processen med at kontrollere klassen, felterne, metoderne, metodeparametrene osv.

Hvis vi analyserer metoden opkald fra Java til Groovy, for eksempel når vi kører eksemplet addWithCompiledClasses, stakken af ​​operation mellem .calcSum og den første linje i den egentlige Groovy-metode ligner:

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

Hvilket er i overensstemmelse med Java. Det samme sker, når vi kaster det objekt, der er returneret af læsseren, og kalder dets metode.

Dette er dog hvad påkaldMethode opkald gør:

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

I dette tilfælde kan vi forstå, hvad der virkelig ligger bag Groovys magt: the MetaClass.

EN MetaClass definerer adfærd for en given Groovy- eller Java-klasse, så Groovy ser på det, når der er en dynamisk operation at udføre for at finde målmetoden eller feltet. Når den er fundet, udfører standardrefleksionsstrømmen den.

To gyldne regler brudt med en påkaldsmetode!

Hvis vi har brug for at arbejde med hundredvis af dynamiske Groovy-filer, hvordan vi kalder vores metoder vil så gøre en enorm præstationsforskel i vores system.

8.2. Metode eller ejendom ikke fundet

Som tidligere nævnt, hvis vi vil implementere nye versioner af Groovy-filer i en cd-livscyklus skal vi behandle dem som om de var en API adskilt fra vores kernesystem.

Dette betyder at sætte på plads flere fejlsikre kontroller og kodedesignbegrænsninger så vores nyligt tilsluttede udvikler sprænger ikke produktionssystemet med et forkert skub.

Eksempler på hver er: at have en CI-rørledning og bruge afskrivningsmetode i stedet for sletning.

Hvad sker der, hvis vi ikke gør det? Vi får forfærdelige undtagelser på grund af manglende metoder og forkert argumentoptælling og -type.

Og hvis vi tror, ​​at kompilering ville redde os, lad os se på metoden calcSum2 () af vores Groovy-scripts:

// denne metode mislykkes i runtime def calcSum2 (x, y) {// FARE! Variablen "log" kan være udefineret log.info "Executing $ x + $ y" // FARE! Denne metode findes ikke! calcSum3 () // FARE! Den loggede variabel "z" er udefineret! log.info ("Logning af en udefineret variabel: $ z")}

Ved at gennemse hele filen ser vi straks to problemer: metoden calcSum3 () og variablen z er ikke defineret nogen steder.

Alligevel er scriptet kompileret med succes uden engang en enkelt advarsel, både statisk i Maven og dynamisk i GroovyClassLoader.

Det mislykkes kun, når vi prøver at påberåbe det.

Mavens statiske kompilering viser kun en fejl, hvis vores Java-kode refererer direkte til calcSum3 ()efter støbning af GroovyObject som vi gør i addWithCompiledClasses () metode, men det er stadig ineffektivt, hvis vi bruger refleksion i stedet.

9. Konklusion

I denne artikel undersøgte vi, hvordan vi kan integrere Groovy i vores Java-applikation, se på forskellige integrationsmetoder og nogle af de problemer, vi kan støde på med blandede sprog.

Som normalt kan kildekoden, der er brugt i eksemplerne, findes på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found