GroovyをJavaアプリケーションに統合する

  • link:/category/programming/ [プログラミング]

  • Groovy

1. 前書き

このチュートリアルでは、GroovyをJavaアプリケーションに統合するための最新のテクニックを探ります。

2. Groovyについてのいくつかの言葉

Groovyプログラミング言語は、強力な*オプションで型指定された動的言語*です。 Apache Software FoundationとGroovyコミュニティによってサポートされており、200人以上の開発者からの貢献があります。
これは、アプリケーション全体の構築、Javaコードと対話するモジュールまたは追加ライブラリの作成、またはオンザフライで評価およびコンパイルされたスクリプトの実行に使用できます。
詳細については、https://www.baeldung.com/groovy-language [Groovy言語の紹介]を読むか、http://groovy-lang.org/ [公式ドキュメント]にアクセスしてください。

3. Mavenの依存関係

執筆時点では、最新の安定版リリースは2.5.7ですが、Groovy 2.6と3.0(両方とも'17年秋に開始)はまだアルファ段階です。
Spring Bootと同様に、すべてのバージョンについて心配することなく、必要な依存関係*:
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>${groovy.version}</version>
    <type>pom</type>
</dependency>

4. 共同編集

Mavenの構成方法の詳細に入る前に、何を扱っているのかを理解する必要があります。
*コードにはJavaファイルとGroovyファイルの両方が含まれます*。 GroovyはJavaクラスを見つけることにまったく問題はありませんが、JavaにGroovyクラスとメソッドを見つけさせたい場合はどうでしょうか。
共同編集が助けになります!
*ジョイントコンパイルは、1つのMavenコマンドで、同じプロジェクト内のJavaファイルとGroovy *ファイルの両方をコンパイルするように設計されたプロセスです。
共同コンパイルでは、Groovyコンパイラーは次のことを行います。
  • ソースファイルを解析する

  • 実装に応じて、以下と互換性のあるスタブを作成します
    Javaコンパイラ

  • Javaコンパイラを呼び出して、Javaソースとともにスタブをコンパイルします
    –このようにして、JavaクラスはGroovyの依存関係を見つけることができます

  • Groovyソースをコンパイルします–今、Groovyソースは
    Javaの依存関係

    それを実装するプラグインによっては、ファイルを特定のフォルダーに分けるか、コンパイラーにファイルの場所を伝える必要があります。
    *ジョイントコンパイルを行わないと、JavaソースファイルはGroovyソースであるかのようにコンパイルされます。

5. Mavenコンパイラプラグイン

*共同コンパイルをサポートするコンパイラプラグインがいくつかあります*、それぞれ長所と短所があります。
Mavenで最も一般的に使用される2つは、Groovy-Eclipse MavenとGMaven +です。

5.1. Groovy-Eclipse Mavenプラグイン

https://github.com/groovy/groovy-eclipse/wiki/Groovy-Eclipse-Maven-plugin#why-another-groovy-compiler-for-maven-what-about-gmaven[Groovy-Eclipse Maven plugin] * GMaven __ + ___のような他のコンパイラーにとって必須のステップであるスタブの生成を回避することにより、共同コンパイルを簡素化しますが、いくつかの構成上の癖があります。
最新のコンパイラアーティファクトの取得を有効にするには、Maven Bintrayリポジトリを追加する必要があります。
<pluginRepositories>
    <pluginRepository>
        <id>bintray</id>
        <name>Groovy Bintray</name>
        <url>https://dl.bintray.com/groovy/maven</url>
        <releases>
            <!-- avoid automatic updates -->
            <updatePolicy>never</updatePolicy>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </pluginRepository>
</pluginRepositories>
次に、プラグインのセクションで、Mavenコンパイラーに使用するGroovyコンパイラーのバージョンを伝えます。*
実際、使用するプラグイン(link:/maven-compiler-plugin[Mavenコンパイラプラグイン])は実際にはコンパイルされませんが、代わりにジョブをhttps:// searchに委任します。 maven.org/search?q=g:org.codehaus.groovy%20a:groovy-eclipse-batch[_groovy-eclipse-batch_アーティファクト]:
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <compilerId>groovy-eclipse-compiler</compilerId>
        <source>${java.version}</source>
        <target>${java.version}</target>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-compiler</artifactId>
            <version>3.3.0-01</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-batch</artifactId>
            <version>${groovy.version}-01</version>
        </dependency>
    </dependencies>
</plugin>
_groovy-all_依存バージョンは、コンパイラバージョンと一致する必要があります。
最後に、ソースの自動検出を設定する必要があります。デフォルトでは、コンパイラは_src / main / java_や_src / main / groovy、_などのフォルダーを検索しますが、* Javaフォルダーが空の場合、コンパイラは検索しませんグルーヴィーなソース*。
テストでも同じメカニズムが有効です。
ファイルの検出を強制するには、_src / main / java_および_src / test / java_に任意のファイルを追加するか、単にhttps://search.maven.org/search?q=g:org.codehaus.groovy%を追加します。 20a:groovy-eclipse-compiler [_groovy-eclipse-compiler_ plugin]:
<plugin>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-eclipse-compiler</artifactId>
    <version>3.3.0-01</version>
    <extensions>true</extensions>
</plugin>
_ <extension> _セクションは、プラグインが2つのGroovyソースフォルダーを含む追加のビルドフェーズと目標を追加できるようにするために必須です。

5.2. GMavenPlusプラグイン

https://github.com/groovy/GMavenPlus/wiki/Choosing-Your-Build-Tool[GMavenPlus plugin]は、古いGMavenプラグインに似た名前を持っている場合がありますが、単なるパッチを作成する代わりに、作者が努力しました。 *コンパイラを特定のGroovyバージョンから単純化および分離する*。
そのために、プラグインはコンパイラプラグインの標準ガイドラインから分離します。
GMavenPlusコンパイラーは、http://groovy-lang.org/indy.html [invokedynamic]、対話型シェルコンソール、Androidなど、当時他のコンパイラーにまだ存在していなかった機能のサポートを追加します。
反対側には、いくつかの複雑な問題があります。
  • Mavenのソースディレクトリを変更して、Javaと
    Javaスタブではなく、Groovyソース

  • で削除しない場合は、スタブを管理する必要があります
    適切な目標

    プロジェクトを設定するには、https://search.maven.org/search?q = g:org.codehaus.gmavenplus%20a:gmavenplus-plugin [gmavenplus-plugin]を追加する必要があります。
<plugin>
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>1.7.0</version>
    <executions>
        <execution>
            <goals>
                <goal>execute</goal>
                <goal>addSources</goal>
                <goal>addTestSources</goal>
                <goal>generateStubs</goal>
                <goal>compile</goal>
                <goal>generateTestStubs</goal>
                <goal>compileTests</goal>
                <goal>removeStubs</goal>
                <goal>removeTestStubs</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <!-- any version of Groovy \>= 1.5.0 should work here -->
            <version>2.5.6</version>
            <scope>runtime</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</plugin>
このプラグインのテストを可能にするために、サンプルで__gmavenplus-pom.xml ___という2番目のpomファイルを作成しました。

5.3. Eclipse-Mavenプラグインを使用したコンパイル

すべてが設定されたので、クラスを最終的に構築できます。
提供した例では、ソースフォルダー_src / main / java_に単純なJavaアプリケーションを作成し、__ src / main / groovy_にいくつかのGroovyスクリプトを作成しました。Groovyクラスとスクリプトを作成できます。
Eclipse-Mavenプラグインですべてをビルドしましょう。
$ mvn clean compile
...
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ core-groovy-2 ---
[INFO] Changes detected - recompiling the module!
[INFO] Using Groovy-Eclipse compiler to compile both Java and Groovy files
...
ここでは、* Groovyがすべてをコンパイルしています*。

5.4. GMavenPlusでコンパイルする

GMavenPlusにはいくつかの違いがあります。
$ mvn -f gmavenplus-pom.xml clean compile
...
[INFO] --- gmavenplus-plugin:1.7.0:generateStubs (default) @ core-groovy-2 ---
[INFO] Using Groovy 2.5.7 to perform generateStubs.
[INFO] Generated 2 stubs.
[INFO]
...
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ core-groovy-2 ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to XXX\Baeldung\TutorialsRepo\core-groovy-2\target\classes
[INFO]
...
[INFO] --- gmavenplus-plugin:1.7.0:compile (default) @ core-groovy-2 ---
[INFO] Using Groovy 2.5.7 to perform compile.
[INFO] Compiled 2 files.
[INFO]
...
[INFO] --- gmavenplus-plugin:1.7.0:removeStubs (default) @ core-groovy-2 ---
[INFO]
...
GMavenPlusが次の追加手順を実行することがすぐにわかります。
  1. groovyファイルごとに1つのスタブを生成する

  2. Javaファイルのコンパイル–スタブとJavaコードの両方

  3. Groovyファイルのコンパイル

    GMavenPlusは、スタブを生成することにより、過去数年間に共同コンパイルを行う際に開発者に多くの頭痛の種をもたらした弱点を継承しています。
    理想的なシナリオでは、すべてが正常に機能しますが、より多くのステップを導入すると、障害点も増えます。たとえば、*スタブをクリーンアップする前にビルドが失敗する可能性があります*。
    これが起こると、古いスタブがIDEを混乱させ、すべてが正しいはずであることがわかっているコンパイルエラーが表示される可能性があります。
    クリーンビルドのみが、痛みを伴う長い魔女狩りを回避します。

5.5. jarファイルでの依存関係のパッケージ化

コマンドラインから* https://www.baeldung.com/executable-jar-with-maven [jarとしてプログラムを実行] *に、https://search.maven.org/search?q = gを追加しました:org.apache.maven.plugins%20a:maven-assembly-plugin [the _maven-assembly-plugin_]。これは、プロパティ_descriptorRef:_で定義された接尾辞で名前が付けられた「ファットjar」内のすべてのGroovy依存関係を含みます。
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <!-- get all project dependencies -->
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <!-- MainClass in mainfest make a executable jar -->
        <archive>
            <manifest>
                <mainClass>com.baeldung.MyJointCompilationApp</mainClass>
            </manifest>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <!-- bind to the packaging phase -->
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>
コンパイルが完了したら、次のコマンドでコードを実行できます。
$ java -jar target/core-groovy-2-1.0-SNAPSHOT-jar-with-dependencies.jar com.baeldung.MyJointCompilationApp

6. その場でGroovyコードをロードする

Mavenコンパイルにより、プロジェクトにGroovyファイルを組み込み、Javaからクラスとメソッドを参照できます。
ただし、実行時にロジックを変更する場合、これは十分ではありません。コンパイルは実行時ステージ外で実行されるため、変更を確認するにはアプリケーションを再起動する必要があります*。
Groovyの動的なパワー(およびリスク)を活用するには、アプリケーションが既に実行されているときにファイルをロードするために利用可能なテクニックを検討する必要があります。

6.1. _ GroovyClassLoader _

これを実現するには、テキストまたはファイル形式のソースコードを解析し、結果のクラスオブジェクトを生成できる_GroovyClassLoaderが必要です。
*ソースがファイルの場合、ローダーに同じクラスの複数のインスタンスを要求するときのオーバーヘッドを回避するために、コンパイル結果もキャッシュされます*。
代わりに、* _ St​​ring_オブジェクトから直接来るスクリプトはキャッシュされません*。したがって、同じスクリプトを複数回呼び出すと、メモリリークが発生する可能性があります。
_GroovyClassLoader_は、他の統合システムが構築される基盤です。
実装は比較的簡単です。
private final GroovyClassLoader loader;

private Double addWithGroovyClassLoader(int x, int y)
  throws 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 });
}

