Integriranje Groovyja u Java programe

1. Uvod

U ovom uputstvu istražit ćemo najnovije tehnike integriranja Groovyja u Java aplikaciju.

2. Nekoliko riječi o Groovyju

Programski jezik Groovy moćan je, neobavezno otkucan i dinamičan jezik. Podržava ga Apache Software Foundation i zajednica Groovy, uz doprinose više od 200 programera.

Može se koristiti za izgradnju cijele aplikacije, za izradu modula ili dodatne biblioteke u interakciji s našim Java kodom ili za pokretanje skripti evaluiranih i kompajliranih u letu.

Za više informacija pročitajte Uvod u Groovy jezik ili idite na službenu dokumentaciju.

3. Ovisnosti Mavena

U vrijeme pisanja ovog članka, posljednje stabilno izdanje je 2.5.7, dok su Groovy 2.6 i 3.0 (oba započeta u jesen '17) još uvijek u alfa fazi.

Slično Spring Boot-u, samo trebamo uključiti groovy-all pom dodati sve ovisnosti možda će nam trebati, bez brige o njihovim verzijama:

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

4. Kompilacija zglobova

Prije ulaska u detalje kako konfigurirati Maven, moramo shvatiti s čime imamo posla.

Naš će kôd sadržavati datoteke Java i Groovy. Groovy uopće neće imati problema s pronalaženjem Java klasa, ali što ako želimo da Java pronađe Groovy klase i metode?

U pomoć dolazi zajednička kompilacija!

Zajednička kompilacija postupak je namijenjen kompajliranju Java i Groovyja datoteke u istom projektu, u jednoj Mavenovoj naredbi.

Zajedničkom kompilacijom, kompajler Groovy:

  • raščlaniti izvorne datoteke
  • ovisno o implementaciji, stvorite kvarove koji su kompatibilni s Java kompajlerom
  • pozvati Java kompajler za kompajliranje kvara zajedno s Java izvorima - na ovaj način Java klase mogu pronaći Groovy ovisnosti
  • sastaviti izvore Groovy - sada naši izvori Groovy mogu pronaći svoje ovisnosti o Javi

Ovisno o dodatku koji ga implementira, možda ćemo morati odvojiti datoteke u određene mape ili reći prevoditelju gdje ih može pronaći.

Bez zajedničke kompilacije, izvorne datoteke Java sastavljale bi se kao da su Groovyjevi izvori. Ponekad bi to moglo funkcionirati jer je većina sintakse Java 1.7 kompatibilna s Groovyjem, ali semantika bi bila drugačija.

5. Dodaci za kompilator Maven

Dostupno je nekoliko dodataka za kompajlere koji podržavaju zajedničku kompilaciju, svaki sa svojim snagama i slabostima.

Dvije najčešće korištene s Mavenom su Groovy-Eclipse Maven i GMaven +.

5.1. Dodatak Groovy-Eclipse Maven

Dodatak Groovy-Eclipse Maven pojednostavljuje zajedničku kompilaciju izbjegavanjem stvaranja mrlja, još uvijek obvezan korak za ostale kompajlere poput GMavena+, ali predstavlja neke konfiguracijske postavke.

Da bismo omogućili pronalazak najnovijih artefakata kompajlera, moramo dodati spremište Maven Bintray:

  bintray Groovy Bintray //dl.bintray.com/groovy/maven nikad nije lažno 

Zatim, u odjeljku dodatka, govorimo Maven kompajleru koju verziju kompajlera Groovy mora koristiti.

U stvari, dodatak koji ćemo koristiti - dodatak za kompajler Maven - zapravo se ne kompajlira, već posao delegira na 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.verzija} -01 

The groovy-all verzija ovisnosti trebala bi odgovarati verziji kompajlera.

Konačno, moramo konfigurirati izvorno automatsko otkrivanje: prema zadanim postavkama, prevodilac će pregledati mape kao što je src / main / java i src / main / groovy, ali ako je naša mapa java prazna, prevodilac neće tražiti naše groovy izvore.

Isti mehanizam vrijedi i za naše testove.

Da bismo prisilili otkrivanje datoteke, mogli bismo dodati bilo koju datoteku u src / main / java i src / test / javaili jednostavno dodajte sastavljač groovy-eclipse uključiti:

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

