Groovy 1.8 新特性: 增强的 AST
编译器在生成字节码前,会先将代码转换为抽象代码树(Abstract Syntax Tree)。在这个转换过程中,我们可以让机器替我们自动插入很多代码。在新的版本中,Groovy 提供了更多的 AST 转换选项,从而进一步减少了那些重复而乏味的例行代码。
@Log 注释
通过使用该注释(或使用 @Commons/@Log4j/@Slf4j 选择其它日志框架),可以在 AST 转换过程中注入日志代码。
1: import groovy.util.logging.*
2:
3: @Log
4: class Car {
5: { log.info 'Static Initiation' }
6: }
7: new Car()
以上代码执行时,会生成级别为 INFO 的日志。(很久很久以前,我们都是靠 IDE 来插入日志代码的,因为很无趣,是吧)
@Field
这是个让我困惑不已的特性,以下是官方的说明:
在写脚本的时候,很容易混淆的是变量的类型,如下代码中 list 在没有修饰的时候其实是脚本对象 run() 方法的本地变量,因此无法在 func 中被调用。但使用 @Field 修饰后则成为了脚本对象的 private field,从而可以被调用。
1: list = [1,2]
2: def func() { list.max() }
3:
4: println func()
很简单对不?等我在 GroovyConsole 中进行验证的时候就不简单了。(不单是 GroovyConsole,我试过了包括命令行和诸如 groovyshell 的 evaluate 方法,都不正常)
首先,上述代码在没有加上 @Field 注释的时候也可以运行
,其转换后的 AST 大致如下:
1: public class script1305201472799
extends groovy.lang.Script {
3: private static org.codehaus.groovy.reflection.ClassInfo $staticclassInfo
4: public static transient boolean __$stMC
5: public static long __timeStamp
6: public static long __timeStamp__239_neverHappen1305201472803
7: final private static java.lang.Long $const$0
8: final private static java.lang.Long $const$1
9: private static org.codehaus.groovy.reflection.ClassInfo $staticclassInfo$
10:
11: public script1305201472799() {
12: }
13:
14: public script1305201472799(groovy.lang.Binding context) {
15: super.setBinding(context)
16: }
17:
18: public static void main(java.lang.String[] args) {
19: org.codehaus.groovy.runtime.InvokerHelper.runScript(script1305201472799,args)
20: }
21:
22: public java.lang.Object run() {
23: list = [1,monospace; direction: ltr; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"> 24: return this.println(this.func())
25: }
可以看到,list 的范围被局限在了 run() 内。
不要关闭 GroovyConsole,“直接加上”注释后,代码依旧可以运行。查看 AST,却发现 list 并没有提升为 field。奇了怪了……
关闭 GroovyConsole,重新打开这段代码……这一次,提示错误:
groovy.lang.MissingPropertyException: No such property: list for class: S
这是什么情况?因为第一次运行的内容作为上下文缓存了下来,因此在第二次运行的时候其实根本就没有处理 @Field 这个注释!而在第三次运行的时候,没有上下文,而编译器无法找到 Field 这个注释的来源,就没有找到 list 这个变量。所以飞哥说:
在 GroovyConsole 中运行 Groovy 脚本,要记得清空上下文(Script 菜单 -> Clear Script Context),否则可能会产生意想不到的问题。
不过问题并没有解决,为啥在没有注释的情况下也能运行?我实在是想不出来了。(需要指出的是,在 Windows 下报错信息是错误的,因为问题不在于有没有 list 这个变量,而是没能正确处理 Field 这个注释。相对的在 Linux 版本下,我得到的报错则是无法找到 Field 这个类。)
为了像官方文档描述的那样使用 @Field 注释,我们需要添加一条 import 语句
1: import groovy.transform.Field
这样,转换后的 AST 变成了如下代码
1: public class script1305202926907
extends groovy.lang.Script {
3: java.lang.Object list
4: private static org.codehaus.groovy.reflection.ClassInfo $staticclassInfo
5: public static transient boolean __$stMC
6: public static long __timeStamp
7: public static long __timeStamp__239_neverHappen1305202926915
8: final private static java.lang.Long $const$0
9: final private static java.lang.Long $const$1
10: private static org.codehaus.groovy.reflection.ClassInfo $staticclassInfo$
11:
12: public script1305202926907() {
13: list = [1,monospace; direction: ltr; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"> 14: }
15:
16: public script1305202926907(groovy.lang.Binding context) {
17: list = [1,monospace; direction: ltr; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"> 18: super.setBinding(context)
19: }
20:
21: public static void main(java.lang.String[] args) {
22: org.codehaus.groovy.runtime.InvokerHelper.runScript(script1305202926907,args)
23: }
24:
25: public java.lang.Object run() {
26: null
27: return this.println(this.func())
28: }
29:
30: public java.lang.Object func() {
31: return list.max()
32: }
注意在第三行,list 确实变成了 field。
@PackageScope
对于多数用户而言,这应该不会是特别重要的特性。考虑以下代码:
1: class T {
2: @PackageScope int i
3: int j
4: def p() { println 'hell world' }
5: }
在默认情况下,Groovy 会自动为 i/j 生成 setter/getter 方法,并将其设为 private,但是在使用 @PackageScope 进行修饰后,i 变成了 friendly 而不是 private,accessor 也没有自动生成,其 AST 大致如下:
1: public class T
implements groovy.lang.GroovyObject
extends java.lang.Object {
3: @groovy.lang.PackageScope
4: int i
5: private int j
6: public java.lang.Object p() {
7: return this.println('hell world')
8: }
9: public int getJ() {
10: }
12: public void setJ(int value) {
13: }
14:
15: ...
16: }
如果对 class 进行修饰的话:
1: @PackageS
cope
class T {
2: def i,j
3: }
则 i/j 自动变成 friendly。
但再次使人困惑的是,这个注释似乎在处理 method 的时候出现了问题,而且试图像注释提供参数([CLASS,FIELDS])的尝试也宣告失败……我想和先前的 Field 注释一样,这应该是个未完成的作品。因此,飞哥又说:
在 Groovy 升级到 1.8.5 以前,请当这个注释不存在吧
(还是那句话,除非你正在被一些奇特的第三方框架所折磨,大可忽略这个问题。事实上 Scala 的语法也提供了类似的粒度及细的访问控制机制,但除非你在做非常底层的开发,否则不太可能用到这种程度的控制吧?)
@AutoClone
这将是一个非常重要的注释。
1: import groovy.transform.AutoClone
2: @AutoClone class T {
3: int i
4: List strs
5: }
以上代码将被扩展为:
1: public class T
implements java.lang.Cloneable,groovy.lang.GroovyObject
extends java.lang.Object {
3: public java.lang.Object clone() throws java.lang.CloneNotSupportedException {
4: java.lang.Object _result = super.clone()
5: if ( i instanceof java.lang.Cloneable) {
6: _result .i = i.clone()
7: }
8: if ( strs instanceof java.lang.Cloneable) {
9: _result .strs = strs.clone()
10: }
11: return _result
14: ...
如果这个类的全部成员都是 final 或是 Cloneable,那么你可以这么写:
2: import static groovy.transform.AutoClo
nestyle.*
3: @AutoClone(style=copY_CONSTRUCTOR) class T {
4: final int i
5: }
现在 Groovy 将使用构造函数来完成 clone
1: protected T(T other) {
2: if ( other .i instanceof java.lang.Cloneable) {
3: this .i = other .i.clone()
4: } else {
5: this .i = other .i
6: }
7: }
8:
9: public java.lang.Object clone() throws java.lang.CloneNotSupportedException {
10: return new T(this)
11: }
如果类已经被实现为 Serializable / Externalizable,那么还可以使用 style = SERIALIZATION 的参数来使用 Serializable 机制进行 clone。
(飞哥:总算这个重要功能没有掉链子……)
@AutoExternalize
(该死的官方文档居然在这里拼写错误,害我莫名了十二秒钟
)
这个注释比较简单
1: import groovy.transform.*
2: @AutoExternalize class T {
3: String name
4: int age
5: transient String nickname
6: }
以上代码会被注入以下方法,同时类 T 会被标记为 Externalizable
1: public void writeExternal(java.io.ObjectOutput out)
throws java.io.IOException {
2: out.writeObject( name )
3: out.writeInt( age )
4: }
5:
6: public void readExternal(java.io.ObjectInput oin) {
7: name = oin.readobject()
8: age = oin.readInt()
9: }
注意,transient 成员被无视了
@TimedInterrupt
这一注释将有助于扼杀那些由患有BRAIN-damAGE综合症的用户所制造了巨耗时的任务,比如包含了死循环的代码
1: @TimedInterrupt(5)
2: import groovy.transform.*
4: while(true) {}
5: 'Done'
这是个死循环,但是在使用注释后,运行该脚本将抛出异常: java.util.concurrent.TimeoutException: Execution timed out after 5 units.
@ThreadInterrupt
我非常喜欢的一个做法是在系统中提供脚本接口,这样的话,业务逻辑变化的时候就不用升级整个系统,而只是写一个 DSL 片段就可以了。但如果这个 DSL 写的有问题,像 System.exit(-1) 这样的退出代码,那么很可能每次新的业务逻辑下来,CEO就要和我谈一次心……但是现在,参考以下代码:
2: @ThreadInterrupt
4: def f() {
5: println 'Hell'
6: }
在加上注释后,执行任何代码前都会检查相关进程有没有结束,检查 f() 的 AST 代码:
1: public java.lang.Object f() {
2: if (java.lang.Thread.currentThread().isInterrupted()) {
3: throw new java.lang.InterruptedException('Execution interrupted. The current thread has been interrupted.')
4: }
5: return this.println('Hell')
6: }
这样总安全了吧。
@ConditionalInterrupt
1: @ConditionalInterrupt({ counter++> 10})
4: counter = 0
6: 2.times { println 'Oh...' }
7:
8: def scriptMethod() {
9: 14.times { t ->
10: 3.times {
11: println counter
12: println "executing script method...$t * $it"
13: }
15: }
16:
17: println counter
18: scriptMethod()
有一个使用了闭包的注释。但这种做法有些奇怪,至少我觉得很少有人能在以上代码中以直觉来判断 counter 在每行代码中的值。根据输出,大致可以看出每次进入一个代码块,就会进行一次条件判断。总的来说,这样的用法多少带了点不好的“味道”。
@ToString
3: @ToString
4: class T {
5: int i,j
8: println new T(i: 12,j: -1)
9: // => T(12,-1)
好吧,到这里我有点觉得 Groovy 的注释快走火入魔了……不过这个功能让我想起了 Scala 的 case 类和 Groovy 的 Expando。虽然输出很简单,但是看看 AST 的 toString 就觉得头大了。
1: public java.lang.String toString() {
2: java.lang.Object _result = new java.lang.StringBuffer()
3: _result.append('T')
4: _result.append('(')
5: if ( $print$names ) {
6: _result.append('i')
7: _result.append(':')
9: _result.append(org.codehaus.groovy.runtime.InvokerHelper.toString( i ))
10: _result.append(',')
11: if ( $print$names ) {
12: _result.append('j')
13: _result.append(':')
15: _result.append(org.codehaus.groovy.runtime.InvokerHelper.toString( j ))
16: _result.append(')')
17: return _result.toString()
18: }
复杂吧……在这里,飞哥的意见是:尽量使用这个功能自动生成以 debug 为目的的 toString 代码,其它就别多想了。
@EqualsAndHashCode
如果你的比较逻辑是基于比较所有成员的值,那么可以使用这个注释
3: @EqualsAndHashCode
6: }
对应的头大代码是:
1: public int hashCode() {
2: java.lang.Object _result = org.codehaus.groovy.util.HashCodeHelper.initHash()
3: _result = org.codehaus.groovy.util.HashCodeHelper.updateHash( _result,i )
4: _result = org.codehaus.groovy.util.HashCodeHelper.updateHash( _result,j )
5: return _result
8: public boolean equals(java.lang.Object other) {
9: if ( other == null) {
10: return false
11: }
12: if (T != other.getClass()) {
13: return false
15: if (this.is(other)) {
16: return true
17: }
18: other = (( other ) as T)
19: if ( i != other .i) {
20: return false
21: }
22: if ( j != other .j) {
23: return false
24: }
25: return true
26: }
这个功能应该也会属于常用功能吧。
@TupleConstructor
4: @TupleConstructor
5: class T {
6: int i,monospace; direction: ltr; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"> 9: println new T(1,-12)
10: // => T(1,-12)
基于元组的构造函数。
@Canonical
注意在上述代码中共叠放了两个注释。从我的角度来看,ToString,TupleConstructor以及Equals/HashCode 都是最常用的功能,而 Canonical 正是它们的组合。当然,也可以个别指定,就好象如下的 toString:
3: @Canonical
4: @ToString(includeNames = true)
10: // => T(i:1,j:-12)
@InheritConstructors
在生成子类的时候,往往不得不一个一个的将超类的构造函数“抄写”一遍,以后不用了
2: @InheritConstructors class MyException extends Exception {}
在这里,MyException 将照搬 Exception 的所有构造函数,AST 代码如下:
@WithReadLock / @WithWriteLock
2: public class ResourceProvider {
4: private final Map data = new HashMap();
6: @WithReadLock
7: public String getResource(String key) throws Exception {
8: return data.get(key);
9: }
11: @WithWriteLock
12: public void refresh() throws Exception {
13: //reload the resources into memory
15: }
在以上代码中,如果没有调用 refresh(),那么可以有多个线程同时调用 getResource(key),一旦有任何线程开始调用 refresh(),那么锁就会生效,所有调用 getResource 的线程都将进入等待状态。
@ListenerList
这是针对 Collection 类的绑定机制。
2: import groovy.beans.*
4: interface MyListener {
5: void eventOccurred(MyEvent event)
8: @Canonical
9: class MyEvent {
10: def source
11: String message
12: }
14: class MyBean {
15: @ListenerList List listeners
16: }
对于 MyBean 而言,AST 将为其添加如下方法:
1: public void addMyListener(MyListener listener) {
2: if ( listener == null) {
3: return null
5: if ( listeners == null) {
6: listeners = []
7: }
8: listeners.add(listener)
9: }
11: public void removeMyListener(MyListener listener) {
12: if ( listener == null) {
13: return null
15: if ( listeners == null) {
16: listeners = []
18: listeners.remove(listener)
19: }
21: public void fireEventOccurred(MyEvent event) {
22: if ( listeners != null) {
23: java.util.ArrayListextends java.lang.Object> __list = new java.util.ArrayListextends java.lang.Object>(listeners)
24: for (java.lang.Object listener : __list ) {
25: listener.eventOccurred(event)
26: }
27: }
28: }
这一注释可以简化 Bean 的编写(不过老实说,我倒是没怎么用 Groovy 来写 Bean)。
PS:总的来说,1.8 版本的 AST 新特性还是很让人满意的,但是大家也看到了 AST 生成的代码了,毕竟是机器生成的,效率上稍稍打了折扣。
相关文章
背景: 8月29日,凌晨4点左右,某服务告警,其中一个...
https://support.smartbear.comeadyapi/docs/soapui/steps/g...
有几个选项可用于执行自定义JMeter脚本并扩展基线JMeter功能...
Scala和Java为静态语言,Groovy为动态语言Scala:函数式编程,...
出处:https://www.jianshu.com/p/ce6f8a1f66f4一、一些内部...
在运行groovy的junit方法时,报了这个错误:java.lang.Excep...