public MyJointCompilationApp() {
    loader = new GroovyClassLoader(this.getClass().getClassLoader());
    // ...
}

6.2. GroovyShell

Shell Script Loaderの_parse()_メソッドは、テキストまたはファイル形式のソースを受け入れ、* _ Script_クラスのインスタンスを生成します*。
このインスタンスは、_Script_から_run()_メソッドを継承します。このメソッドは、ファイル全体を上から下に実行し、実行された最後の行で指定された結果を返します。
必要に応じて、コード内で_Script_を拡張し、デフォルトの実装をオーバーライドして内部ロジックを直接呼び出すこともできます。
_Script.run()_を呼び出す実装は次のようになります。
private Double addWithGroovyShellRun(int x, int y) throws 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());
    // ...
}
_run()_はパラメーターを受け入れないため、ファイルに_Binding_オブジェクトを使用して初期化するグローバル変数を追加する必要があることに注意してください。
このオブジェクトは_GroovyShell_初期化で渡されるため、変数はすべての_Script_インスタンスと共有されます。
よりきめ細かいコントロールが必要な場合は、_invokeMethod()_を使用できます。これにより、リフレクションを介して独自のメソッドにアクセスし、引数を直接渡すことができます。
この実装を見てみましょう:
private final GroovyShell shell;

