Java自定义注解入门之源码编译阶段

环境

MacBook Pro
Java:1.8
gradle:5.5.1

前言

公司封装了一个@slf4j,用于日志打印;
最近研究了下这个注解,了解到了AST,抽象树这个概念;
明白了lombok这样的插件也是利用这个技术来实现的;
在研究过程中,被下面这个错卡了很久:

错误: 程序包com.sun.source.tree不存在
import com.sun.source.tree.Tree;

Java自定义注解入门之源码编译阶段_第1张图片
特意记录下;

抽象语法树 AST

编程语言有很多,但是基本都是:类型、运算符、函数、对象,流程语句组成的本质;

那该如何表示这些本质性的东西呢?

答案:转化为统一结构;

这个统一的结构不依赖于源语言的语法,只代表源语言中的语法结构,如类型、修饰符、运算符、函数、对象等,
这就是抽象语法树 AST;其是源代码的抽象语法结构的树状表现形式,每一个节点代表一个语法结构。

我自己的理解为:用代码去描述代码的结构

因为有了这么一个统一的结构去描述源代码的结构,
并且JDK官方用Java代码实现了这个结构,
所以我们就可以通过自定义的方式去实现我们的一些想法。

权威参考
《深入理解jvm虚拟机》第三版 第10章 前端编译与优化

简单列子

项目结构

 processor 
		|
		|--src
		|   |--java
		|		|--com
		|			|--hello
		|				|--Person
		|
		|
		|
		|
		|--myprocessor
				|-- src
					 |--main
					 	 |--java
					 	 	 |-- com.processor
					 	 	 		|-- MyGetterProcessor
					 	 	 		|-- MyGetter

创建项目

Java自定义注解入门之源码编译阶段_第2张图片
下一步

Java自定义注解入门之源码编译阶段_第3张图片

再点下一步后,再点击Finish即可。

创建module

这个module是专门用来存放自定义注解和自定义注解处理器的。

先点击项目名,再
Java自定义注解入门之源码编译阶段_第4张图片

Java自定义注解入门之源码编译阶段_第5张图片

下一步后,再点击Finish即可。

自定义注解

myprocessor模块中创建MyGetter注解

package com.processor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author yutao
 * @since 2020/5/9 4:46 下午
 */
@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface MyGetter {
}

创建注解处理器

package com.processor;

import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

/**
 * @author yutao
 * @since 2020/5/9 4:47 下午
 */
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.processor.MyGetter")
public class MyGetterProcessor extends AbstractProcessor {

    private Messager messager; // 编译时期输入日志的
    private JavacTrees javacTrees; // 提供了待处理的抽象语法树
    private TreeMaker treeMaker; // 封装了创建AST节点的一些方法
    private Names names; // 提供了创建标识符的方法

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        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) {
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MyGetter.class);
        elementsAnnotatedWith.forEach(e -> {
            JCTree tree = javacTrees.getTree(e);
            tree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    // 在抽象树中找出所有的变量
                    for (JCTree jcTree : jcClassDecl.defs) {
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    // 对于变量进行生成方法的操作
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
                        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<>();
        // 生成表达式 例如 this.a = a;
        JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(
                names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
        statements.append(aThis);
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

        // 生成入参
        JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER),
                jcVariableDecl.getName(), jcVariableDecl.vartype, null);
        List<JCTree.JCVariableDecl> parameters = List.of(param);

        // 生成返回对象
        JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),
                getNewMethodName(jcVariableDecl.getName()), methodType, List.nil(),
                parameters, List.nil(), block, null);

    }

    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }

    private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
        return treeMaker.Exec(
                treeMaker.Assign(
                        lhs,
                        rhs
                )
        );
    }
}

注册自定义注解处理器

虽然写了自定义处理器,但是怎样才能让jvm认识它呢?
需要在resources目录中添加META-INF.services文件夹,里面创建一个javax.annotation.processing.Processor文件;
javax.annotation.processing.Processor 这个文件里面写自定义注解处理器的全类名称,多个的话,就换行写:
Java自定义注解入门之源码编译阶段_第6张图片

com.processor.MyGetterProcessor

在主项目中添加依赖模块

因为我们自定义注解和处理器是在myprocessor模块中的:
主项目中要使用自定义注解@MyGetter时,得在build.gradle中添加依赖:

compile project('myprocessor')

gradle项目的话,要让编译时,使用自定义注解处理器,还得指定:

annotationProcessor project(':myprocessor')

完整:

plugins {
    id 'java'
}

group 'com.misssad'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile project('myprocessor')
    annotationProcessor project(':myprocessor')
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

使用自定义注解

在主项目中创建一个Person类:
Java自定义注解入门之源码编译阶段_第7张图片

package com.hello;

import com.processor.MyGetter;

/**
 * @author yutao
 * @since 2020/5/9 4:50 下午
 */
@MyGetter
public class Person {

    private String name;
}

编译

在IDEA的右侧,找到gradle,在Tasks - other - compileJava

假设按照上面的步骤来的,不出意外,会报错:

Java自定义注解入门之源码编译阶段_第8张图片

错误: 程序包com.sun.source.tree不存在
import com.sun.source.tree.Tree;

会报上面这样的错误;

为什么会报这个错误呢?

因为在编译的时候,gradle找不到tools.jar

网上有大部分解决方案是:
Java自定义注解入门之源码编译阶段_第9张图片
网上大部分做法是打开Project Settings,然后再Libraries中点击左上角+号,把tools.jar添加上去。
但是我这样做过,不行,没有解决掉我的问题。即依然报错。

gradle编译项目找不到tools.jar问题

解决办法是:

因为注解处理器是写在子模块里面的,所以在myprocessor模块中的build.gradle中添加:

compile files("${System.properties['java.home']}/../lib/tools.jar")

完整的:

plugins {
    id 'java'
}

group 'com.misssad'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
//    implementation fileTree(dir: 'lib', include: ['*.jar'])
    testCompile group: 'junit', name: 'junit', version: '4.12'
    compile files("${System.properties['java.home']}/../lib/tools.jar")
}

再次执行gradle任务中的compileJava任务,即可。

然后在build文件夹中找到相应的class文件,看看效果:

Java自定义注解入门之源码编译阶段_第10张图片
成功了。

参考地址:

Java中实现自定义的注解处理器(Annotation Processor)

99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个

AOP 最后一块拼图 | AST 抽象语法树 —— 最轻量级的AOP方法

Android自定义注解处理器

Gradle找不到tools.jar

你可能感兴趣的:(Java)