跳到主要内容

插入式注解的浅浅探究

· 阅读需 7 分钟

介绍

类似lombok,通过@Data注解,即可将getter/setter/toString等方法注入到编译后的class代码中。

本文即是对它的原理的探究。

编写代码

一、定义@MyData注解

先看一下 lombok 如何定义@Data注解的
lombok.Data
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
String staticConstructor() default "";
}

仿照@Data写一个@MyData注解:

com.one.annotation.MyData
@Target(ElementType.TYPE) // 标注在类上
@Retention(RetentionPolicy.SOURCE) // 仅在源码级别保留
public @interface MyData {
}

二、实现处理器Processor

插入式注解处理器,该处理器是实现了JSR-269提案定义的Pluggable Annotation Processing API实现

模拟功能:生成一个类,类里面有个sayHello方法。(简单了解)

后面实现了一个高级点的处理器。点此跳转🚩

添加类MyDataProcessor
com.one.process.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

四、使用注解

com.one.domain.User
@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开头:

例如:

  1. @RequestMapping("/user") ==> @RequestMapping({"app/user","/user"})

  2. @RequestMapping({"/client","/user"}) ==> @RequestMapping({"app/client","/client","app/user","/user"})

1.需要引入jdk的tools.jar

操作AST,需要java提供的AST工具类: JavacTreesTreeMakerNames

pom.xml
<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
com.one.process.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开发,大概知道一些门道,知道能怎么操作一些类就够了。