private Double addWithGroovyShell(int x, int y) throws IOException {
    Script script = shell.parse(new File("src/main/groovy/com/baeldung/", "CalcScript.groovy"));
    return (Double) script.invokeMethod("calcSum", new Object[] { x, y });
}

public MyJointCompilationApp() {
    // ...
    shell = new GroovyShell(loader, new Binding());
    // ...
}
内部では、_GroovyShell_は_GroovyClassLoader_を使用して、結果のクラスをコンパイルおよびキャッシュするため、前述の同じルールが同じ方法で適用されます。

6.3. _ GroovyScriptEngine _

_GroovyScriptEngine_クラスは、*スクリプトとその依存関係のリロード*に依存するアプリケーションに特に適しています。
これらの追加機能はありますが、実装にはわずかな違いしかありません。
private final GroovyScriptEngine engine;

private void addWithGroovyScriptEngine(int x, int y) throws IllegalAccessException,
  InstantiationException, ResourceException, ScriptException {
    Class<GroovyObject> calcClass = engine.loadScriptByName("CalcMath.groovy");
    GroovyObject calc = calcClass.newInstance();
    Object result = calc.invokeMethod("calcSum", new Object[] { x, y });
    LOG.info("Result of CalcMath.calcSum() method is {}", result);
}

public MyJointCompilationApp() {
    ...
    URL url = null;
    try {
        url = new File("src/main/groovy/com/baeldung/").toURI().toURL();
    } catch (MalformedURLException e) {
        LOG.error("Exception while creating url", e);
    }
    engine = new GroovyScriptEngine(new URL[] {url}, this.getClass().getClassLoader());
    engineFromFactory = new GroovyScriptEngineFactory().getScriptEngine();
}
今度はソースルートを構成する必要があり、スクリプトをその名前だけで参照します。
_loadScriptByName_メソッドの内部を見ると、現在キャッシュにあるソースがまだ有効かどうかをエンジンがチェックするcheck _isSourceNewer_がすぐにわかります。
*ファイルが変更されるたびに、_GroovyScriptEngine_はその特定のファイルとそれに依存するすべてのクラスを自動的にリロードします。*
これは便利で強力な機能ですが、非常に危険な副作用を引き起こす可能性があります。
その場合、独自のキャッシュメカニズムを実装してこの問題に対処する必要があります。

