Integrering av Groovy i Java-applikasjoner

1. Introduksjon

I denne opplæringen vil vi utforske de nyeste teknikkene for å integrere Groovy i en Java-applikasjon.

2. Noen få ord om groovy

Groovy-programmeringsspråket er et kraftig, valgfritt og dynamisk språk. Den støttes av Apache Software Foundation og Groovy-samfunnet, med bidrag fra mer enn 200 utviklere.

Den kan brukes til å bygge en hel applikasjon, til å lage en modul eller et ekstra bibliotek som samhandler med Java-koden vår, eller til å kjøre skript evaluert og kompilert i farta.

For mer informasjon, les Introduksjon til Groovy Language eller gå til den offisielle dokumentasjonen.

3. Maven-avhengigheter

I skrivende stund er den siste stabile utgivelsen 2.5.7, mens Groovy 2.6 og 3.0 (begge startet høsten '17) fortsatt er i alfafasen.

I likhet med Spring Boot, vi trenger bare å inkludere groovy-all pom for å legge til alle avhengighetene vi kan trenge uten å bekymre oss for versjonene deres:

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

4. Felles samling

Før vi går inn i detaljene om hvordan du konfigurerer Maven, må vi forstå hva vi har å gjøre med.

Koden vår inneholder både Java- og Groovy-filer. Groovy vil ikke ha noe problem i det hele tatt å finne Java-klassene, men hva om vi vil at Java skal finne Groovy-klasser og metoder?

Her kommer felles samling til unnsetning!

Felles kompilering er en prosess designet for å kompilere både Java og Groovy filer i samme prosjekt, i en enkelt Maven-kommando.

Med felles kompilering vil Groovy-kompilatoren:

  • analyser kildefilene
  • avhengig av implementeringen, opprett stubber som er kompatible med Java-kompilatoren
  • påkalle Java-kompilatoren for å kompilere stubbene sammen med Java-kilder - på denne måten kan Java-klasser finne Groovy-avhengigheter
  • kompilere Groovy-kildene - nå kan Groovy-kildene finne Java-avhengighetene

Avhengig av programtillegget som implementerer det, kan det hende vi blir bedt om å skille filene i bestemte mapper eller fortelle kompilatoren hvor du finner dem.

Uten felles kompilering ville Java-kildefilene bli samlet som om de var Groovy-kilder. Noen ganger kan dette fungere siden det meste av Java 1.7-syntaksen er kompatibel med Groovy, men semantikken vil være annerledes.

5. Maven Compiler Plugins

Det er noen få kompilator-plugins tilgjengelig som støtter felles kompilering, hver med sine styrker og svakheter.

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

5.1. Groovy-Eclipse Maven-plugin

Groovy-Eclipse Maven-pluginet forenkler samlingen ved å unngå generering av stubber, fortsatt et obligatorisk trinn for andre kompilatorer som GMaven+, men det presenterer noen konfigurasjonsegenskaper.

For å muliggjøre henting av de nyeste kompilatorgjenstandene, må vi legge til Maven Bintray-arkivet:

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

I plugin-delen, vi forteller Maven-kompilatoren hvilken Groovy-kompilatorversjon den må bruke.

Faktisk kompilerer vi ikke pluggen vi bruker - Maven compiler plugin, men delegerer i stedet jobben til groovy-eclipse-batch gjenstand:

 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 avhengighetsversjonen skal samsvare med kompilatorversjonen.

Til slutt må vi konfigurere kildeautodiscovery: som standard vil kompilatoren se i mapper som src / main / java og src / main / groovy, men Hvis Java-mappen vår er tom, vil ikke kompilatoren lete etter de groove kildene våre.

Den samme mekanismen gjelder for våre tester.

For å tvinge filoppdagelsen, kan vi legge til hvilken som helst fil i src / main / java og src / test / java, eller bare legg til groovy-eclipse-compiler plugg inn:

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

De seksjonen er obligatorisk for å la pluginet legge til den ekstra byggefasen og målene, som inneholder de to Groovy-kildemappene.

5.2. GMavenPlus-programtillegget

GMavenPlus-pluginet kan ha et navn som ligner på det gamle GMaven-pluginet, men i stedet for å lage en ren oppdatering, gjorde forfatteren et forsøk på å forenkle og koble kompilatoren fra en bestemt Groovy-versjon.

For å gjøre dette skiller pluginet seg fra standardretningslinjene for kompilator-plugins.

GMavenPlus-kompilatoren legger til støtte for funksjoner som fremdeles ikke var til stede i andre kompilatorer på den tiden, for eksempel invokedynamic, den interaktive skallkonsollen og Android.

På den andre siden gir det noen komplikasjoner:

  • den endrer Mavens kildekataloger å inneholde både Java- og Groovy-kildene, men ikke Java-stubber
  • den krever at vi administrerer stubber hvis vi ikke sletter dem med de riktige målene

For å konfigurere prosjektet vårt, må vi legge til gmavenplus-plugin:

 org.codehaus.gmavenplus gmavenplus-plugin 1.7.0 utfør addSources addTestSources generereStubber kompilere generereTestStubs kompilereTester fjerneStubber fjerneTestStubs org.codehaus.groovy groovy-all = 1.5.0 skal fungere her -> 2.5.6 runtime pom 

For å tillate testing av dette pluginet, opprettet vi en andre pom-fil kalt gmavenplus-pom.xml i prøven.

5.3. Kompilering med Eclipse-Maven Plugin

Nå som alt er konfigurert, kan vi endelig bygge klassene våre.

I eksemplet vi ga, opprettet vi et enkelt Java-program i kildemappen src / main / java og noen Groovy-skript i src / main / groovy, der vi kan lage Groovy-klasser og skript.

La oss bygge alt med Eclipse-Maven-plugin:

$ mvn clean compile ... [INFO] --- maven-compiler-plugin: 3.8.0: compile (default-compile) @ core-groovy-2 --- [INFO] Endringer oppdaget - kompilering av modulen! [INFO] Bruker Groovy-Eclipse-kompilatoren til å kompilere både Java- og Groovy-filer ...

Her ser vi det Groovy kompilerer alt.

5.4. Kompilering med GMavenPlus

GMavenPlus viser noen forskjeller:

$ mvn -f gmavenplus-pom.xml clean compile ... [INFO] --- gmavenplus-plugin: 1.7.0: generereStubber (standard) @ core-groovy-2 --- [INFO] Bruke Groovy 2.5.7 til utfør generereStubber. [INFO] Genererte to stubber. [INFO] ... [INFO] --- maven-compiler-plugin: 3.8.1: kompilere (standardkompilering) @ core-groovy-2 --- [INFO] Oppdagede endringer - kompilering av modulen! [INFO] Kompilering av 3 kildefiler til XXX \ Baeldung \ TutorialsRepo \ core-groovy-2 \ target \ classes [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: kompilere (standard) @ core- groovy-2 --- [INFO] Bruke Groovy 2.5.7 til å utføre kompilering. [INFO] Kompilert to filer. [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: removeStubs (standard) @ core-groovy-2 --- [INFO] ...

Vi merker med en gang at GMavenPlus går gjennom de ekstra trinnene for:

  1. Genererer stubber, en for hver groovy fil
  2. Kompilering av Java-filer - stubber og Java-kode likt
  3. Kompilering av Groovy-filene

Ved å generere stubber arver GMavenPlus en svakhet som forårsaket mange hodepine for utviklere de siste årene når de jobber med felles kompilering.

I det ideelle scenariet vil alt fungere bra, men å introdusere flere trinn har vi også flere feilpunkter: for eksempel bygningen kan mislykkes før du kan rydde opp stubbene.

Hvis dette skjer, kan gamle stubber som er igjen, forvirre IDEen vår, som da viser kompileringsfeil der vi vet at alt skal være riktig.

Bare en ren bygning ville da unngå en smertefull og lang heksejakt.

5.5. Emballasjeavhengighet i Jar-filen

Til kjør programmet som en krukke fra kommandolinjen, la vi til maven-assembly-plugin, som vil omfatte alle Groovy-avhengighetene i en "fettkrukke" med postfixet som er definert i eiendommen deskriptorRef:

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

Når kompileringen er fullført, kan vi kjøre koden vår med denne kommandoen:

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

6. Laster inn Groovy Code on the Fly

Maven-samlingen lar oss inkludere Groovy-filer i prosjektet vårt og referere til klasser og metoder fra Java.

Selv om dette ikke er nok hvis vi vil endre logikken ved kjøretid: samlingen løper utenfor kjøretidsfasen, så Vi må fortsatt starte søknaden på nytt for å se endringene.

For å dra nytte av den dynamiske kraften (og risikoen) til Groovy, må vi utforske teknikkene som er tilgjengelige for å laste inn filene når applikasjonen allerede kjører.

6.1. GroovyClassLoader

For å oppnå dette trenger vi GroovyClassLoader, som kan analysere kildekoden i tekst eller filformat og generere de resulterende klasseobjektene.

Når kilden er en fil, lagres også kompileringsresultatet, for å unngå overhead når vi spør lasteren om flere forekomster av samme klasse.

Skript kommer direkte fra en String objektet, i stedet, blir ikke bufretDerfor kan det fortsatt føre til minnelekkasjer å ringe det samme skriptet flere ganger.

GroovyClassLoader er grunnlaget for andre integreringssystemer.

Implementeringen er relativt enkel:

privat endelig GroovyClassLoader loader; private Double addWithGroovyClassLoader (int x, int y) kaster 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}); } offentlig MyJointCompilationApp () {loader = ny GroovyClassLoader (this.getClass (). getClassLoader ()); // ...} 

6.2. GroovyShell

Shell Script Loader analysere () metoden godtar kilder i tekst eller filformat og genererer en forekomst av Manus klasse.

Denne forekomsten arver løpe() metode fra Manus, som kjører hele filen fra topp til bunn og returnerer resultatet gitt av den siste linjen som ble utført.

Hvis vi vil, kan vi også utvide Manus i koden vår, og overstyre standardimplementeringen for å kalle direkte vår interne logikk.

Implementeringen å ringe Script.run () ser slik ut:

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

Vær oppmerksom på at løpe() godtar ikke parametere, så vi må legge til noen globale variabler i filen vår ved å initialisere dem gjennom Bindende gjenstand.

Når dette objektet sendes i GroovyShell initialisering deles variablene med alle Manus tilfeller.

Hvis vi foretrekker en mer detaljert kontroll, kan vi bruke den påkalleMetode (), som kan få tilgang til våre egne metoder gjennom refleksjon og sende argumenter direkte.

La oss se på denne implementeringen:

privat finale GroovyShell skall; privat Double 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", nytt objekt [] {x, y}); } public MyJointCompilationApp () {// ... shell = new GroovyShell (loader, new Binding ()); // ...} 

Under dyna, GroovyShell stoler på GroovyClassLoader for å kompilere og cache de resulterende klassene, så de samme reglene som er forklart tidligere gjelder på samme måte.

6.3. GroovyScriptEngine

De GroovyScriptEngine klasse er spesielt for de applikasjoner som stole på omlasting av et skript og dets avhengighet.

Selv om vi har disse tilleggsfunksjonene, har implementeringen bare noen få små forskjeller:

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

Denne gangen må vi konfigurere kilderøtter, og vi refererer til skriptet med bare navnet, som er litt renere.

Ser inn i loadScriptByName metoden, kan vi se med en gang sjekken isSourceNewer der motoren sjekker om kilden som er i hurtigbufferen fortsatt er gyldig.

Hver gang filen vår endres, GroovyScriptEngine vil automatisk laste den aktuelle filen og alle klassene på nytt, avhengig av den.

Selv om dette er en praktisk og kraftig funksjon, kan det føre til en veldig farlig bivirkning: å laste inn mange ganger vil et stort antall filer føre til CPU-overhead uten advarsel.

Hvis det skjer, kan det hende vi må implementere vår egen cachemekanisme for å håndtere dette problemet.

6.4. GroovyScriptEngineFactory (JSR-223)

JSR-223 gir en standard API for å ringe skriptrammer siden Java 6.

Implementeringen ser lik ut, selv om vi går tilbake til lasting via full filstier:

privat slutt ScriptEngine engineFromFactory; privat tomrom 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", nytt objekt [] {x, y}); LOG.info ("Resultatet av CalcMath.calcSum () -metoden er {}", resultat); } offentlig MyJointCompilationApp () {// ... engineFromFactory = ny GroovyScriptEngineFactory (). getScriptEngine (); }

Det er flott hvis vi integrerer appen vår med flere skriptspråk, men funksjonssettet er mer begrenset. For eksempel, det støtter ikke klasselading. Som sådan, hvis vi bare integrerer med Groovy, kan det være bedre å holde oss til tidligere tilnærminger.

7. Fallgruver av dynamisk kompilering

Ved å bruke en av metodene ovenfor kan vi lage et program som leser skript eller klasser fra en bestemt mappe utenfor jar-filen vår.

Dette vil gi oss fleksibilitet for å legge til nye funksjoner mens systemet kjører (med mindre vi trenger ny kode i Java-delen), og oppnår dermed en slags kontinuerlig leveringsutvikling.

Men pass på dette tokantede sverdet: Vi må nå beskytte oss veldig nøye mot feil som kan oppstå både ved kompileringstid og kjøretid, de facto å sikre at koden vår mislykkes trygt.

8. Fallgruver ved å kjøre Groovy i et Java-prosjekt

8.1. Opptreden

Vi vet alle at når et system må være veldig performant, er det noen gyldne regler å følge.

To som kan veie mer på prosjektet vårt er:

  • unngå refleksjon
  • minimer antall bytekodeinstruksjoner

Spesielt refleksjon er en kostbar operasjon på grunn av prosessen med å sjekke klassen, feltene, metodene, metodeparametrene og så videre.

Hvis vi analyserer metoden samtaler fra Java til Groovy, for eksempel når vi kjører eksemplet addWithCompiledClasses, stabelen av drift mellom .calcSum og den første linjen i selve Groovy-metoden ser ut som:

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

Noe som stemmer overens med Java. Det samme skjer når vi kaster gjenstanden som lasteren returnerer og kaller metoden.

Dette er imidlertid hva påkalleMetode samtalen gjør:

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

I dette tilfellet kan vi sette pris på hva som egentlig ligger bak Groovys makt: Metaklass.

EN Metaklass definerer oppførselen til en hvilken som helst Groovy- eller Java-klasse, så Groovy ser på den når det er en dynamisk operasjon å utføre for å finne målmetoden eller feltet. Når den er funnet, utfører standardrefleksjonsflyten den.

To gyldne regler brutt med en påkalle metoden!

Hvis vi trenger å jobbe med hundrevis av dynamiske Groovy-filer, hvordan vi kaller metodene våre vil da gjøre en enorm ytelsesforskjell i systemet vårt.

8.2. Metode eller eiendom ikke funnet

Som nevnt tidligere, hvis vi vil distribuere nye versjoner av Groovy-filer i en CD-livssyklus, må vi behandle dem som om de var en API atskilt fra kjernesystemet vårt.

Dette betyr å få på plass flere feil-sikre kontroller og kode design begrensninger slik at vår nylig tilsluttte utvikler ikke sprenger produksjonssystemet med feil trykk.

Eksempler på hver er: å ha en CI-rørledning og bruke avskrivning av metoden i stedet for å bli slettet.

Hva skjer hvis vi ikke gjør det? Vi får fryktelige unntak på grunn av manglende metoder og feil antall tellinger og typer.

Og hvis vi tror at samlingen vil redde oss, la oss se på metoden calcSum2 () av våre Groovy-skript:

// denne metoden vil mislykkes i runtime def calcSum2 (x, y) {// FARE! Variabelen "logg" kan være udefinert log.info "Utfører $ x + $ y" // FARE! Denne metoden eksisterer ikke! calcSum3 () // FARE! Den loggede variabelen "z" er udefinert! log.info ("Logge en udefinert variabel: $ z")}

Ved å se gjennom hele filen ser vi umiddelbart to problemer: metoden calcSum3 () og variabelen z er ikke definert hvor som helst.

Allikevel er skriptet kompilert med suksess, uten en eneste advarsel, både statisk i Maven og dynamisk i GroovyClassLoader.

Det mislykkes bare når vi prøver å påkalle det.

Mavens statiske kompilering viser bare en feil hvis Java-koden vår refererer direkte til calcSum3 (), etter å ha kastet GroovyObject som vi gjør i addWithCompiledClasses () metode, men det er fortsatt ineffektivt hvis vi bruker refleksjon i stedet.

9. Konklusjon

I denne artikkelen undersøkte vi hvordan vi kan integrere Groovy i Java-applikasjonen, se på forskjellige integrasjonsmetoder og noen av problemene vi kan støte på med blandede språk.

Som vanlig kan kildekoden som brukes i eksemplene bli funnet på GitHub.


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