Integrácia Groovy do aplikácií Java

1. Úvod

V tomto tutoriáli preskúmame najnovšie techniky integrácie Groovy do Java aplikácie.

2. Niekoľko slov o Groovy

Programovací jazyk Groovy je výkonný, voliteľný a dynamický jazyk. Podporuje ho Apache Software Foundation a komunita Groovy s príspevkami od viac ako 200 vývojárov.

Môže sa použiť na zostavenie celej aplikácie, na vytvorenie modulu alebo ďalšej knižnice interagujúcej s našim kódom Java alebo na spustenie skriptov vyhodnotených a zostavených za behu.

Ďalšie informácie nájdete v časti Úvod do jazyka Groovy Language alebo v oficiálnej dokumentácii.

3. Závislosti Maven

V čase písania tohto článku je najnovšie stabilné vydanie 2.5.7, zatiaľ čo Groovy 2.6 a 3.0 (obidve začali na jeseň 17) sú stále v alfa fáze.

Podobne ako Spring Boot, musíme len zahrnúť groovy-všetko pom pridať všetky závislosti bez obáv o ich verzie budeme možno potrebovať:

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

4. Spoločná kompilácia

Predtým, ako sa pustíme do podrobností o tom, ako nakonfigurovať Maven, musíme pochopiť, s čím máme do činenia.

Náš kód bude obsahovať súbory Java aj Groovy. Groovy nebude mať vôbec problém nájsť triedy Java, ale čo ak chceme, aby Java našla triedy a metódy Groovy?

Prichádza spoločná kompilácia na záchranu!

Spoločná kompilácia je proces určený na kompiláciu jazykov Java a Groovy súbory v rovnakom projekte, v jednom príkaze Maven.

Vďaka spoločnej kompilácii kompilátor Groovy:

  • analyzovať zdrojové súbory
  • v závislosti od implementácie vytvorte pahýly, ktoré sú kompatibilné s kompilátorom Java
  • vyvolať kompilátor Java na kompiláciu stubov spolu so zdrojmi Java - týmto spôsobom môžu triedy Java nájsť závislosti Groovy
  • kompilujte zdroje Groovy - teraz naše zdroje Groovy môžu nájsť svoje závislosti Java

V závislosti na implementácii doplnku sa od nás môže vyžadovať, aby sme súbory rozdelili do konkrétnych priečinkov alebo aby sme kompilátoru oznámili, kde ich nájde.

Bez spoločnej kompilácie by sa zdrojové súbory Java kompilovali, akoby to boli zdroje Groovy. Niekedy to môže fungovať, pretože väčšina syntaxe Java 1.7 je kompatibilná s programom Groovy, ale sémantika by bola iná.

5. Pluginy kompilátora Maven

Existuje niekoľko doplnkov kompilátora, ktoré podporujú spoločnú kompiláciu, každý so svojimi silnými a slabými stránkami.

Dva najbežnejšie používané s Mavenom sú Groovy-Eclipse Maven a GMaven +.

5.1. Doplnok Groovy-Eclipse Maven

Plugin Groovy-Eclipse Maven zjednodušuje spoločnú kompiláciu tým, že sa vyhýba generovaniu pahýľov, stále povinný krok pre ostatných prekladateľov, ako je GMaven+, ale predstavuje niekoľko konfiguračných zvláštností.

Aby sme umožnili načítanie najnovších artefaktov kompilátora, musíme pridať úložisko Maven Bintray:

  bintray Groovy Bintray //dl.bintray.com/groovy/maven nikdy nepravdivé 

Potom v sekcii doplnkov povieme kompilátoru Maven, ktorú verziu kompilátora Groovy musí použiť.

V skutočnosti sa plugin, ktorý použijeme - plugin kompilátora Maven - v skutočnosti kompiluje, ale namiesto toho deleguje úlohu na groovy-eclipse-batch artefakt:

 maven-compiler-plugin 3.8.0 kompilátor groovy-eclipse $ {java.version} $ {java.version} org.codehaus.groovy kompilátor groovy-eclipse 3.3.0-01 org.codehaus.groovy groovy-eclipse-batch $ {groovy.version} -01 

The groovy-všetko závislá verzia by sa mala zhodovať s verziou kompilátora.