6.4. GroovyScriptEngineFactory(JSR-223)

https://jcp.org/aboutJava/communityprocess/final/jsr223/index.html[JSR-223]は、Java 6以降のスクリプトフレームワークを呼び出すための*標準APIを提供します。
実装は同様に見えますが、完全なファイルパスを介して読み込みに戻ります。
private final ScriptEngine engineFromFactory;

private void addWithEngineFactory(int x, int y) throws 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();
    Object result = calc.invokeMethod("calcSum", new Object[] { x, y });
    LOG.info("Result of CalcMath.calcSum() method is {}", result);
}

public MyJointCompilationApp() {
    // ...
    engineFromFactory = new GroovyScriptEngineFactory().getScriptEngine();
}
アプリをいくつかのスクリプト言語と統合するのは素晴らしいことですが、*その機能セットはより制限されています*たとえば、*クラスのリロードをサポートしていません*。 そのため、Groovyのみと統合する場合は、以前のアプローチに固執する方がよい場合があります。

7. 動的コンパイルの落とし穴

上記の方法のいずれかを使用して、jarファイルの外部の特定のフォルダーからスクリプトまたはクラスを読み取るアプリケーションを作成できます*。
これにより、システムの実行中に(Java部分に新しいコードが必要な場合を除いて)*新しい機能を追加できる柔軟性が得られ、ある種の継続的デリバリー開発が実現します。
しかし、この両刃の剣に注意してください:コンパイル時と実行時の両方で発生する可能性のある障害*から、コードを安全に失敗させることを事実上保証する必要があります。

8. JavaプロジェクトでGroovyを実行する場合の落とし穴

