1. 简介

本文将介绍如何将 Groovy 集成到 Java 应用中。我们会探讨几种主流的集成方式,包括 Maven 编译配置、运行时动态加载 Groovy 脚本等,并分析其优缺点及潜在问题。


2. 关于 Groovy 的简要说明

Groovy 是一种强大且灵活的 可选静态类型 + 动态类型语言,由 Apache 软件基金会维护,社区活跃,开发者贡献广泛。

它既可以用于构建完整应用,也可以作为模块或脚本嵌入 Java 项目中。例如:

  • 构建整个系统
  • 实现插件或配置逻辑
  • 动态运行脚本(evaluated on the fly)

如需深入了解 Groovy,请参考 Groovy 语言简介官方文档


3. Maven 依赖配置

使用 Maven 集成 Groovy 最简单的方式是引入 groovy-all,它包含了所有必需的依赖:

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>3.0.15</version>
    <type>pom</type>
</dependency>

✅ 优点:简单粗暴,无需关心版本兼容性
❌ 缺点:可能引入不必要的依赖,体积较大


4. 联合编译(Joint Compilation)

联合编译指的是在同一个项目中同时编译 Java 和 Groovy 文件。其核心机制如下:

  • Groovy 编译器解析源码
  • 根据实现生成 Java 可识别的 stubs(存根)
  • Java 编译器编译 stubs + Java 源文件
  • 最后编译 Groovy 源文件

⚠️ 如果不使用联合编译,Java 源文件将被当作 Groovy 编译,虽然语法兼容性高,但语义可能不同,导致运行时错误。


5. Maven 编译插件

目前主流的 Groovy 编译插件有两个:

5.1. Groovy-Eclipse Maven 插件

✅ 优点:

  • 不需要生成 stubs
  • 编译过程更简洁

❌ 缺点:

  • 配置稍复杂
  • 需要额外添加 Bintray 仓库

配置示例:

<pluginRepositories>
    <pluginRepository>
        <id>bintray</id>
        <name>Groovy Bintray</name>
        <url>https://dl.bintray.com/groovy/maven</url>
    </pluginRepository>
</pluginRepositories>

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.12.1</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.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-batch</artifactId>
            <version>3.0.9-03</version>
        </dependency>
    </dependencies>
</plugin>

5.2. GMavenPlus 插件

✅ 优点:

  • 支持 invokedynamic、Android、交互式 shell
  • 更现代,支持动态编译和热加载

❌ 缺点:

  • 会修改 Maven 的源码目录结构
  • 需手动管理 stubs 的生成与清理

配置示例:

<plugin>
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>2.1.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>
            <version>3.0.15</version>
            <scope>runtime</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</plugin>

5.3. 使用 Groovy-Eclipse 插件编译

执行命令:

$ mvn clean compile

输出示例:

[INFO] Using Groovy-Eclipse compiler to compile both Java and Groovy files

5.4. 使用 GMavenPlus 编译

执行命令:

$ mvn -f gmavenplus-pom.xml clean compile

输出关键步骤:

  1. 生成 stubs
  2. 编译 Java(含 stubs)
  3. 编译 Groovy 文件
  4. 清理 stubs

⚠️ 如果编译失败,可能残留 stubs,影响后续构建,建议使用 clean 构建避免此类问题。

5.5. 打包为可执行 Jar

使用 maven-assembly-plugin 打包所有依赖为 fat jar:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.4.2</version>
    <configuration>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifest>
                <mainClass>com.baeldung.MyJointCompilationApp</mainClass>
            </manifest>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <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 代码

6.1. GroovyClassLoader

  • 可解析文件或字符串
  • 缓存文件类,不缓存字符串类
  • 是 Groovy 动态加载的基础

示例代码:

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

  • 生成 Script 对象
  • 可通过 run() 执行整个脚本,或通过 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());
}

6.3. 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 = new File("src/main/groovy/com/baeldung/").toURI().toURL();
    engine = new GroovyScriptEngine(new URL[] {url}, this.getClass().getClassLoader());
}

6.4. GroovyScriptEngineFactory (JSR-223)

  • 基于 JSR-223 标准
  • 支持多种脚本语言,但功能受限
  • 不支持类重载

示例代码:

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();
}

7. 动态编译的陷阱

  • 可实现运行时脚本更新,实现类 CD(持续交付)能力

  • 但需注意:

    • 编译失败风险(如语法错误)
    • 运行时异常(如方法缺失)
    • 内存泄漏(如未清理的类加载器)

建议:

  • 使用 CI/CD 流程验证脚本
  • 对脚本进行沙箱隔离
  • 使用版本控制

8. 在 Java 项目中使用 Groovy 的常见问题

8.1. 性能问题

Groovy 动态方法调用依赖 MetaClass 和反射机制,性能远低于 Java 原生调用。

对比示例:

  • Java 直接调用栈:4 层
  • Groovy invokeMethod 调用栈:15+ 层

✅ 建议:

  • 尽量避免高频动态调用
  • 使用静态编译(@CompileStatic
  • 对关键路径进行性能测试

8.2. 方法或变量未定义

Groovy 不强制检查方法和变量是否存在,导致运行时异常:

def calcSum2(x, y) {
    log.info "Executing $x + $y"
    calcSum3()  // 方法不存在
    log.info("Logging an undefined variable: $z")  // z 未定义
}

⚠️ 即使编译通过,运行时仍可能失败。

✅ 建议:

  • 使用 CI 验证脚本逻辑
  • 弃用方法而非删除
  • 提供运行时异常捕获机制

9. 总结

本文介绍了几种在 Java 项目中集成 Groovy 的主流方式:

  • Maven 联合编译(Groovy-Eclipse、GMavenPlus)
  • 动态加载(GroovyClassLoader、GroovyShell、GroovyScriptEngine)

同时也分析了潜在的风险:

  • 性能瓶颈
  • 编译与运行时错误
  • 类加载和内存泄漏

如果你希望利用 Groovy 的灵活性来增强 Java 项目,建议根据实际需求选择合适的集成方式,并做好安全和性能评估。

完整示例代码请参考 GitHub


原始标题:Integrating Groovy into Java Applications