Nakoniec musíme nakonfigurovať naše automatické zisťovanie zdrojov: predvolene by kompilátor hľadal priečinky ako napr src / main / java a src / main / groovy, ale ak je náš priečinok Java prázdny, kompilátor nebude hľadať naše groovy zdroje.

Rovnaký mechanizmus platí aj pre naše testy.

Aby sme vynútili objavenie súboru, mohli by sme doň pridať akýkoľvek súbor src / main / java a src / test / javaalebo jednoducho pridajte kompilátor groovy-eclipse zapojiť:

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

The Táto časť je povinná, aby doplnok mohol pridať fázu a ciele navyše, ktoré obsahujú dva zdrojové priečinky Groovy.

5.2. Doplnok GMavenPlus

Názov doplnku GMavenPlus môže mať názov podobný starému doplnku GMaven, ale namiesto vytvorenia obyčajnej opravy sa autor pokúsil zjednodušiť a odpojiť kompilátor od konkrétnej verzie Groovy.

Za týmto účelom sa plugin oddeľuje od štandardných pokynov pre doplnky kompilátora.

Kompilátor GMavenPlus pridáva podporu pre funkcie, ktoré v tom čase ešte neboli prítomné v iných kompilátoroch, napríklad invokedynamic, interaktívna konzola shellu a Android.

Na druhej strane to predstavuje určité komplikácie:

  • to upravuje adresáre zdrojov Maven aby obsahoval zdroje Java aj Groovy, ale nie pahýle Java
  • to vyžaduje, aby sme zvládli pahýly ak ich nevymažeme so správnymi cieľmi

Ak chcete nakonfigurovať náš projekt, musíme pridať doplnok gmavenplus:

 org.codehaus.gmavenplus gmavenplus-plugin 1.7.0 spustiť addSources addTestSources generateStubs kompilovať generateTestStubs compileTests removeStubs removeTestStubs org.codehaus.groovy groovy-all = 1.5.0 by tu malo fungovať -> 2.5.6 runtime pom 

Aby sme umožnili testovanie tohto pluginu, vytvorili sme druhý súbor pom s názvom gmavenplus-pom.xml vo vzorke.

5.3. Kompiluje sa s doplnkom Eclipse-Maven

Teraz, keď je všetko nakonfigurované, môžeme konečne zostavovať naše triedy.

V príklade, ktorý sme poskytli, sme v zdrojovom priečinku vytvorili jednoduchú aplikáciu Java src / main / java a niekoľko skriptov Groovy vo formáte src / main / groovy, kde môžeme vytvárať Groovy triedy a skripty.

Vytvorme všetko pomocou pluginu Eclipse-Maven:

$ mvn clean compile ... [INFO] --- maven-compiler-plugin: 3.8.0: compile (default-compile) @ core-groovy-2 --- [INFO] Boli zistené zmeny - kompilácia modulu! [INFO] Používanie kompilátora Groovy-Eclipse na kompiláciu súborov Java a Groovy ...

Tu to vidíme Groovy zostavuje všetko.

5.4. Kompilácia s GMavenPlus

GMavenPlus vykazuje určité rozdiely:

