Java集成Groovy
1. 介绍
在这次教程里,我们将会探索一下如何将Groovy集成到一个Java应用中.
2. Groovy的简短介绍
Groovy是一个很有用的弱类型动态语言。开发支持主要来源于Apache基金会和超过200个开发者的Groovy社区。
它可以用来构建一个完整的工程,或者作为一个Module,第三方集成到Java代码中。甚至可以作为脚本在执行时动态编译。
更多的介绍,请阅读 Introduction to Groovy Language 或者 official documentation.
3. Maven依赖
在本教程时,最新的稳定版本是2.5.7
, 2.6
和3.0
都还在Beta
阶段。
类似于Spring Boot方便快捷,我们只需要引用groovy-all一个依赖就可以,不用关系它内部依赖的groovy其他module版本。
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy.version}</version>
<type>pom</type>
</dependency>
4. 联合编译(Joint compilation)
在开始具体如何编写Maven前,我们需要了解一下它集成的原理。
我们的代码包含了Java和Groovy文件. Groovy可以直接找到Java的class,但是Groovy应该如何找到Groovy的类和方法呢?
这就需要感谢联合编译了。
联合编译是一个Maven命令,负责将一个工程内的java和groovy进行编译。
在联合编译帮助下,Groovy编译器可以完成以下几件事情:
- 解析groovy原文件
- 根据编译实现方式的不同,创建stubs
- 调用Javac编译这些stubs,从而使Java的类可以找到
- 编译Groovy源文件,这样Groovy就可以找到依赖的Java类和方法
根据编译插件实现的不同,我们需要将Groovy文件放入到特定的文件夹或者通过配置告诉编译插件。
没有联合编译,Java原文件会被当成Groovy编译器当成Groovy脚本进行编译。有时候这是可行的,因为自从1.7开始,java的语法和groovy语法是兼容的,但是含义却可能并不一致。
5. Maven编译插件
支持联合编译的不止一个,但是都各有优缺点。目前使用最多的是Groovy-Eclipse
和GMaven+
.
5.1. Groovy-Eclipse插件
Groovy-Eclipse Maven plugin通过其他插件都会生成stubs来减少联合编译的复杂度,但是却显示比较奇怪。
为了可以使用最新的编译版本,我们需要添加Maven的二进制库:
<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>
然后,在插件部分,我们仍然需要配置Groovy编译器的版本.
实际上,我们使用的the Maven compiler plugin并不真正执行编译groovy,而是将这个编译工作交给 [the groovy-eclipse-batch artifact](https://search.maven.org/search?q=g:org.codehaus.groovy a: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版本应该与编译器版本保持一致。
最后我们配置我们的source路径。编译器将自动会扫描src/main/java和src/main/groovy,但是如果我们的java文件夹是空的,那么编译器就会停止不去寻找groovy文件。
对于test也是同样的机制。
如果想要强制扫描文件,我们可以添加任意文件到src/main/java或src/test/java,或者添加配置[groovy-eclipse-compiler plugin](https://search.maven.org/search?q=g:org.codehaus.groovy a:groovy-eclipse-compiler):
<plugin>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-compiler</artifactId>
<version>3.3.0-01</version>
<extensions>true</extensions>
</plugin>
配置负责告诉编译器去额外进行编译。
5.2. GMavenPlus插件
GMavenPlus plugin 看起来有点类似GMaven plugin, 但在这个项目中,作者不仅仅是改动了一下功能,更是将编译器的版本与Groovy版本进行解耦。
要想达到这样的目标,插件需要定义出编译插件的标准。
GMavenPlus compiler有一些其他编译插件没有的特性, 比如说invokedynamic, 交互式命令行, 和Android.
但它同时也会有一些劣势:
- 如果 修改Maven’s source文件夹 将对Java和Groovy都进行重新编译。
- 需要我们关注如何删除studs,如果maven的goal执行过程中不包括这个,那么就会更麻烦一些。
让我们来看一下配置[gmavenplus-plugin](https://search.maven.org/search?q=g:org.codehaus.gmavenplus a: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>
为了进行test,我们创建了一个gmavleplus-pom.xml.
5.3. 使用Eclipse-Maven插件编译
现在所有都配置好了,我们终于可以编译我们的class了。
在示例中,我们提供了一个简单的java应用,java代码在src/main/java, groovy代码在src/main/groovy下。
那么让我们来编译一下吧:
$ 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使用了一下额外的几步:
- 对应每个Groovy生成stub
- 编译Java文件,包括stubs和java代码
- 编译Groovy文件
GMavenPlus继承了生成stubs,这可能会很开发者带来一些麻烦。
在理想的情形中,一切都应该很顺利。但是增加步骤就会增加失败的记录:比如,构建可能在清理stubs前失败.
如果这种情况发生,旧的stubs就会留下来,虽然明明一切都没问题,但是IDE会提示编译错误。
当然这种情况也可以通过clean的命令进行解决。
5.5. 在Jar包打包Groovy依赖
**如果需要执行run the program as a jar **, 我们需要添加 [the maven-assembly-plugin](https://search.maven.org/search?q=g:org.apache.maven.plugins a:maven-assembly-plugin), 把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, 它可以读取文本或者文件格式的源码,通过编译生成对应的class对象。
当动态语言来源是文件时,groovy会缓存编译的结果, 来避免重复编译带来的负担。
但来源是String对象的脚本时,groovy不会缓存,因此可能会造成内存的泄露问题。
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
…
7. 动态编译的缺点
使用以上的动态方法,我们可以做到应用不重启直接读取脚本或者jar包外的文件来生成代码。
这让我们很方便在系统执行时去添加新逻辑,达到类似热部署的开发模式。
但是任何事情都是双刃剑,我们也必须知道动态可能会在编译器和运行期产生的失败,从而破坏我们的代码安全性。
8. Java中使用Groovy的缺点
8.1. 性能
…
8.2. 找不到方法或属性
必须要设置安全和准确检查…
9. 总结
在这篇文章中,我们探索了一下groovy与java集成的方式,和可能会遇到的一些问题.
按照管理,文章中的代码可以访问GitHub.