前言
在看本文之前首先应该先阅读hongyang的Android 如何编写基于编译时注解的项目这一篇文章。我是在看了这篇文章后突发奇想:能不能直接修改Java类中的代码而不去生成新的类呢?然后就开始自行寻找解决方案,但是无奈,网上几乎没有关于这个问题的解决方案,我个人弄了很久,可能现在总结得也不全,写出来希望能够帮助到有相同需求的人,也希望有经验的朋友提出不同的解决方案。
思路
编译时修改代码,Java有一个JSR269的API标准,只要按照这个API来实现就可以在编译时进行代码修改。这里有一篇文章对于JSR269进行了比较详细的介绍:http://developer.51cto.com/art/201305/392858.htm,在这篇文章顶部有一个连接到英文版本的原文,都可以详细看看。
开始
现在在查看上面其他的文章之后我们就可以开始进行编码了。首先需要引入一个jar包,就是在你的jdk目录下面那个tools.jar文件,引入到项目中,就是hongyang项目中的ioc-compiler这一个项目里面。然后再开始编写Processor。
annotation
我这里定义了一个不带任何参数的注解
@Retention(RetentionPolicy.CLASS)
public @interface AntiBean {
}
processor
继承于AbstractProcessor
init
需要用到几个对象:
private Trees trees;
private TreeMaker make;
private Name.Table names;
private Context context;
然后再processor的init方法中进行初始化
trees = Trees.instance( processingEnv );
context = ((JavacProcessingEnvironment)
processingEnv).getContext();
make = TreeMaker.instance( context );
names = Names.instance( context ).table;
这里Trees.instance( processingEnv )中间参数可能会被标记红色的下划线,但是并不影响编译和运行,下面names的初始化方法可能是上面51cto那个文章里面的,也可能是我这儿这种写法的,但是最终结果都不影响,具体要看你自己引用的那个tools.jar的版本了,我这儿是1.8的版本。
process
在process方法中判断注解是否是AntiBean,然后再遍历出来注解过的对象:
if (!roundEnv.processingOver()) {
Set<? extends Element> elements =
roundEnv.getElementsAnnotatedWith( AntiBean.class );
for (Element each : elements) {
if (each.getKind() == ElementKind.CLASS) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.NOTE, " assertions inlined class " + ((TypeElement) each).getQualifiedName().toString() );
JCTree tree = (JCTree) trees.getTree( each );
TreeTranslator visitor = new Inliner();
tree.accept( visitor );
}
}
}
这里的trees.getTree( each )下面也可能会被标红,但是也并不需要管他。
上面这些都不是主要的,这些只是取得了被注解的类的JCTree对象,主要操作在上面那个Inliner里面。
Inliner
继承于TreeTranslator,TreeTranslator继承于Visitor,顾名思义,Visitor就是访问的意思,只要继承于Visitor就可以访问JCTree中的节点,TreeTranslator就是用于访问JCTree中的节点并且修改节点。
private class Inliner extends TreeTranslator {
//这个方法会被循环调用,也就是说如果类中有两个方法,那么会分别调用两次这个方法来传入两个不同的方法进来,是按照先后顺序调用的,如果两个方法中间有成员变量的声明,那么会先去调用visitMethodDef(传入方法1),再去调用visitVarDef然后再回来掉用一次visitMethodDef(传入方法2)
@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
super.visitMethodDef( jcMethodDecl );
//如果方法名叫做getUserName则把它的名字修改成testMethod
if (jcMethodDecl.getName().toString().equals( "getUserName" )) {
JCTree.JCMethodDecl methodDecl = make.MethodDef( jcMethodDecl.getModifiers(), names.fromString( "testMethod" ), jcMethodDecl.restype, jcMethodDecl.getTypeParameters(), jcMethodDecl.getParameters(), jcMethodDecl.getThrows(), jcMethodDecl.getBody(), jcMethodDecl.defaultValue );
result = methodDecl;
}
}
}
完整的Processor代码
@AutoService(Processor.class)
public class AntiBeanProcessor extends AbstractProcessor {
private Trees trees;
private TreeMaker make;
private Name.Table names;
private Context context;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init( processingEnv );
trees = Trees.instance( processingEnv );
context = ((JavacProcessingEnvironment)
processingEnv).getContext();
make = TreeMaker.instance( context );
names = Names.instance( context ).table;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add( AntiBean.class.getCanonicalName() );
return supportTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
Set<? extends Element> elements =
roundEnv.getElementsAnnotatedWith( AntiBean.class );
for (Element each : elements) {
if (each.getKind() == ElementKind.CLASS) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.NOTE, " assertions inlined class " + ((TypeElement) each).getQualifiedName().toString() );
JCTree tree = (JCTree) trees.getTree( each );
TreeTranslator visitor = new Inliner();
tree.accept( visitor );
}
}
} else
processingEnv.getMessager().printMessage(
Diagnostic.Kind.NOTE, " assertions inlined." );
return false;
}
private class Inliner extends TreeTranslator {
@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
super.visitMethodDef( jcMethodDecl );
if (jcMethodDecl.getName().toString().equals( "getUserName" )) {
JCTree.JCMethodDecl methodDecl = make.MethodDef( jcMethodDecl.getModifiers(), names.fromString( "testMethod" ), jcMethodDecl.restype, jcMethodDecl.getTypeParameters(), jcMethodDecl.getParameters(), jcMethodDecl.getThrows(), jcMethodDecl.getBody(), jcMethodDecl.defaultValue );
result = methodDecl;
}
}
}
}
总结
我本来是想要做到我只写JavaBean中的成员变量,然后使用注解之后在编译时自动把get,set方法加上去的,但是做到最后我才发现,就算是修改到了类中的方法,编译成class之后是被修改过后的,但是IDE在编写代码的时候是不能够解析到那个生成出来的方法的,换句话说就是点不出来的,因为IDE是直接解析的源代码(是.java文件,不是.class文件),也就是为什么hongyang在做的时候是生成了一个新的类,再用一个通用的接口来调用到那一个代理类的原因。而且我也在访问到成员变量声明之后尝试着做生成新的方法添加到类的代码中,但是由于之前写的网上几乎没有文档的原因,我没能够做到,我只做到了修改,没能够做到新增方法。就在刚才我也找到了一篇说这个问题的网页:How do I use Java Compiler Tree API (com.sun.source.util) to reconstruct source code,这里面提到的说法是AST不一定能够实现新增方法在代码里面,并提出了一个叫做PTS的东西,但是这个我也大概搜索了一下,基本上没找到关于这个的任何资料。
还有就是我上面说到的那个JavaBean的那一个生成,是有一个很完善的插件使用的,而且几乎主流的IDE都是配备了相应的插件的:LOMBOK。它主要处理的代码在这儿HandleSetter.java,但是它项目整个太大了,我确实看不太懂了,没能够仿照着它的模式自己写一个出来。如果有兴趣的可以去看看。最后如果有哪位朋友实现过这个功能的,还请多多赐教啊!