1. 序章

このチュートリアルでは、GroovyをJavaアプリケーションに統合するための最新の手法について説明します。

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

Groovyプログラミング言語は、強力なオプションで入力された動的言語です。 これは、Apache Software FoundationとGroovyコミュニティによってサポートされており、200人以上の開発者からの貢献があります。

これを使用して、アプリケーション全体を構築したり、Javaコードと対話するモジュールや追加のライブラリを作成したり、その場で評価およびコンパイルされたスクリプトを実行したりできます。

詳細については、 Groovy言語の概要を読むか、公式ドキュメントにアクセスしてください。

3. Mavenの依存関係

執筆時点では、最新の安定版リリースは2.5.7ですが、Groovy 2.6および3.0(どちらも2017年秋に開始)はまだアルファ段階です。

Spring Bootと同様に、 groovy-all pomを含めるだけで、バージョンを気にせずに、必要になる可能性のあるすべての依存関係を追加できます。

<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クラスとメソッドを見つけさせたい場合はどうでしょうか。

共同編集が救いの手を差し伸べます!

共同コンパイルは、JavaファイルとGroovyファイルの両方を同じプロジェクトで単一のMavenコマンドでコンパイルするように設計されたプロセスです。

共同コンパイルを使用すると、Groovyコンパイラは次のようになります。

  • ソースファイルを解析します
  • 実装に応じて、Javaコンパイラと互換性のあるスタブを作成します
  • Javaコンパイラを呼び出して、Javaソースとともにスタブをコンパイルします。これにより、JavaクラスはGroovyの依存関係を見つけることができます。
  • Groovyソースをコンパイルする–これでGroovyソースはJavaの依存関係を見つけることができます

それを実装するプラグインによっては、ファイルを特定のフォルダーに分割するか、コンパイラーにそれらの場所を指示する必要がある場合があります。

共同コンパイルがないと、JavaソースファイルはGroovyソースであるかのようにコンパイルされます。ほとんどのJava 1.7構文はGroovyと互換性があるため、これが機能する場合がありますが、セマンティクスは異なります。

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

共同コンパイルをサポートするコンパイラプラグインがいくつかあります、それぞれに長所と短所があります。

Mavenで最も一般的に使用される2つは、Groovy-EclipseMavenとGMaven+です。

5.1. Groovy-EclipseMavenプラグイン

Groovy-EclipseMavenプラグインは、スタブの生成を回避することで共同コンパイルを簡素化します。これは、GMaven + などの他のコンパイラーにとって必須の手順ですが、いくつかの構成上の癖があります。 。

最新のコンパイラアーティファクトを取得できるようにするには、MavenBintrayリポジトリを追加する必要があります。

<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コンパイラのバージョンを通知します。

実際、使用するプラグイン– Mavenコンパイラプラグイン–は実際にはコンパイルされませんが、代わりに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 /javasrc/ main / groovy、などのフォルダーを調べますが javaフォルダーが空の場合、コンパイラーはGroovyソースを検索しません。

同じメカニズムが私たちのテストにも当てはまります。

ファイル検出を強制するには、 src / main /javaおよびsrc/ test / java に任意のファイルを追加するか、groovy-eclipse-compilerプラグイン[ X157X]:

<plugin>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-eclipse-compiler</artifactId>
    <version>3.3.0-01</version>
    <extensions>true</extensions>
</plugin>

The プラグインに2つのGroovyソースフォルダーを含む追加のビルドフェーズと目標を追加させるには、セクションが必須です。

5.2. GMavenPlusプラグイン

GMavenPlusプラグインは古いGMavenプラグインに似た名前を持っているかもしれませんが、作者は単なるパッチを作成する代わりに、コンパイラを単純化して特定のGroovyバージョンから切り離すように努力しました 。

そのために、プラグインはコンパイラプラグインの標準ガイドラインから分離します。

GMavenPlusコンパイラは、 invokedynamic 、インタラクティブシェルコンソール、Androidなど、当時の他のコンパイラにはまだ存在していなかった機能のサポートを追加します。

反対に、それはいくつかの複雑さを示します:

  • Mavenのソースディレクトリを変更して、JavaとGroovyソースの両方を含みますが、Javaスタブは含みません。
  • it では、適切な目標でスタブを削除しない場合、スタブを管理する必要があります

プロジェクトを構成するには、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ファイルでの依存関係のパッケージ化

コマンドラインからプログラムをjarとして実行するために、 maven-assembly-plugin を追加しました。これには、接尾辞が付いた「fatjar」にすべてのGroovy依存関係が含まれます。プロパティdescriptorRef:で定義されています

<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、が必要です。これは、テキストまたはファイル形式のソースコードを解析し、結果のクラスオブジェクトを生成できます。

ソースがファイルの場合、同じクラスの複数のインスタンスをローダーに要求するときのオーバーヘッドを回避するために、コンパイル結果もキャッシュされます

代わりに、 Stringオブジェクトから直接取得されたスクリプトはキャッシュされません。したがって、同じスクリプトを複数回呼び出すと、メモリリークが発生する可能性があります。

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

シェルスクリプトローダー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 メソッドの内部を見ると、エンジンが現在キャッシュにあるソースがまだ有効であるかどうかをチェックするチェックisSourceNewerをすぐに確認できます。

ファイルが変更されるたびに、 GroovyScriptEngine は、その特定のファイルとそれに依存するすべてのクラスを自動的に再読み込みします。

これは便利で強力な機能ですが、非常に危険な副作用を引き起こす可能性があります。大量のファイルを何度もリロードすると、警告なしにCPUオーバーヘッドが発生します。

その場合、この問題に対処するために独自のキャッシュメカニズムを実装する必要があるかもしれません。

6.4. GroovyScriptEngineFactory (JSR-223)

JSR-223 は、Java6以降のスクリプトフレームワークを呼び出すための標準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つは次のとおりです。

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

特にリフレクションは、クラス、フィールド、メソッド、メソッドパラメータなどをチェックするプロセスのためにコストのかかる操作です。

たとえば、JavaからGroovyへのメソッド呼び出しを分析すると、例 addWithCompiledClasses を実行すると、.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つのinvokeメソッドで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. 結論

この記事では、GroovyをJavaアプリケーションに統合する方法を探り、さまざまな統合方法と、混合言語で発生する可能性のあるいくつかの問題について説明しました。

いつものように、例で使用されているソースコードはGitHubにあります。