插入式注解的浅浅探究
介绍
类似lombok,通过@Data注解,即可将getter/setter/toString等方法注入到编译后的class代码中。
本文即是对它的原理的探究。
编写代码
一、定义@MyData
注解
@Data
注解的@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
String staticConstructor() default "";
}
仿照@Data
写一个@MyData
注解:
@Target(ElementType.TYPE) // 标注在类上
@Retention(RetentionPolicy.SOURCE) // 仅在源码级别保留
public @interface MyData {
}
二、实现处理器Processor
插入式注解处理器,该处理器是实现了JSR-269提案定义的Pluggable Annotation Processing API实现
模拟功能:生成一个类,类里面有个sayHello方法。(简单了解)
后面实现了一个高级点的处理器。点此跳转🚩
添加类MyDataProcessor
package com.one.process;
import com.one.annotation.MyData;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;
@SupportedAnnotationTypes("com.one.annotation.MyData") // 处理的注解类型
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 支持的 Java 版本
public class MyDataProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(MyData.class)) {
if (element instanceof TypeElement) {
TypeElement typeElement = (TypeElement) element;
String className = typeElement.getSimpleName().toString();
String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
// 生成代码
generateCode(className, packageName);
}
}
return true;
}
private void generateCode(String className, String packageName) {
try {
// 创建新的 Java 文件
JavaFileObject file = processingEnv.getFiler().createSourceFile(packageName + "." + className + "Generated");
try (PrintWriter writer = new PrintWriter(file.openWriter())) {
// 写入生成的代码
writer.println("package " + packageName + ";");
writer.println();
writer.println("public class " + className + "Generated {");
writer.println(" public String sayHello() {");
writer.println(" return \"Hello\";");
writer.println(" }");
writer.println("}");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、注册注解处理器(SPI)
注解处理器需要SPI服务发现机制
在src/main/resources/META-INF/services/javax.annotation.processing.Processor
内编写:
com.one.process.MyDataProcessor
四、使用注解
@MyData
public class User {
private String name;
private int age;
}
五、编译并查看生成代码
可以使用CTRL+F9,构建代码,在target/classes
中找到User.class
同目录中有一个UserGenerated.class
target/
六、生成后的代码可以直接使用。
target/
拓展
刚才那个例子很简答,获取类的信息,添加了一个类,用字符串形式写入。
我们使用AST的特性来写一些东西。基本可以完成任何功能。
功能,使用在Controller类上,拓展@RequestMapping注解中的value,每个元素都加上app开头:
例如:
@RequestMapping("/user")
==>@RequestMapping({"app/user","/user"})
@RequestMapping({"/client","/user"})
==>@RequestMapping({"app/client","/client","app/user","/user"})
1.需要引入jdk的tools.jar
操作AST,需要java提供的AST工具类:
JavacTrees
、TreeMaker
、Names
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
2.代码
添加类MyDataProcessor
package com.one.process;
import com.one.annotation.MyData;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Names;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Function;
@SupportedAnnotationTypes("com.one.annotation.MyData") // 处理的注解类型
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 支持的 Java 版本
public class MyDataProcessor extends AbstractProcessor {
private JavacTrees javacTrees;
private TreeMaker treeMaker;
private Names names;
// 解包工具方法
private static <T> T jbUnwrap(Class<? extends T> iface, T wrapper) {
T unwrapped = null;
try {
final Class<?> apiWrappers = wrapper.getClass().getClassLoader()
.loadClass("org.jetbrains.jps.javac.APIWrappers");
final Method unwrapMethod = apiWrappers.getDeclaredMethod("unwrap", Class.class, Object.class);
unwrapped = iface.cast(unwrapMethod.invoke(null, iface, wrapper));
} catch (Throwable ignored) {}
return unwrapped != null ? unwrapped : wrapper;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
// 解包 ProcessingEnvironment
processingEnv = jbUnwrap(ProcessingEnvironment.class, processingEnv);
super.init(processingEnv);
this.javacTrees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(MyData.class)) {
if (element.getKind() == ElementKind.CLASS) {
// 处理类上的 @RequestMapping 注解
processRequestMapping(element);
}
}
return true;
}
/**
* 判断类路径是否和指定类相同
* @param classPath 类路径
* @param clazz 类
* @return
*/
private boolean isAnnotation(String classPath,Class<?> clazz) {
String[] paths = {clazz.getName(),clazz.getSimpleName()};
for (String path : paths) {
if (classPath.equals(path)) {
return true;
}
}
return false;
}
/**
* 修改类上的 @RequestMapping 注解的 value 值
*/
private void processRequestMapping(Element classElement) {
// 获取类上的 @RequestMapping 注解
RequestMapping annotation = classElement.getAnnotation(RequestMapping.class);
if (annotation == null) {
return;// 缺少注解
}
// 提取原始路径数组
String[] rmValues = annotation.value();
ListBuffer<String> newPaths = new ListBuffer<>();
// 生成新的路径数组(原路径 , app/前缀路径)
for (String rmValue : rmValues) {
newPaths.add("app" + (rmValue.startsWith("/") ? rmValue : "/" + rmValue));
newPaths.add(rmValue);
}
// 通过 AST 操作替换原注解(此处需结合 TreeMaker 等底层 API)
// 获取classElement的RequestMapping的注解,然后将value值替换为newPaths
JCTree.JCClassDecl jcc = (JCTree.JCClassDecl) javacTrees.getTree(classElement);
// 获取注解去
JCTree.JCModifiers modifiers = jcc.getModifiers();
List<JCAnnotation> annotations = modifiers.getAnnotations();
JCAnnotation newRequestMapping = null;
for (int i = 0; i < annotations.size(); i++) {
JCAnnotation anno = annotations.get(i);
// 处理RequestMapping注解
if (isAnnotation(anno.getAnnotationType().toString(), RequestMapping.class)) {
// 获取注解参数
List<JCTree.JCExpression> arguments = anno.getArguments();
JCExpression[] newJcExpressions = new JCExpression[arguments.size()];
// 修改参数值
for (int j = 0; j < arguments.size(); j++) {
// 属性
JCTree.JCExpression argument = arguments.get(j);
JCAssign jcAssign = (JCAssign) argument;
// 替换 value 参数值,其他值不变
if (jcAssign.lhs.toString().equals("value")) {// 默认为value
newJcExpressions[j] = createNewValueArray(newPaths.toArray(new String[0]));
} else {
newJcExpressions[j] = argument;
}
}
// 替换旧参数
newRequestMapping = treeMaker.Annotation(
treeMaker.Ident(names.fromString("RequestMapping" )),
com.sun.tools.javac.util.List.from(newJcExpressions)
);
}
}
if (newRequestMapping != null) {
// 遍历原始列表,跳过不需要的元素
List<JCAnnotation> newList = List.nil();
for (JCAnnotation anno : annotations) {
if (isAnnotation(anno.getAnnotationType().toString(), RequestMapping.class)) {
// 替换旧的RequestMapping注解
newList = newList.append(newRequestMapping);
continue;
}
newList = newList.append(anno);
}
// 赋值给注解
jcc.mods.annotations = newList;
}
}
private JCExpression createNewValueArray(String[] newPaths) {
List<JCTree.JCExpression> newElements = List.nil();
for (String path : newPaths) {
newElements = newElements.append(treeMaker.Literal(path));
}
// 创建左侧值(value)
JCExpression lhs = treeMaker.Ident(names.fromString("value"));
// 创建右侧值({"/user", "app/user"})
JCExpression rhs = treeMaker.NewArray(
null,
List.nil(), // 维度表达式(为空)
newElements // 数组元素
);
return treeMaker.Assign(lhs, rhs);
}
public void print(Object s){
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"看看:"+s);
}
}
🛂调试问题
因为是编译阶段执行的代码,无法debug,只能通过这种方式打印日志,步步调整。
// 输出日志(调试用)
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"合并后的路径: " + newPaths + " @ " + classElement.getSimpleName());
🛂有个问题,就是单模块是无法使用的。
细说:就是得编译的时候,扫描SPI的时候,得保证MyDataProcessor
是在jar包中的。
那么至少得是多模块时才能使用。
建议是把注解、注解处理器封装在一个模块内,哪里使用,哪里写SPI。或者再把SPI封装一个包,引用上个模块,这样直接使用注解就行了。
总结
一些念叨
怎么说好呢,看了lombok的源码我一脸懵,我就一个疑问,为什么可以不用引用tools.jar,lombok就可以生效啊!?
问了一下AI,它回答我:
运行时加载:Lombok 通过
ShadowClassLoader
动态加载 tools.jar 的类,避免直接暴露给用户代码。
好!即便你能动态加载,那源码中的类呢?
本地看,其中有些类是以SCL的形式存在。
最后也实在难以懂,就到这吧,不过也实际体验了一下javac的AST开发,大概知道一些门道,知道能怎么操作一些类就够了。