如何自己實現lombok功能-Pluggable Annotation Processing的講解與實踐

如何自己實現一個lombok?

lombok具有超級實用簡單的註解,減少了很多代碼的書寫,誰用誰知道。但是具有探索精神的程序員肯定會問他是怎麼實現的?

憑經驗我們知道,其是在編譯階段直接生成了代碼,與運行時是無關的,它的github地址:https://github.com/rzwitserloot/lombok

下面是一個很簡單的基本實現,主要涉及到以下知識點:

  1. Pluggable Annotation Processing
  2. AST(抽象語法樹)
  3. maven插件安裝
  4. javac編譯

項目結構

先來個簡單的測試:

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── jimo
│   │   │           ├── Data.java
│   │   │           ├── Main.java
│   │   │           └── MyProcessor.java

代碼

Data.java

package com.jimo;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
}

Main.java

@Data
public class Main {

    private int age;
    private String name;

    public static void main(String[] args) {
        new Main();
        System.out.println("yes");
    }
}

MyProcessor.java

package com.jimo;

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.Writer;
import java.util.Set;

@SupportedAnnotationTypes(value = {"com.jimo.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("begin");
        for (TypeElement annotation : annotations) {
            if (annotation.getSimpleName().contentEquals("Data")) {
                Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(annotation);
                for (Element element : elementsAnnotatedWith) {
                    String pkg = element.getEnclosingElement().toString();
                    String className = element.getSimpleName().toString();

                    try {
                        rewriteClass(pkg, className);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    System.out.println("1:" + pkg);
                    System.out.println("2:" + className);
                }
            }
            System.out.println("anno:" + annotation);
        }
        System.out.println(roundEnv);
        return true;
    }

    private void rewriteClass(String pkg, String className) throws IOException {
        final JavaFileObject sourceFile = processingEnv.getFiler().createClassFile(pkg + ".NewClass");
//        JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(pkg + ".Test");
//        JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(pkg + "." + className);
        try (Writer writer = sourceFile.openWriter()) {
            writer.write("package " + pkg + ";");
            writer.write("class Main{");
            writer.write("private int age;");
            writer.write("}");
        }
    }
}

如何使用

javac編譯法

需要先編譯處理器,再編譯其他代碼

// 編譯處理器
src\main\java>javac com\jimo\MyProcessor.java
// 帶上處理器
javac -processor com.jimo.MyProcessor com\jimo\Main.java

然後會看到編譯時輸出:

com.jimo.Data
[errorRaised=false, rootElements=[com.jimo.Main], processingOver=false]
[errorRaised=false, rootElements=[], processingOver=true]
警告: 註釋處理不適用於隱式編譯的文件。
  使用 -implicit 指定用於隱式編譯的策略。
1 個警告

maven插件編譯法

在插件中聲明處理器:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <annotationProcessors>
                        <annotationProcessor>
                            com.jimo.MyProcessor
                        </annotationProcessor>
                    </annotationProcessors>
                </configuration>
            </plugin>
        </plugins>
    </build>

同樣,我們也需要先編譯處理器:因爲默認,maven編譯是放在target/classes/com/jimo/xxx.class,所以如下:
(編譯命令參考:javac -d ..\..\..\target\classes\ com\jimo\MyProcessor.java

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── jimo
│   │   │           ├── Data.java
│   │   │           ├── Main.java
│   │   │           └── MyProcessor.java
│   │   └── resources
│   └── test
│       └── java
└── target
    ├── classes
    │   └── com
    │       └── jimo
    │           └── MyProcessor.class

然後使用maven compile命令,或者在IDEA裏調用Lifecycle-->compile按鈕:

結果:

[INFO] Compiling 3 source files to D:\workspace\test-demos\j-lombok-demo\target\classes
com.jimo.Data
[errorRaised=false, rootElements=[com.jimo.MyProcessor, com.jimo.Data, com.jimo.Main], processingOver=false]
[errorRaised=false, rootElements=[], processingOver=true]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

jar包引入

以上2種方式都是在一個倉庫裏使用,僅供開發測試,真正投入使用時,還是用jar包的方式好。

實踐lombok

上面的方法只能生成新的類和class文件,而有時需要修改源代碼和class,這就更復雜一些,需要懂得AST(抽象語法樹)和編譯原理。

安裝tools.jar 到本地倉庫:因爲需要用到裏面的AST代碼

mvn install:install-file -Dfile=JDK目錄\lib\tools.jar -DgroupId=com.sun -DartifactId=tools -Dversion=1.8 -Dpackaging=jar

寫處理器代碼:

package com.jimo;

import com.google.auto.service.AutoService;
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.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

@SupportedAnnotationTypes(value = {"com.jimo.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
    /**
     * AST
     */
    private JavacTrees trees;
    /**
     * 操作修改AST
     */
    private TreeMaker treeMaker;
    /**
     * 符號封裝類,處理名稱
     */
    private Names names;
    /**
     * 打印信息
     */
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        trees = JavacTrees.instance(processingEnv);
        final Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        treeMaker = TreeMaker.instance(context);
        names = Names.instance(context);
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("begin");

        final Set<? extends Element> dataAnnotations = roundEnv.getElementsAnnotatedWith(Data.class);

        dataAnnotations.stream().map(element -> trees.getTree(element)).forEach(
                tree -> tree.accept(new TreeTranslator() {
                    @Override
                    public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
                        // print method name
                        System.out.println("-------------2");
                        messager.printMessage(Diagnostic.Kind.NOTE, jcMethodDecl.toString());
                        super.visitMethodDef(jcMethodDecl);
                    }

                    @Override
                    public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {

                        System.out.println("-------------1");
                        final Map<Name, JCTree.JCVariableDecl> treeMap =
                                jcClassDecl.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE))
                                        .map(tree -> (JCTree.JCVariableDecl) tree)
                                        .collect(Collectors.toMap(JCTree.JCVariableDecl::getName, Function.identity()));

                        treeMap.forEach((k, var) -> {
                            messager.printMessage(Diagnostic.Kind.NOTE, "var:" + k);
                            System.out.println("-------------3");
                            try {
                                // add getter
                                jcClassDecl.defs = jcClassDecl.defs.prepend(getter(var));
                                // add setter
                                jcClassDecl.defs = jcClassDecl.defs.prepend(setter(var));
//                                jcClassDecl.defs.prepend(setter(var));
                            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                                e.printStackTrace();
                                messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
                            }
                        });

                        super.visitClassDef(jcClassDecl);
                    }
                })
        );
        return true;
    }

    /**
     * 自定義setter
     */
    private JCTree setter(JCTree.JCVariableDecl var) throws ClassNotFoundException, IllegalAccessException,
            InstantiationException {
        // 方法級別public
        final JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

        final Name varName = var.getName();
        Name methodName = methodName(varName, "set");

        // 方法體
        ListBuffer<JCTree.JCStatement> jcStatements = new ListBuffer<>();
        jcStatements.append(treeMaker.Exec(treeMaker.Assign(
                treeMaker.Select(treeMaker.Ident(names.fromString("this")), varName),
                treeMaker.Ident(varName)
        )));
        final JCTree.JCBlock block = treeMaker.Block(0, jcStatements.toList());

        // 返回值類型void
        JCTree.JCExpression returnType =
                treeMaker.Type((Type) (Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance()));

        List<JCTree.JCTypeParameter> typeParameters = List.nil();

        // 參數
        final JCTree.JCVariableDecl paramVars = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER,
                List.nil()), var.name, var.vartype, null);
        final List<JCTree.JCVariableDecl> params = List.of(paramVars);

        List<JCTree.JCExpression> throwClauses = List.nil();
        // 重新構造一個方法, 最後一個參數是方法註解的默認值,這裏沒有
        return treeMaker.MethodDef(modifiers, methodName, returnType, typeParameters, params, throwClauses, block,
                null);
    }

    /**
     * 構造駝峯命名
     */
    private Name methodName(Name varName, String prefix) {
        return names.fromString(prefix + varName.toString().substring(0, 1).toUpperCase()
                + varName.toString().substring(1));
    }

    /**
     * 構造getter
     */
    private JCTree getter(JCTree.JCVariableDecl var) {
        // 方法級別
        final JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

        // 方法名稱
        final Name methodName = methodName(var.getName(), "get");

        // 方法內容
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), var.getName())));
        final JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

        // 返回值類型
        final JCTree.JCExpression returnType = var.vartype;

        // 沒有參數類型
        List<JCTree.JCTypeParameter> typeParameters = List.nil();

        // 沒有參數變量
        List<JCTree.JCVariableDecl> params = List.nil();

        // 沒有異常
        List<JCTree.JCExpression> throwClauses = List.nil();

        // 構造getter
        return treeMaker.MethodDef(modifiers, methodName, returnType, typeParameters, params, throwClauses, block,
                null);
    }
}

使用google的auto service進行配置處理器:

引入依賴:

        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc4</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.google.auto</groupId>
            <artifactId>auto-common</artifactId>
            <version>0.10</version>
            <optional>true</optional>
        </dependency>

注意到上面類中的@AutoService註解就是來自這。

這樣編譯完後,會生成META-INF信息:

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── jimo
│   │   │           ├── CustomProcessor.java
│   │   │           ├── Data.java
│   │   └── resources
│   └── test
│       └── java
└── target
    ├── classes
    │   ├── META-INF
    │   │   └── services
    │   │       └── javax.annotation.processing.Processor
    │   └── com
    │       └── jimo
    │           ├── CustomProcessor$1.class
    │           ├── CustomProcessor.class
    │           ├── Data.class

javax.annotation.processing.Processor這個文本文件裏就一句話:

com.jimo.CustomProcessor

接着可以使用mvn install安裝到本地,讓其他項目引入試試。

目標類:

@Data
public class User {
    private int age;
    private String name;
    public int height;
}

編譯完後:

public class User {
    private int age;
    private String name;
    public int height;

    public void setAge(int age) {
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public User() {
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章