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
输出关键步骤:
- 生成 stubs
- 编译 Java(含 stubs)
- 编译 Groovy 文件
- 清理 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。