在开发中我们常常会用到类似 lombok 、mapstruct 或者 mybatisplus 的框架,只要加入几个注解即可生成对应的方法,既然被很多框架使用,了解其中的原理还是非常有必要的。
基于 JSR 269(Pluggable Annotation Processing API)规范,提供插入式注解处理接口,Java 6 开始支持,它的主要功能是在 Java 编译期对源码进行处理, 通过这些规范插件,可以读取、修改、添加抽象语法树中的任意元素。
如上图 Javac 在编器期间,如果使用注解处理器对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直至处理完成,再对语法树进行修改。
创建一个注解处理器分为如下几步:
在我们日常开发中,如果需要做一些埋点,AOP 并非唯一选择,APT 在有些场景下也可以使用的,支持静态方法和私有方法,同时稳定性也比较好,覆盖的场景比较全。
1 APT(Annotation Processing Tool )注解处理器 2 javac api处理AST(抽象语法树)
大致原理如下图所示:
如想具体分析 lombok 的实现,可以从 Processor 和AnnotationProcessor 这两个类的 process 方法入手,通过 lombok.javac.JavacAnnotationHandler 处理器找到对应的注解实现。
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
public @interface Data {
}
该 Data 注解只能在编译期的时候获取到,在运行期是无法获取到的。
通过实现Processor 接口可以自定义注解处理器,这里我们采用更简单的方法通过继承AbstractProcessor 类实现自定义注解处理器, 实现抽象方法 process 处理我们想要的功能。
@SupportedAnnotationTypes({"com.nicky.lombok.annotation.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DataProcessor extends AbstractProcessor {@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {}@Overridepublic boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {}
}
@SupportedAnnotationTypes 注解表示哪些注解需要注解处理器处理,可以多个注解校验 @SupportedSourceVersion 注解 用于指定jdk使用版本
如果不使用注解也可以在重写父类方法
Set getSupportedAnnotationTypes()
SourceVersion getSupportedSourceVersion
...
主要是用于初始化上下文等信息
具体处理注解的业务方法
/*** 抽象语法树*/private JavacTrees trees;/*** AST*/private TreeMaker treeMaker;/*** 标识符*/private Names names;/*** 日志处理*/private Messager messager;private Filer filer;public synchronized void init(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment);this.trees = JavacTrees.instance(processingEnv);Context context = ((JavacProcessingEnvironment)processingEnv).getContext();this.treeMaker = TreeMaker.instance(context);messager = processingEnvironment.getMessager();this.names = Names.instance(context);filer = processingEnvironment.getFiler();}
基本成员变量说明:
注: 使用AST语法需要使用本地包 tools.jar 包
com.sun tools 1.8 system ${java.home}/../lib/tools.jar
@Overridepublic boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {Set extends Element> annotation = roundEnv.getElementsAnnotatedWith(Data.class);annotation.stream().map(element -> trees.getTree(element)).forEach(tree -> tree.accept(new TreeTranslator() {@Overridepublic void visitClassDef(JCClassDecl jcClass) {//过滤属性Map treeMap =jcClass.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE)).map(tree -> (JCVariableDecl)tree).collect(Collectors.toMap(JCVariableDecl::getName, Function.identity()));//处理变量treeMap.forEach((k, jcVariable) -> {messager.printMessage(Diagnostic.Kind.NOTE, String.format("fields:%s", k));try {//增加get方法jcClass.defs = jcClass.defs.prepend(generateGetterMethod(jcVariable));//增加set方法jcClass.defs = jcClass.defs.prepend(generateSetterMethod(jcVariable));} catch (Exception e) {messager.printMessage(Diagnostic.Kind.ERROR, Throwables.getStackTraceAsString(e));}});//增加toString方法jcClass.defs = jcClass.defs.prepend(generateToStringBuilderMethod());super.visitClassDef(jcClass);}@Overridepublic void visitMethodDef(JCMethodDecl jcMethod) {//打印所有方法messager.printMessage(Diagnostic.Kind.NOTE, jcMethod.toString());//修改方法if ("getTest".equals(jcMethod.getName().toString())) {result = treeMaker.MethodDef(jcMethod.getModifiers(), getNameFromString("testMethod"), jcMethod.restype,jcMethod.getTypeParameters(), jcMethod.getParameters(), jcMethod.getThrows(),jcMethod.getBody(), jcMethod.defaultValue);}super.visitMethodDef(jcMethod);}}));return true;}
上面逻辑分别实现了getter方法 setter方法 toString方法
大致逻辑:
1 过滤包含Data的 Element 变量 2 根据 Element 获取AST语法树 3 创建语法翻译器重写 visitClassDef 和 visitMethodDef 方法 4 过滤变量生成 get方法 set方法 和 toString方法
private JCMethodDecl generateGetterMethod(JCVariableDecl jcVariable) {//修改方法级别JCModifiers jcModifiers = treeMaker.Modifiers(Flags.PUBLIC);//添加方法名称Name methodName = handleMethodSignature(jcVariable.getName(), "get");//添加方法内容ListBuffer jcStatements = new ListBuffer<>();jcStatements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(getNameFromString("this")), jcVariable.getName())));JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());//添加返回值类型JCExpression returnType = jcVariable.vartype;//参数类型List typeParameters = List.nil();//参数变量List parameters = List.nil();//声明异常List throwsClauses = List.nil();//构建方法return treeMaker.MethodDef(jcModifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null);}
private JCMethodDecl generateSetterMethod(JCVariableDecl jcVariable) throws ReflectiveOperationException {//修改方法级别JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);//添加方法名称Name variableName = jcVariable.getName();Name methodName = handleMethodSignature(variableName, "set");//设置方法体ListBuffer jcStatements = new ListBuffer<>();jcStatements.append(treeMaker.Exec(treeMaker.Assign(treeMaker.Select(treeMaker.Ident(getNameFromString("this")), variableName),treeMaker.Ident(variableName))));//定义方法体JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());//添加返回值类型JCExpression returnType =treeMaker.Type((Type)(Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance()));List typeParameters = List.nil();//定义参数JCVariableDecl variableDecl = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariable.name, jcVariable.vartype, null);List parameters = List.of(variableDecl);//声明异常List throwsClauses = List.nil();return treeMaker.MethodDef(modifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null);
}
private JCMethodDecl generateToStringBuilderMethod() {//修改方法级别JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);//添加方法名称Name methodName = getNameFromString("toString");//设置调用方法函数类型和调用函数JCExpressionStatement statement = treeMaker.Exec(treeMaker.Apply(List.of(memberAccess("java.lang.Object")),memberAccess("com.nicky.lombok.adapter.AdapterFactory.builderStyleAdapter"),List.of(treeMaker.Ident(getNameFromString("this")))));ListBuffer jcStatements = new ListBuffer<>();jcStatements.append(treeMaker.Return(statement.getExpression()));//设置方法体JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());//添加返回值类型JCExpression returnType = memberAccess("java.lang.String");//参数类型List typeParameters = List.nil();//参数变量List parameters = List.nil();//声明异常List throwsClauses = List.nil();return treeMaker.MethodDef(modifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null);}
private JCExpression memberAccess(String components) {String[] componentArray = components.split("\\.");JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0]));for (int i = 1; i < componentArray.length; i++) {expr = treeMaker.Select(expr, getNameFromString(componentArray[i]));}return expr;}private Name handleMethodSignature(Name name, String prefix) {return names.fromString(prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, name.toString()));}private Name getNameFromString(String s) {return names.fromString(s);}
最后是通过 SPI 的方式加载注解处理器,spi 可以用 java 自带的方式,具体用法可以参考我的文章:框架基础之SPI机制 , 这里我们使用 google 封装的 auto-service 框架来实现。
在pom文件中引入
com.google.auto.service auto-service 1.0-rc4 true
com.google.auto auto-common 0.10 true
然后在添加AutoService注解
@SupportedAnnotationTypes({"com.nicky.lombok.annotation.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class DataProcessor extends AbstractProcessor {
}
最后就是 mvn clean install打包到本地仓库作为一个公共包
[INFO] Installing /Users/chenxing/Documents/sourcecode/id-generator-spring-boot-starter/lombok-enchance/target/java-feature.jar to /Users/chenxing/m2repository/com/nicky/lombok-enchance/1.0.4/lombok-enchance-1.0.4.jar
[INFO] Installing /Users/chenxing/Documents/sourcecode/id-generator-spring-boot-starter/lombok-enchance/pom.xml to /Users/chenxing/m2repository/com/nicky/lombok-enchance/1.0.4/lombok-enchance-1.0.4.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.372 s
[INFO] Finished at: 2022-09-03T10:44:27+08:00
[INFO] ------------------------------------------------------------------------
➜ lombok-enchance git:(master) ✗
我们测试下,我们的注解处理器是否按所想的那样,实现了相应功能。
在项目中引入本地依赖 例如我的仓库依赖坐标:
com.nicky lombok-enchance 1.0.4
给LombokTest 类添加 @Data 注解
@Data
public class LombokTest {private String name;private int age;public LombokTest(String name) {this.name = name;}public static void main(String[] args) {LombokTest lombokTest = new LombokTest("nicky");lombokTest.age = 18;System.out.println(lombokTest.toString());}
}
我们编译上面的类,查看 class文件是否生成了getField() setField() toString()方法
public class LombokTest {private java.lang.String name;private int age;public java.lang.String toString() { /* compiled code */ }public void setName(java.lang.String name) { /* compiled code */ }public java.lang.String getName() { /* compiled code */ }public void setAge(int age) { /* compiled code */ }public int getAge() { /* compiled code */ }public LombokTest(java.lang.String name) { /* compiled code */ }public static void main(java.lang.String[] args) { /* compiled code */ }
}
成功啦 😁
最后测试下main方法
打印结果如下:
{“name”:“清水”,“age”:18}
说明toString方法生效了。
当然对于 get 和 set 方法 直接在IDE工具里还是无法调用的,需要编写 IDE 的 Lombok 插件,这里就不去扩展了。
Reference
以上就是APT 注解处理器实现 Lombok 常用注解功能详解的详细内容,更多关于APT 实现Lombok注解功能的资料请点击获取《Android核心技术笔记》或查看主页其它相关文章!
spi全称为 (Service Provider Interface),是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制,一种解耦非常优秀的思想。
spi的工作原理: 就是ClassPath路径下的META-INF/services文件夹中, 以接口的全限定名来命名文件名,
文件里面写该接口的实现。然后再资源加载的方式,读取文件的内容(接口实现的全限定名), 然后再去加载类。
spi可以很灵活的让接口和实现分离, 让api提供者只提供接口, 第三方来实现。