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 注释的时候也可以运行

Sad smile

,其转换后的 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: @PackageScope class T {
   2:     def i,j
   3: }

则 i/j 自动变成 friendly。

但再次使人困惑的是,这个注释似乎在处理 method 的时候出现了问题,而且试图像注释提供参数([CLASS,FIELDS])的尝试也宣告失败……我想和先前的 Field 注释一样,这应该是个未完成的作品。因此,飞哥又说:

在 Groovy 升级到 1.8.5 以前,请当这个注释不存在吧

Smile

(还是那句话,除非你正在被一些奇特的第三方框架所折磨,大可忽略这个问题。事实上 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.AutoClonestyle.*
   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

(该死的官方文档居然在这里拼写错误,害我莫名了十二秒钟

Sad smile

这个注释比较简单

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 代码如下:

1: public MyException() {
   2:     super()
   3: }
   4:  
   5: public MyException(java.lang.String param0) {
   6:     super(param0)
   9: public MyException(java.lang.String param0,java.lang.Throwable param1) {
  10:     super(param0,param1)
  11: }
  12:  
  13: public MyException(java.lang.Throwable param0) {
  14:     super(param0)
  15: }

@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...