这两天掉入了一个Groovy metaClass的陷阱,好不容易才爬出来。
为了说清楚这个问题,先看几行测试代码
void testMetaClass() { def domain = new Domain() domain.metaClass.p = 'v1' assertEquals 'v1', domain.p domain.p = 'v2' assertEquals 'v2', domain.p domain.metaClass.p = 'v3' assertEquals 'v2', domain.p }
这是一个已经通过了的测试:
-------------------------------------------------------
Running 1 unit test...
Running test com.grs.GroovyMetaClassTests...null
Empty test suite.
PASSED
Tests Completed in 359ms ...
-------------------------------------------------------
注意Groovy测试代码的最后两行:
domain.metaClass.p = 'v3' assertEquals 'v2', domain.p
乍一看挺让人纳闷的: 执行
domain.metaClass.p = 'v3'
之后,为什么domain.p的值仍然是'v2'呢?这就是我称之为“陷阱”的地方了。看来Groovy中似乎有这么一条规则:往GroovyObject中第一次通过
domain.metaClass.aProperty = aValue
的方式往domain中注入aProperty之后, 再执行
domain.metaClass.aProperty = yetAnotherValue
则无法更改domain.aProperty,而
domain.aProperty = yetAnotherValue
却可以成功更改。
爬出这个陷阱,让我们来看这个问题该如何解决:我怎么才能知道aProperty有没有已经注入呢?换言之,我怎么知道我是应该写成
domain.metaClass.aProperty = yetAnotherValue
呢,还是应该写成
domain.aProperty = yetAnotherValue
呢?
我有两个workarounds:
1. 通过try...catch...来检测
class GroovyFieldAccessor { static void set(String fieldName, Object obj, Object v) { def metaPropAlreadyInjected = true try { if(obj."$fieldName") {} // do nothing but check if d."$fieldName" will throw an exception. } catch(x) {metaPropAlreadyInjected = false} if(metaPropAlreadyInjected) { obj."$fieldName" = v } else { obj.metaClass."$fieldName" = v } } }
2. 每次都两句一起写:
obj.metaClass."$fieldName" = v obj."$fieldName" = v
可保万无一失。
个人比较喜欢后者,因为它就两行,够简洁。