The odjeljak je obavezan kako bi dodatak dodao dodatnu fazu izrade i ciljeve, koji sadrže dvije izvorne mape Groovy.

5.2. Dodatak GMavenPlus

Dodatak GMavenPlus možda ima ime slično starom dodatku GMaven, ali umjesto da stvori samo zakrpu, autor se potrudio da pojednostaviti i razdvojiti kompajler od određene Groovy verzije.

Da bi to učinio, dodatak se odvaja od standardnih smjernica za dodatke kompajlera.

Prevoditelj GMavenPlus dodaje podršku za značajke koje u to vrijeme još uvijek nisu bile prisutne u drugim kompajlerima, kao što su invokedynamic, interaktivna konzola ljuske i Android.

S druge strane, to predstavlja neke komplikacije:

  • to mijenja Mavenove direktorijume izvora da sadrži i Java i Groovy izvore, ali ne i Java stub-ove
  • to zahtijeva od nas da upravljamo krhotinama ako ih ne izbrišemo s odgovarajućim ciljevima

Da bismo konfigurirali naš projekt, trebamo dodati dodatak gmavenplus:

 org.codehaus.gmavenplus gmavenplus-plugin 1.7.0 izvršiti addSources addTestSources generirati Stubs sastaviti generiratiTestStubs compileTests removeStubs removeTestStubs org.codehaus.groovy groovy-all = 1.5.0 ovdje treba raditi -> 2.5.6 runtime pom 

Da bismo omogućili testiranje ovog dodatka, stvorili smo drugu pom datoteku pod nazivom gmavenplus-pom.xml u uzorku.

5.3. Sastavljanje s dodatkom Eclipse-Maven

Sad kad je sve konfigurirano, napokon možemo graditi svoje predmete.

U primjeru koji smo pružili stvorili smo jednostavnu Java aplikaciju u izvornoj mapi src / main / java i neke Groovyjeve skripte u src / main / groovy, gdje možemo stvoriti Groovy klase i skripte.

Izgradimo sve pomoću dodatka Eclipse-Maven:

$ mvn clean compile ... [INFO] --- maven-compiler-plugin: 3.8.0: compile (default-compile) @ core-groovy-2 --- [INFO] Otkrivene promjene - ponovno sastavljanje modula! [INFO] Korištenje kompajlera Groovy-Eclipse za kompajliranje datoteka Java i Groovy ...

Ovdje to vidimo Groovy sve sastavlja.

5.4. Sastavljanje s GMavenPlusom

GMavenPlus pokazuje neke razlike:

