前两篇文章分别分析了基于Java Agent的premain和attach方式来修改字节码,premain是在类加载前修改,attach是在类加载后修改,本文继续讲字节码的修改,只不过修改的时间是在更早的编译阶段。通过使用插拔式注解处理API(Pluggable Annotation Processing API, JSR 269)可以让我们定义的注解在编译期而非运行期生效,从而达到在编译期修改字节码的目的。当前非常流行的lombok框架就是使用该特性来实现,在项目中我们通过引入lombok的依赖和安装ide插件即可使用其提供的注解大大简化代码的开发,本文通过实现一个Getter注解来说明其工作原理。
如下图所示,本文要实现的Getter注解最终目标就是让这段有"语法错误"的代码能够通过编译并运行,也就是让使用该注解的类能够自动生成get方法。
为了方便测试类的使用,我们将实现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包
<dependencies>
<!--引入系统路径的tools jar包-->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<!--log4j2日志依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
</dependencies>
build部分, 自身项目在编译前并没有Processor的class文件且也不需要用到,因此在编译期要过滤Processor文件并且在打包前再拷回来
<build>
<resources>
<!--编译时过滤掉Processor文件-->
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>META-INF/**/*</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>process-META</id>
<!--打包前再将文件拷贝过来-->
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target/classes</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/resources/</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
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<JCTree.JCVariableDecl> 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<JCTree.JCStatement> 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
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-3level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
以上就是全部代码的实现,然后使用mvn cean install
将该项目提交到本地maven仓库。
然后在测试项目中引入上一步生成的jar包:
<dependencies>
<dependency>
<groupId>com.hebh</groupId>
<artifactId>getterbok-demo</artifactId>
<version>0.0.1</version>
</dependency>
</dependencies>
在测试项目中执行编译命令mvn compile
,从如下打印日志中可以看出我们的Getter注解已经生效了
看看idea反编译.class文件的源码:
可以看到已经生成了getValue方法, 并且已经没有了Getter方法。
在target目录执行运行命令java com.hebh.App
, 顺利打印出字符串。
目标达成。。。