8.1. パフォーマンス

システムが非常に高いパフォーマンスを発揮する必要がある場合、従うべきいくつかの黄金のルールがあることは誰もが知っています。
私たちのプロジェクトでさらに重くなる可能性のある2つは次のとおりです。
  • 反射を避ける

  • バイトコード命令の数を最小限に抑える

    特に、リフレクションは、クラス、フィールド、メソッド、メソッドパラメーターなどをチェックするプロセスのため、コストのかかる操作です。
    たとえば、_addWithCompiledClasses_の例を実行しているときにJavaからGroovyへのメソッド呼び出しを分析すると、_。calcSum_と実際のGroovyメソッドの最初の行の間の操作のスタックは次のようになります。
calcSum:4, CalcScript (com.baeldung)
addWithCompiledClasses:43, MyJointCompilationApp (com.baeldung)
addWithStaticCompiledClasses:95, MyJointCompilationApp (com.baeldung)
main:117, App (com.baeldung)
これはJavaと一貫しています。 ローダーから返されたオブジェクトをキャストし、そのメソッドを呼び出すときにも同じことが起こります。
ただし、これは_invokeMethod_呼び出しが行うことです。
calcSum:4, CalcScript (com.baeldung)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (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)
invokeMethod:821, MetaClassImpl (groovy.lang)
invokeMethod:44, GroovyObjectSupport (groovy.lang)
invokeMethod:77, Script (groovy.lang)
addWithGroovyShell:52, MyJointCompilationApp (com.baeldung)
addWithDynamicCompiledClasses:99, MyJointCompilationApp (com.baeldung)
main:118, MyJointCompilationApp (com.baeldung)
この場合、Groovyのパワーの背後にあるもの、つまりMetaClassに感謝することができます。
  • _MetaClass_は特定のGroovyまたはJavaクラスの動作を定義するため、Groovyは、ターゲットメソッドまたはフィールドを見つけるために実行する動的な操作があるたびにそれを調べます。 検出されると、標準のリフレクションフローがそれを実行します。

    1つの呼び出しメソッドで2つのゴールデンルールが壊れています!
    数百の動的なGroovyファイルで作業する必要がある場合、*メソッドをどのように呼び出すかによって、システムのパフォーマンスが大きく異なります*。

8.2. メソッドまたはプロパティが見つかりません

前述したように、CDライフサイクルで* Groovyファイルの新しいバージョンを展開する場合は、コアシステムとは別にAPI *のように扱う必要があります。
これは、*複数のフェイルセーフチェックとコード設計制限*を導入することを意味するため、新しく参加した開発者が間違ったプッシュで本番システムを爆破することはありません。
それぞれの例は次のとおりです。CIパイプラインを持ち、削除の代わりにメソッドの廃止を使用します。
そうしないとどうなりますか? メソッドが欠落しているため、引数の数と型が間違っているため、恐ろしい例外が発生します。
そして、コンパイルが私たちを救うと思うなら、Groovyスクリプトの_calcSum2()_メソッドを見てみましょう:
// this method will fail in runtime
def calcSum2(x, y) {
    // DANGER! The variable "log" may be undefined
    log.info "Executing $x + $y"
    // DANGER! This method doesn't exist!
    calcSum3()
    // DANGER! The logged variable "z" is undefined!
    log.info("Logging an undefined variable: $z")
}
ファイル全体を見ると、2つの問題がすぐにわかります。メソッド_calcSum3()_と変数_z_はどこにも定義されていません。
それでも、Mavenでは静的に、GroovyClassLoaderでは動的に、警告は1つも表示されずに、スクリプトは正常にコンパイルされます。
呼び出そうとしたときにのみ失敗します。
Mavenの静的コンパイルは、_addWithCompiledClasses()_メソッドで行うように_GroovyObject_をキャストした後、Javaコードが_calcSum3()_を直接参照する場合にのみエラーを表示しますが、代わりにリフレクションを使用する場合は依然として無効です。

9. 結論

この記事では、JavaアプリケーションにGroovyを統合する方法を検討し、さまざまな統合方法と、混合言語で発生する可能性のあるいくつかの問題を調べました。
いつものように、例で使用されているソースコードは、https://github.com/eugenp/tutorials/tree/master/core-groovy-2 [GitHub]にあります。