$ mvn -f gmavenplus-pom.xml clean compile ... [INFO] --- gmavenplus-plugin: 1.7.0: generirajStubs (zadano) @ core-groovy-2 --- [INFO] Korištenje Groovy 2.5.7 za izvesti generiratiStubs. [INFO] Generirano 2 klica. [INFO] ... [INFO] --- maven-compiler-plugin: 3.8.1: compile (default-compile) @ core-groovy-2 --- [INFO] Otkrivene promjene - ponovno sastavljanje modula! [INFO] Sastavljanje 3 izvorne datoteke u XXX \ Baeldung \ TutorialsRepo \ core-groovy-2 \ target \ classes [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: compile (default) @ core- groovy-2 --- [INFO] Korištenje Groovyja 2.5.7 za izvođenje kompajliranja. [INFO] Sastavljene 2 datoteke. [INFO] ... [INFO] --- gmavenplus-plugin: 1.7.0: removeStubs (zadano) @ core-groovy-2 --- [INFO] ...

Odmah primjećujemo da GMavenPlus prolazi kroz dodatne korake:

  1. Generiranje kvara, po jedan za svaku groovy datoteku
  2. Sastavljanje Java datoteka - sličica i koda
  3. Sastavljanje datoteka Groovy

Generirajući kvarove, GMavenPlus nasljeđuje slabost koja je programerima zadavala brojne glavobolje u proteklim godinama, kada su radili sa zajedničkom kompilacijom.

U idealnom scenariju sve bi funkcioniralo sasvim u redu, ali uvođenjem više koraka imamo i više točaka neuspjeha: na primjer, izgradnja može propasti prije nego što uspije očistiti klizaljke.

Ako se to dogodi, stari ostaci koji ostanu oko nas mogu zbuniti naš IDE, što bi onda pokazalo pogreške pri kompilaciji tamo gdje znamo da bi sve trebalo biti točno.

Tada bi samo čista građa izbjegla bolan i dug lov na vještice.

5.5. Ovisnosti pakiranja u Jar datoteci

Do pokrenite program kao jar iz naredbenog retka, dodali smo maven-assembly-plugin, koji će uključivati ​​sve ovisnosti Groovyja u "masnoj teglici" imenovanoj postfixom definiranim u svojstvu descriptorRef:

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

Nakon završetka kompilacije, možemo pokrenuti naš kod pomoću ove naredbe:

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

6. Učitavanje Groovy koda na Fly

Kompilacija Maven omogućila nam je da u naš projekt uključimo Groovy datoteke i uputimo njihove klase i metode s Jave.

Iako to nije dovoljno ako želimo promijeniti logiku u vrijeme izvođenja: kompilacija se izvodi izvan runtime faze, pa još uvijek moramo ponovno pokrenuti našu aplikaciju kako bismo vidjeli naše promjene.

Da bismo iskoristili dinamičnu snagu (i rizike) Groovyja, moramo istražiti tehnike dostupne za učitavanje naših datoteka kad naša aplikacija već radi.

6.1. GroovyClassLoader

Da bismo to postigli, potreban nam je GroovyClassLoader, koji mogu raščlaniti izvorni kod u tekstu ili formatu datoteke i generirati rezultirajuće objekte klase.

Kad je izvor datoteka, rezultat kompilacije također se predmemorira, kako bismo izbjegli režijske troškove kada od loader-a tražimo više primjeraka iste klase.

Skripta dolazi izravno iz a Niz objekt, umjesto toga, neće biti u predmemoriji, stoga bi pozivanje iste skripte više puta moglo i dalje uzrokovati curenje memorije.

GroovyClassLoader je temelj na kojem se grade drugi sustavi integracije.

Implementacija je relativno jednostavna:

privatni konačni GroovyClassLoader loader; private Double addWithGroovyClassLoader (int x, int y) baca IllegalAccessException, InstantiationException, IOException {Class calcClass = loader.parseClass (new File ("src / main / groovy / com / baeldung /", "CalcMath.groov"; GroovyObject calc = (GroovyObject) calcClass.newInstance (); return (Double) calc.invokeMethod ("calcSum", novi objekt [] {x, y}); } javni MyJointCompilationApp () {loader = novi GroovyClassLoader (this.getClass (). getClassLoader ()); // ...} 

6.2. GroovyShell

Učitavač skripti školjke raščlaniti () metoda prihvaća izvore u tekstu ili formatu datoteke i generira instancu Skripta razred.

Ova instanca nasljeđuje trčanje() metoda iz Skripta, koji izvršava cijelu datoteku od vrha do dna i vraća rezultat zadnjeg izvršenog retka.

Ako želimo, možemo i produžiti Skripta u našem kodu i nadjačati zadanu implementaciju da izravno poziva našu unutarnju logiku.

Provedbu nazvati Script.run () izgleda ovako:

private Double addWithGroovyShellRun (int x, int y) baca IOException {Script script = shell.parse (nova datoteka ("src / main / groovy / com / baeldung /", "CalcScript.groovy")); return (Double) script.run (); } public MyJointCompilationApp () {// ... shell = new GroovyShell (loader, new Binding ()); // ...} 

Imajte na umu da trčanje() ne prihvaća parametre, pa bismo u našu datoteku trebali dodati neke globalne varijable koje bi ih inicijalizirale kroz Uvez objekt.

Kako se ovaj objekt prenosi u GroovyShell inicijalizacije, varijable se dijele sa svim Skripta instance.

Ako više volimo zrnatiju kontrolu, možemo koristiti invokeMethod (), koji mogu pristupiti vlastitim metodama kroz razmišljanje i izravno prosljeđivati ​​argumente.

Pogledajmo ovu implementaciju:

privatna konačna ljuska GroovyShell; private Double addWithGroovyShell (int x, int y) baca IOException {Script script = shell.parse (nova datoteka ("src / main / groovy / com / baeldung /", "CalcScript.groovy")); return (Double) script.invokeMethod ("calcSum", novi objekt [] {x, y}); } public MyJointCompilationApp () {// ... shell = new GroovyShell (loader, new Binding ()); // ...} 

Ispod pokrivača, GroovyShell oslanja se na GroovyClassLoader za sastavljanje i predmemoriranje rezultirajućih klasa, pa se na isti način primjenjuju ista prethodno objašnjena pravila.

6.3. GroovyScriptEngine

The GroovyScriptEngine klasa je posebno za one programe koji oslanjati se na ponovno učitavanje skripte i njezinih ovisnosti.

Iako imamo ove dodatne značajke, implementacija ima samo nekoliko malih razlika:

privatni konačni GroovyScriptEngine motor; private void addWithGroovyScriptEngine (int x, int y) baca IllegalAccessException, InstantiationException, ResourceException, ScriptException {Class calcClass = engine.loadScriptByName ("CalcMath.groovy"); GroovyObject calc = calcClass.newInstance (); Rezultat objekta = calc.invokeMethod ("calcSum", novi objekt [] {x, y}); LOG.info ("Rezultat metode CalcMath.calcSum () je {}", rezultat); } javni MyJointCompilationApp () {... URL url = null; probajte {url = nova datoteka ("src / main / groovy / com / baeldung /"). toURI (). toURL (); } catch (MalformedURLException e) {LOG.error ("Iznimka tijekom izrade url-a", e); } engine = novi GroovyScriptEngine (novi URL [] {url}, this.getClass (). getClassLoader ()); engineFromFactory = novo GroovyScriptEngineFactory (). getScriptEngine (); }

Ovaj put moramo konfigurirati izvorne korijene, a skriptu upućujemo samo s njenim imenom, što je malo čišće.

Gledajući unutar loadScriptByName metodu, možemo odmah vidjeti ček isSourceNewer gdje motor provjerava je li izvor koji je trenutno u predmemoriji i dalje valjan.

Svaki put kad se naša datoteka promijeni, GroovyScriptEngine automatski će ponovo učitati tu određenu datoteku i sve klase ovisno o njoj.

Iako je ovo zgodna i moćna značajka, mogla bi izazvati vrlo opasnu nuspojavu: ponovno učitavanje mnogo puta ogromnog broja datoteka rezultirat će procesorskim troškovima bez upozorenja.

Ako se to dogodi, možda ćemo trebati primijeniti vlastiti mehanizam predmemoriranja kako bismo se pozabavili tim problemom.

6.4. GroovyScriptEngineFactory (JSR-223)

JSR-223 pruža a standardni API za pozivanje okvira za skriptiranje od Jave 6.

Implementacija izgleda slično, iako se vraćamo učitavanju putem punih staza datoteka:

privatni konačni ScriptEngine engineFromFactory; private void addWithEngineFactory (int x, int y) baca IllegalAccessException, InstantiationException, javax.script.ScriptException, FileNotFoundException {Class calcClas = (Class) engineFromFactory.eval (new FileReader (new File ("src / main / ba / src / main / ba / src / main "," CalcMath.groovy "))); GroovyObject calc = (GroovyObject) calcClas.newInstance (); Rezultat objekta = calc.invokeMethod ("calcSum", novi objekt [] {x, y}); LOG.info ("Rezultat metode CalcMath.calcSum () je {}", rezultat); } public MyJointCompilationApp () {// ... engineFromFactory = new GroovyScriptEngineFactory (). getScriptEngine (); }

Super je ako integriramo našu aplikaciju s nekoliko skriptnih jezika, ali njegov je skup značajki ograničeniji. Na primjer, ne podržava ponovno učitavanje klase. Kao takvi, ako se integriramo samo s Groovyem, možda bi bilo bolje držati se ranijih pristupa.

7. Zamke dinamične kompilacije

Pomoću bilo koje gore navedene metode mogli bismo stvoriti aplikaciju koja čita skripte ili klase iz određene mape izvan naše jar datoteke.

Ovo bi nam dalo fleksibilnost dodavanja novih značajki dok sustav radi (osim ako nam je potreban novi kôd u Java dijelu), čime se postiže neka vrsta kontinuiranog razvoja isporuke.

Ali pripazite ovaj mač s dvije oštrice: sada se moramo vrlo pažljivo zaštititi kvarovi koji bi se mogli dogoditi i za vrijeme kompajliranja i za vrijeme izvođenja, de facto osiguravajući da naš kôd sigurno ne uspije.

8. Zamke pokretanja Groovyja u Java projektu

8.1. Izvođenje

Svi znamo da kada sustav treba biti vrlo učinkovit, mora se slijediti neka zlatna pravila.

Dvije koje bi mogle imati veći značaj za naš projekt su:

  • izbjegavajte refleksiju
  • smanjite broj uputa za bajt kod

Refleksija je posebno skupa operacija zbog postupka provjere klase, polja, metoda, parametara metode itd.

Ako analiziramo pozive metode s Jave na Groovy, na primjer, prilikom izvođenja primjera addWithCompiledClasses, hrpa operacija između .kalcSum a prvi redak stvarne Groovyjeve metode izgleda:

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

Što je u skladu s Javom. Isto se događa kada bacimo objekt koji je vratio loader i pozovemo njegovu metodu.

Međutim, ovo je ono što invokeMethod poziv čini:

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

U ovom slučaju možemo shvatiti što zapravo stoji iza Groovyjeve moći: MetaClass.

A MetaClass definira ponašanje bilo koje dane Groovy ili Java klase, tako da Groovy to proučava kad god se izvrši dinamička operacija kako bi se pronašla ciljna metoda ili polje. Jednom pronađen, izvršava ga standardni tok refleksije.

Dva zlatna pravila prekršena jednom metodom zazivanja!

Ako trebamo raditi sa stotinama dinamičnih Groovy datoteka, kako nazivamo svoje metode, tada će napraviti veliku razliku u izvedbi u našem sustavu.

8.2. Metoda ili svojstvo nisu pronađeni

Kao što je ranije spomenuto, ako želimo implementirati nove verzije Groovy datoteka u životnom ciklusu CD-a, moramo tretirati ih kao da su API odvojeno od našeg osnovnog sustava.

To znači postavljanje na mjesto višestruke provjere otkaza i ograničenja dizajna koda tako da naš novopridruženi programer ne diže proizvodni sustav u pogon pogrešnim pritiskom.

Primjeri svakog od njih su: imati CI cjevovod i koristiti obustavu metode umjesto brisanja.

Što će se dogoditi ako to ne učinimo? Dobivamo strašne iznimke zbog nedostajućih metoda i pogrešnog broja i vrste argumenata.

A ako mislimo da bi nas kompilacija spasila, pogledajmo metodu calcSum2 () naših Groovy skripti:

// ova metoda neće uspjeti u vrijeme izvođenja def calcSum2 (x, y) {// OPASNOST! Varijabla "log" može biti nedefinirana log.info "Izvršenje $ x + $ y" // OPASNOST! Ova metoda ne postoji! calcSum3 () // OPASNOST! Zabilježena varijabla "z" nije definirana! log.info ("Zapisivanje nedefinirane varijable: $ z")}

Pregledom cijele datoteke odmah vidimo dva problema: metodu calcSum3 () a varijabla z nisu nigdje definirani.

Unatoč tome, skripta je uspješno sastavljena, bez ijednog upozorenja, kako statički u Mavenu, tako i dinamički u GroovyClassLoaderu.

Neće uspjeti samo kad ga pokušamo prizvati.

Mavenova statička kompilacija prikazat će pogrešku samo ako se naš Java kôd izravno odnosi na nju calcSum3 (), nakon lijevanja GroovyObject kao što to činimo u addWithCompiledClasses () metoda, ali je i dalje neučinkovita ako umjesto toga koristimo refleksiju.

9. Zaključak

U ovom smo članku istražili kako možemo integrirati Groovy u našu Java aplikaciju, gledajući različite metode integracije i neke probleme s kojima se mogu susresti mješoviti jezici.

Kao i obično, izvorni kod korišten u primjerima može se naći na GitHubu.