$ mvn -f gmavenplus-pom.xml čistá kompilácia ... [INFO] --- gmavenplus-plugin: 1.7.0: generateStubs (predvolené) @ core-groovy-2 --- [INFO] Pomocou programu Groovy 2.5.7 až vykonať generateStubs. [INFO] Generované 2 pahýly. [INFO] ... [INFO] --- plugin maven-compiler: 3.8.1: kompilácia (predvolená kompilácia) @ core-groovy-2 --- [INFO] Zistené zmeny - prekompilovanie modulu! [INFO] Kompilácia 3 zdrojových súborov do XXX \ Baeldung \ TutorialsRepo \ core-groovy-2 \ target \ classes [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: kompilácia (predvolené) @ core- groovy-2 --- [INFO] Na vykonanie kompilácie sa používa program Groovy 2.5.7. [INFO] Zostavil 2 súbory. [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: removeStubs (predvolené) @ core-groovy-2 --- [INFO] ...

Hneď si všimneme, že GMavenPlus prechádza ďalšími krokmi:

  1. Generujú sa pahýly, jeden pre každý groovy súbor
  2. Kompilácia súborov Java - pahýly a kód Java rovnako
  3. Kompilácia súborov Groovy

Generovaním útržkov dedí GMavenPlus slabinu, ktorá vývojárom v posledných rokoch pri práci so spoločnou kompiláciou spôsobovala veľa bolesti hlavy.

V ideálnom scenári by všetko fungovalo dobre, ale zavedením ďalších krokov máme aj viac bodov zlyhania: napríklad zostavenie môže zlyhať skôr, ako bude možné vyčistiť pahýly.

Ak sa to stane, môžu staré pahýly okolo zmiasť naše IDE, ktoré by potom ukázalo chyby kompilácie, keď vieme, že všetko by malo byť správne.

Iba čistá stavba by sa potom vyhla bolestivému a dlhému lovu na čarodejnice.

5.5. Závislosti balenia v súbore Jar

To spustite program ako jar z príkazového riadku, pridali sme maven-assembly-plugin, ktorá bude obsahovať všetky závislosti Groovy v „tučnej nádobe“ pomenovanej postfixom definovaným vo vlastnosti deskriptorRef:

 org.apache.maven.plugins maven-assembly-plugin 3.1.0 jar-with-dependencies com.baeldung.MyJointCompilationApp make-assembly balíček single 

Po dokončení kompilácie môžeme náš kód spustiť pomocou tohto príkazu:

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

6. Načítanie Groovy kódu za behu

Kompilácia Maven umožňuje zahrnúť súbory Groovy do nášho projektu a odkazovať na ich triedy a metódy z Javy.

Aj keď to nestačí, ak chceme zmeniť logiku za behu: kompilácia beží mimo behovú fázu, takže aby sme mohli vidieť naše zmeny, musíme stále reštartovať našu aplikáciu.

Aby sme mohli využiť dynamickú silu (a riziká) Groovy, musíme preskúmať dostupné techniky na načítanie našich súborov, keď je už naša aplikácia spustená.

6.1. GroovyClassLoader

Aby sme to dosiahli, potrebujeme GroovyClassLoader, ktoré môžu analyzovať zdrojový kód v textovom alebo súborovom formáte a generovať výsledné objekty triedy.

Ak je zdrojom súbor, výsledok kompilácie sa tiež uloží do medzipamäte, aby sme sa vyhli režijným nákladom, keď požiadame zavádzač o viac inštancií tej istej triedy.

Skript pochádzajúci priamo z a String objekt sa namiesto toho nebude ukladať do pamäte cache, teda volanie toho istého skriptu viackrát môže stále spôsobiť únik pamäte.

GroovyClassLoader je základom, na ktorom sú postavené ďalšie integračné systémy.

Implementácia je pomerne jednoduchá:

súkromný finálny nakladač GroovyClassLoader; private Double addWithGroovyClassLoader (int x, int y) hodí 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", nový objekt [] {x, y}); } public MyJointCompilationApp () {loader = nový GroovyClassLoader (this.getClass (). getClassLoader ()); // ...} 

6.2. GroovyShell

Zavádzač skriptov Shell analyzovať () metóda prijíma zdroje v textovom alebo súborovom formáte a generuje inštanciu súboru Scenár trieda.

Táto inštancia dedí run () metóda z Scenár, ktorý vykoná celý súbor zhora nadol a vráti výsledok daný posledným vykonaným riadkom.

Ak chceme, môžeme aj predĺžiť Scenár v našom kóde a prepíše predvolenú implementáciu, aby volala priamo našu internú logiku.

Implementácia volať Script.run () vyzerá takto:

private Double addWithGroovyShellRun (int x, int y) hodí 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 ()); // ...} 

Upozorňujeme, že run () neprijíma parametre, takže by sme do nášho súboru museli pridať nejaké globálne premenné, ktoré by ich inicializovali prostredníctvom Viazanie objekt.

Keď je tento objekt odovzdaný v GroovyShell inicializácia, sú premenné zdieľané so všetkými Scenár inštancie.

Ak uprednostňujeme podrobnejšiu kontrolu, môžeme použiť invokeMethod (), ktoré majú prístup k našim vlastným metódam prostredníctvom reflexie a priameho predkladania argumentov.

Pozrime sa na túto implementáciu:

súkromná konečná škrupina GroovyShell; private Double addWithGroovyShell (int x, int y) hodí IOException {Script script = shell.parse (new File ("src / main / groovy / com / baeldung /", "CalcScript.groovy")); return (Double) script.invokeMethod ("calcSum", nový objekt [] {x, y}); } public MyJointCompilationApp () {// ... shell = new GroovyShell (loader, new Binding ()); // ...} 

Pod prikrývkami, GroovyShell sa spolieha na GroovyClassLoader pre kompiláciu a ukladanie do výsledných tried, takže rovnaké pravidlá vysvetlené vyššie platia rovnako.

6.3. GroovyScriptEngine

The GroovyScriptEngine triedy je zvlášť pre tie aplikácie, ktoré spoliehať sa na opätovné načítanie skriptu a jeho závislostí.

Aj keď máme tieto ďalšie funkcie, implementácia má iba niekoľko malých rozdielov:

súkromný finálny engine GroovyScriptEngine; private void addWithGroovyScriptEngine (int x, int y) hodí IllegalAccessException, InstantiationException, ResourceException, ScriptException {Class calcClass = engine.loadScriptByName ("CalcMath.groovy"); GroovyObject calc = calcClass.newInstance (); Výsledok objektu = calc.invokeMethod ("calcSum", nový objekt [] {x, y}); LOG.info ("Výsledok metódy CalcMath.calcSum () je {}", výsledok); } public MyJointCompilationApp () {... URL url = null; skúste {url = nový súbor ("src / main / groovy / com / baeldung /"). toURI (). toURL (); } catch (MalformedURLException e) {LOG.error ("Výnimka pri vytváraní adresy URL", e); } engine = nový GroovyScriptEngine (nová URL [] {url}, this.getClass (). getClassLoader ()); engineFromFactory = nový GroovyScriptEngineFactory (). getScriptEngine (); }

Tentokrát musíme nakonfigurovať zdrojové korene a skript označujeme iba jeho menom, ktoré je trochu čistejšie.

Pri pohľade dovnútra loadScriptByName metódu, môžeme hneď vidieť šek isSourceNewer kde motor kontroluje, či je zdroj momentálne v pamäti cache stále platný.

Zakaždým, keď sa zmení náš súbor, GroovyScriptEngine automaticky načíta konkrétny súbor a všetky triedy, ktoré od neho závisia.

Aj keď je to šikovná a výkonná funkcia, mohla by spôsobiť veľmi nebezpečný vedľajší účinok: mnohonásobné opätovné načítanie veľkého množstva súborov bude mať za následok réžiu procesora bez varovania.

Ak sa tak stane, možno budeme musieť implementovať náš vlastný mechanizmus ukladania do pamäte cache, aby sme tento problém zvládli.

6.4. GroovyScriptEngineFactory (JSR-223)

JSR-223 poskytuje a štandardné API pre volanie skriptovacích rámcov od Javy 6.

Implementácia vyzerá podobne, aj keď sa vrátime k načítaniu pomocou úplných ciest k súborom:

súkromný finálny engine ScriptEngineFromFactory; private void addWithEngineFactory (int x, int y) hodí 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 (); Výsledok objektu = calc.invokeMethod ("calcSum", nový objekt [] {x, y}); LOG.info ("Výsledok metódy CalcMath.calcSum () je {}", výsledok); } public MyJointCompilationApp () {// ... engineFromFactory = nový GroovyScriptEngineFactory (). getScriptEngine (); }

Je skvelé, ak integrujeme našu aplikáciu do niekoľkých skriptovacích jazykov, ale jeho sada funkcií je obmedzenejšia. Napríklad, nepodporuje opätovné načítanie triedy. Preto, ak sa integrujeme iba do Groovy, môže byť lepšie držať sa skorších prístupov.

7. Úskalia dynamickej kompilácie

Pomocou ktorejkoľvek z vyššie uvedených metód by sme mohli vytvoriť aplikáciu, ktorá číta skripty alebo triedy z konkrétneho priečinka mimo nášho súboru jar.

Toto by nám poskytlo flexibilita pri pridávaní nových funkcií za behu systému (pokiaľ nevyžadujeme nový kód v časti Java), čím dosiahneme akýsi druh vývoja Continuous Delivery.

Ale pozor na tento dvojsečný meč: pred ktorým sa teraz musíme chrániť veľmi opatrne zlyhania, ktoré by sa mohli stať v čase kompilácie aj za behu, de facto zaistením bezpečného zlyhania nášho kódu.

8. Úskalia spustenia Groovy v projekte Java

8.1. Výkon

Všetci vieme, že keď musí byť systém veľmi výkonný, treba dodržiavať niekoľko zlatých pravidiel.

Dva, ktoré môžu na našom projekte zavážiť viac, sú:

  • vyhnúť sa reflexii
  • minimalizovať počet pokynov pre bytecode

Najmä reflexia je nákladná operácia v dôsledku procesu kontroly triedy, polí, metód, parametrov metódy atď.

Ak analyzujeme volania metódy z Javy na Groovy, napríklad pri spustení príkladu addWithCompiledClasses, zásobník operácií medzi .calcSum a prvý riadok skutočnej metódy Groovy vyzerá takto:

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

Čo je v súlade s Javou. To isté sa stane, keď vrhneme objekt vrátený nakladačom a zavoláme jeho metódu.

To je však to, čo invokeMethod hovor robí:

calcSum: 4, CalcScript (com.baeldung) invoke0: -1, NativeMethodAccessorImpl (sun.reflect) invoke: 62, NativeMethodAccessorImpl (sun.reflect) invoke: 43, DelegatingMethodAccessorImpl (sun.reflect) invoke: 498, metóda (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) , MetaClassImpl (groovy.lang) invokeMethod: 44, GroovyObjectSupport (groovy.lang) invokeMethod: 77, Script (groovy.lang) addWithGroovyShell: 52, MyJointCompilationApp (com.baeldung) addWithDynamicCompiledClasses: 99, MyJa , MyJointCompilationApp (com.baeldung)

V tomto prípade môžeme oceniť, čo skutočne stojí za silou Groovyho: MetaClass.

A MetaClass definuje správanie ktorejkoľvek danej triedy Groovy alebo Java, takže Groovy sa na ňu pozrie vždy, keď je potrebné vykonať dynamickú operáciu za účelom nájdenia cieľovej metódy alebo poľa. Len čo sa nájde, vykoná ho štandardný tok odrazu.

Dve zlaté pravidlá porušené jednou metódou vyvolania!

Ak potrebujeme pracovať so stovkami dynamických súborov Groovy, ako nazývame naše metódy, potom urobí obrovský rozdiel vo výkone v našom systéme.

8.2. Metóda alebo nehnuteľnosť sa nenašli

Ako už bolo spomenuté skôr, ak chceme nasadiť nové verzie súborov Groovy v životnom cykle CD, musíme zaobchádzať s nimi, akoby boli API oddelene od nášho základného systému.

To znamená zavedenie viacnásobné kontroly proti zlyhaniu a obmedzenia návrhu kódu takže náš novo pridaný vývojár nevyfúkne produkčný systém nesprávnym stlačením.

Príklady každého z nich sú: zavedenie kanálu CI a použitie metódy deprecation namiesto odstránenia.

Čo sa stane, ak to neurobíme? Dostávame hrozné výnimky kvôli chýbajúcim metódam a nesprávnym počtom a typom argumentov.

A ak si myslíme, že by nás kompilácia zachránila, pozrime sa na metódu calcSum2 () našich skriptov Groovy:

// táto metóda zlyhá za behu def calcSum2 (x, y) {// NEBEZPEČENSTVO! Premenná „log“ môže byť nedefinovaná log.info „Vykonávanie $ x + $ y“ // NEBEZPEČENSTVO! Táto metóda neexistuje! calcSum3 () // NEBEZPEČENSTVO! Prihlásená premenná „z“ nie je definovaná! log.info ("Prihlásenie nedefinovanej premennej: $ z")}

Pri prehliadaní celého súboru okamžite vidíme dva problémy: metódu calcSum3 () a premenná z nie sú nikde definované.

Aj napriek tomu je skript zostavený úspešne, bez jediného varovania, a to ako staticky v Mavene, tak aj dynamicky v GroovyClassLoader.

Zlyhá to, iba keď sa to pokúsime vyvolať.

Statická kompilácia spoločnosti Maven zobrazí chybu, iba ak sa na ňu priamo odvoláva náš kód Java calcSum3 (), po nahodení GroovyObject ako to robíme v addWithCompiledClasses () metóda, ale je stále neúčinné, ak namiesto nej použijeme reflexiu.

9. Záver

V tomto článku sme skúmali, ako môžeme integrovať Groovy do našej aplikácie Java, pričom sme sa zamerali na rôzne metódy integrácie a niektoré problémy, s ktorými sa môžeme stretnúť pri zmiešaných jazykoch.

Ako obvykle, zdrojový kód použitý v príkladoch nájdete na GitHub.