Grails会在编译期往domain class中注入id和version两个属性(在编译后的class文件中可以发现它们)。它是怎么做到的呢?
搜索Grails的源代码,有如下发现:
1. 找到一个叫做DefaultGrailsDomainClassInjector的类,其中有
public void performInjection(SourceUnit source, GeneratorContext context,
ClassNode classNode) {
if (isDomainClass(classNode, source) && shouldInjectClass(classNode)) {
performInjectionOnAnnotatedEntity(classNode);
}
}
public void performInjectionOnAnnotatedEntity(ClassNode classNode) {
injectIdProperty(classNode);
injectVersionProperty(classNode);
injectToStringMethod(classNode);
injectAssociations(classNode);
}
// ...
2. 找到java类GlobalEntityASTTransformation,其中有这么两句:
GrailsDomainClassInjector domainInjector = new DefaultGrailsDomainClassInjector();
domainInjector.performInjectionOnAnnotatedEntity(cNode);
3. 文件META-INF/services/org.codehaus.groovy.transform.ASTTransformation中有
org.codehaus.groovy.grails.compiler.injection.GlobalEntityASTTransformation
4. 进一步搜索Groovy的源代码,类ASTTransformationVisitor中有
Enumeration<URL> globalServices = transformLoader.getResources("META-INF/services/org.codehaus.groovy.transform.ASTTransformation");
while (globalServices.hasMoreElements()) {
// ...
}
貌似找到了答案:Grails在执行groovyc的时候,从 META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件中读取到org.codehaus.groovy.grails.compiler.injection.GlobalEntityASTTransformation -> 调用它的visit方法 -> 调用DefaultGrailsDomainClassInjector的performInjection方法,实现domain class的id和version属性注入。如果这个假设成立,那么我们只要修改DefaultGrailsDomainClassInjector类的performInjectionOnAnnotatedEntity方法,就可以注入更多的属性,比如
public void performInjectionOnAnnotatedEntity(ClassNode classNode) {
// ...
// added by S.C. on 26May2010
injectDeletedProperty(classNode);
injectDateCreatedProperty(classNode);
injectHistoryProperty(classNode);
// ...
}
应该可以注入deleted, dateCreated, history这三个属性。
权且试试,把编译后得到的类拷贝进%GRAILS_HOME%/dist/grails-core-1.2.2.jar,覆盖之。执行grails clean, grails run-app...结果很令人沮丧,domain class中deleted, dateCreated, history这三个属性一个都没有。(期间还碰到一个问题,让我现在依然摸不着头脑:执行grails war,得到的war中WEB-INF/lib下的grails-core-1.2.2.jar竟然还是未被修改过的!它是从哪里来的呢?dist下的确定已经修改成功了,源代码也改了,甚至网线也断掉了,不可能从dist拷贝过来,也不是从src编译过来然后打的jar包,也不可能从grails网站下载 -- 它到底是从哪来的呢?????? 这个问题权且放一放。)
结论:假设不成立。
怎么才能让domain class编译得到的字节码中含有我需要的deleted, dateCreated, history属性呢?(我不想在写代码的时候往每个domain class中都写上这三个属性,这样会让我感觉很不爽。我只想利用groovy的AST编译期注入)
纠结了几天,今天终于另辟蹊径,实现了这个需求。上代码(注:以下部分代码由grails的源代码片段修改而成):
package com.grs.ast;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@GroovyASTTransformationClass("com.grs.ast.GrsDomainTransformation")
public @interface Auditable {
}
package com.grs.ast;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.grails.commons.GrailsDomainClassProperty;
import org.codehaus.groovy.grails.compiler.injection.GrailsASTUtils;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class GrsDomainTransformation implements ASTTransformation {
public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
for (ASTNode node : nodes) {
if (!(node instanceof ClassNode)) {
continue;
}
try {
transform((ClassNode) node);
}
catch(Exception exception){
}
break;
}
}
private void transform(ClassNode classNode) {
injectDeletedProperty(classNode);
injectDateCreatedProperty(classNode);
injectHistoryProperty(classNode);
}
private void injectDeletedProperty(ClassNode classNode) {
final boolean hasDeleted = GrailsASTUtils.hasOrInheritsProperty(classNode, GrsDomainClassProperty.DELETED);
if (!hasDeleted) {
classNode.addProperty(GrsDomainClassProperty.DELETED, Modifier.PUBLIC, new ClassNode(Boolean.class), null, null, null);
}
}
private void injectDateCreatedProperty(ClassNode classNode) {
final boolean hasDateCreated = GrailsASTUtils.hasOrInheritsProperty(classNode, GrailsDomainClassProperty.DATE_CREATED);
if (!hasDateCreated) {
classNode.addProperty(GrailsDomainClassProperty.DATE_CREATED, Modifier.PUBLIC, new ClassNode(java.util.Date.class), null, null, null);
}
}
private void injectHistoryProperty(ClassNode classNode) {
final boolean hasHistory = GrailsASTUtils.hasOrInheritsProperty(classNode, GrsDomainClassProperty.HISTORY);
if (!hasHistory) {
classNode.addProperty(GrsDomainClassProperty.HISTORY, Modifier.PUBLIC, new ClassNode(ArrayList.class), null, null, null);
}
}
}
package com.grs.timesheet
import com.grs.ast.Auditable
@Auditable
class Project {
String name
String description
static constraints = {
}
}
在IntelliJ中查看target/classes/com/grs/timesheet/Project.class,看到
// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available
package com.grs.timesheet;
@com.grs.ast.Auditable
public static class Project implements groovy.lang.GroovyObject{
private java.lang.String name;
private java.lang.String description;
private static java.lang.Object constraints;
java.lang.Long id;
java.lang.Long version;
java.lang.Boolean deleted;
java.util.Date dateCreated;
java.util.ArrayList history;
public Project() { /* compiled code */ }
public java.lang.String toString() { /* compiled code */ }
public java.lang.String getName() { /* compiled code */ }
public void setName(java.lang.String s) { /* compiled code */ }
public java.lang.String getDescription() { /* compiled code */ }
public void setDescription(java.lang.String s) { /* compiled code */ }
public static java.lang.Object getConstraints() { /* compiled code */ }
public static void setConstraints(java.lang.Object o) { /* compiled code */ }
public java.lang.Long getId() { /* compiled code */ }
public void setId(java.lang.Long aLong) { /* compiled code */ }
public java.lang.Long getVersion() { /* compiled code */ }
public void setVersion(java.lang.Long aLong) { /* compiled code */ }
public java.lang.Boolean getDeleted() { /* compiled code */ }
public void setDeleted(java.lang.Boolean aBoolean) { /* compiled code */ }
public java.util.Date getDateCreated() { /* compiled code */ }
public void setDateCreated(java.util.Date date) { /* compiled code */ }
public java.util.ArrayList getHistory() { /* compiled code */ }
public void setHistory(java.util.ArrayList arrayList) { /* compiled code */ }
}
deleted, dateCreated,history这三个属性已经成功注入,同时对应的getter/setter(属于boilerplate code)也有了。
Cheers!