前两篇文章分别分析了基于Java Agent的premain和attach方式来修改字节码,premain是在类加载前修改,attach是在类加载后修改,本文继续讲字节码的修改,只不过修改的时间是在更早的编译阶段。通过使用插拔式注解处理API(Pluggable Annotation Processing API, JSR 269)可以让我们定义的注解在编译期而非运行期生效,从而达到在编译期修改字节码的目的。当前非常流行的lombok框架就是使用该特性来实现,在项目中我们通过引入lombok的依赖和安装ide插件即可使用其提供的注解大大简化代码的开发,本文通过实现一个Getter注解来说明其工作原理。

如下图所示,本文要实现的Getter注解最终目标就是让这段有"语法错误"的代码能够通过编译并运行,也就是让使用该注解的类能够自动生成get方法。

image-20190130194202335

为了方便测试类的使用,我们将实现Getter功能的代码写在一个单独的工程并打成jar包并提交到自己本地的maven项目,完整的的项目结构如下:

getterbok
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── hebh
│ ├── Getter.java
│ └── GetterProcessor.java
└── resources
├── META-INF
│ └── services
│ └── javax.annotation.processing.Processor
└── log4j2.xml

相关依赖,除了日志外还要引入java自带的tools包

com.sun
tools
1.8
system
${java.home}/../lib/tools.jar
org.apache.logging.log4j
log4j-api
2.11.1
org.apache.logging.log4j
log4j-core
2.11.1
build部分, 自身项目在编译前并没有Processor的class文件且也不需要用到,因此在编译期要过滤Processor文件并且在打包前再拷回来
src/main/resources
META-INF/**/*
org.apache.maven.plugins
maven-compiler-plugin
3.1
1.8
1.8
org.apache.maven.plugins
maven-resources-plugin
2.6
process-META
prepare-package
copy-resources
target/classes
${basedir}/src/main/resources/
**/*
Getter注解类的定义, 限定其使用范围和生效时期
@Target({ElementType.TYPE}) // 使用在类上
@Retention(RetentionPolicy.SOURCE) //表示这个注解只在编译期起作用
public @interface Getter {
}
继承AbstractProcessor的GetterProcessor类,限定要处理哪些注解和源码级别,该类也是实现功能的核心类,通过重写process方法来对字节码进行修改。
@SupportedAnnotationTypes("com.hebh.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {
private static final Logger logger = LogManager.getLogger(GetterProcessor.class);
private Messager messager;
private JavacTrees trees;
private TreeMaker treeMaker;
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
logger.debug("Enter method init");
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public synchronized boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
if(annotations.size() > 0){
logger.debug("Enter method process, {}, {}", annotations, roundEnv.getRootElements());
}
Set extends Element> set = roundEnv.getElementsAnnotatedWith(Getter.class);
set.forEach(element -> {
JCTree jcTree = trees.getTree(element);
jcTree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
List jcVariableDeclList = List.nil();
for (JCTree tree : jcClassDecl.defs) {
if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
}
}
jcVariableDeclList.forEach(jcVariableDecl -> {
logger.debug( "{} has been processed", jcVariableDecl.getName());
jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
});
return true;
}
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
ListBuffer statements = new ListBuffer<>();
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null);
}
/**
* 获取新方法名,get + 将第一个字母大写 + 后续部分, 例如 value 变为 getValue
* @param name
* @return
*/
private Name getNewMethodName(Name name) {
String s = name.toString();
return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}
}
javax.annotation.processing.Processor,采用Java的SPI(Service Provider Interface)机制,放在META-INF/services文件夹下面, 以接口全路径为名,实现类全路径为内容,而在程序运行时能够动态为接口替换实现类。
com.hebh.GetterProcessor
Log4j2.xml
以上就是全部代码的实现,然后使用mvn cean install将该项目提交到本地maven仓库。
然后在测试项目中引入上一步生成的jar包:
com.hebh
getterbok-demo
0.0.1
在测试项目中执行编译命令mvn compile,从如下打印日志中可以看出我们的Getter注解已经生效了
image-20190130201517269
看看idea反编译.class文件的源码:
image-20190130201944544
可以看到已经生成了getValue方法, 并且已经没有了Getter方法。
在target目录执行运行命令java com.hebh.App, 顺利打印出字符串。
image-20190130202346830

目标达成。。。