首頁 > 軟體

利用Java手寫一個簡易的lombok的範例程式碼

2022-10-16 14:00:19

1.概述

在物件導向程式設計中,必不可少的需要在程式碼中定義物件模型,而在基於Java的業務平臺開發實踐中尤其如此。相信大家在平時開發中也深有感觸,本來是沒有多少程式碼開發量的,但是因為定義的業務模型物件比較多,而需要重複寫Getter/Setter、構造器方法、字串輸出的ToString方法、Equals/HashCode方法等。我們都知道Lombok能夠替大家完成這些繁瑣的操作,但是其背後的原理很少有人會關注或者說得清,本文會帶著大家瞭解這一開發神器內部的執行機制與原理!

Lombok是一款Java開發外掛,使得Java開發者可以通過其定義的一系列註解來消除業務工程中冗長和繁瑣的程式碼,尤其對於簡單的Java模型物件(POJO)。在開發環境中使用Lombok外掛後,Java開發人員可以節省出重複構建,諸如HashCode和Equals這樣的方法以及各種業務物件模型的accessor和ToString等方法的大量時間。對於這些方法,它能夠在編譯原始碼期間自動幫我們生成這些方法,且並不會如反射那樣降低程式的效能。主要是這樣比較靈活,即使你在實體類中新增了屬性,也不用重新回過頭來維護該實體的set和get方法等。

2.lombok使用方法

安裝外掛,在編譯類路徑中加入lombok.jar包(具體安裝方法可自己百度);

在需要簡化的類或方法上,加上要使用的註解;

使用支援lombok的編譯工具編譯原始碼(關於支援lombok的編譯工具,見4.支援lombok的編譯工具);

編譯得到的位元組碼檔案中自動生成Lombok註解對應的方法或程式碼;

3.lombok原理解析

接下來,我們進行lombok的原理分析,以Oracle的javac編譯工具為例。自Java 6起,javac開始支援JSR 269 Pluggable Annotation Processing API規範,只要程式實現了該API,就能在java原始碼編譯時呼叫定義的註解。舉例來說,現在有一個實現了"JSR 269 API"的程式A,那麼使用javac編譯原始碼的時候具體流程如下:

javac對原始碼進行分析,生成一棵抽象語法樹(AST);

執行過程中呼叫實現了"JSR 269 API"的A程式;

此時A程式就可以完成它自己的邏輯,包括修改第一步驟得到的抽象語法樹(AST);

javac使用修改後的抽象語法樹(AST)生成位元組碼檔案;

詳細的流程圖如下:

從上面的Lombok執行的流程圖中可以看出,在Javac 解析成AST抽象語法樹之後, Lombok 根據自己編寫的註解處理器,動態地修改 AST,增加新的節點(即Lombok自定義註解所需要生成的程式碼),最終通過分析生成JVM可執行的位元組碼Class檔案。使用Annotation Processing自定義註解是在編譯階段進行修改,而jdk的反射技術是在執行時動態修改,兩者相比,反射雖然更加靈活一些但是帶來的效能損耗更加大。

Lombok本質上就是一個實現了JSR 269 API的程式,在使用javac的命令過程中,它生效的具體流程如下:

  • javac對原始碼進行分析,生成一棵抽象語法樹(AST);
  • 執行過程中呼叫實現了JSR 269 API的lombok程式;
  • 編譯機會呼叫lombok程式對第一步得到的AST進行處理,找到其註解所在類對應的語法樹(AST),然後修改該語法樹,增加註解對應的方法或程式碼片段到定義的相應樹節點;
  • javac使用修改後的抽象語法樹生成最終的java位元組碼檔案;

4.手寫簡易lombok

使用的是idea工具進行開發,使用的jdk版本為1.8,因為我們是自己手寫的idea提示會報錯,但是能正常執行,因為lombok是idea針對於他有外掛提示,我們的沒有,但是也不影響正常使用。

1.我們需要使用到jdk安裝路徑下lib包下的tools.jar,我們可以收到加入到專案依賴,也可以在maven中直接引入。我們直接使用idea新建一個普通的maven專案,然後設定如下,最後將這個專案打包一下,在別的專案中引入即可。

maven設定如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>lombok</groupId>
    <artifactId>com.compass.lombok</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>C:/Program Files/Java/jdk1.8.0_251/lib/tools.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc5</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

還有一個是 com.google.auto.service 這個是使用SPI機制的一個依賴,關於spi可以自行百度瞭解,這裡就不再進行展開。

關鍵核心介面:AbstractProcessor,這個就是在編譯期處理註解的一個介面,然後我們可以通過實現這個介面通過修改位元組碼檔案,最終在位元組碼檔案中生成get和set方法。

首先我們定義一個DATA註解:

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.RUNTIME)
public @interface Data {
}

然後寫一個 DataAnnotationProcessor 繼承AbstractProcessor即可

import com.compass.lombok.annotation.Data;
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 java.util.Set;

/**
 * @author compass
 * @date 2022-10-13
 * @since 1.0
 **/
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.compass.lombok.annotation.Data")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DataAnnotationProcessor extends AbstractProcessor {
    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private Names names;

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Data.class);
        for (Element element : set) {
            javacTrees.getTree(element).accept(new TreeTranslator(){
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    jcClassDecl.defs.stream()
                            .filter(it->it.getKind().equals(Tree.Kind.VARIABLE))
                            .map(it->(JCTree.JCVariableDecl) it).forEach(it->{
                                jcClassDecl.defs = jcClassDecl.defs.prepend(genGetterMethod(it));
                                jcClassDecl.defs = jcClassDecl.defs.prepend(genSetterMethod(it));

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

    private JCTree.JCMethodDecl genGetterMethod(JCTree.JCVariableDecl jcVariableDecl){
        JCTree.JCIdent _this = treeMaker.Ident(names.fromString("this"));
        Name name = jcVariableDecl.getName();
        JCTree.JCFieldAccess select = treeMaker.Select(_this, name);
        JCTree.JCReturn returnStatement = treeMaker.Return(select);

        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        statements.append(returnStatement);

        JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

        Name getMethodName = getGetMethodName(jcVariableDecl.getName());

        JCTree.JCExpression returnMethodType = jcVariableDecl.vartype;

        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

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

        List<JCTree.JCVariableDecl> parameterList = List.nil();

        List<JCTree.JCExpression> throwList = List.nil();

        return treeMaker.MethodDef(modifiers, getMethodName, returnMethodType, methodGenericParamList, parameterList, throwList, body, null);

    }
    public  JCTree.JCMethodDecl genSetterMethod(JCTree.JCVariableDecl jcVariableDecl){

        JCTree.JCIdent _this = treeMaker.Ident(names.fromString("this"));
        Name name = jcVariableDecl.getName();
        JCTree.JCFieldAccess select = treeMaker.Select(_this, name);
        JCTree.JCAssign statementAssign = treeMaker.Assign(select, treeMaker.Ident(jcVariableDecl.getName()));
        JCTree.JCExpressionStatement statement = treeMaker.Exec(statementAssign);
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        statements.append(statement);

        JCTree.JCVariableDecl params = treeMaker.VarDef(
                treeMaker.Modifiers(Flags.PARAMETER, List.nil()),
                jcVariableDecl.name,
                jcVariableDecl.vartype,
                null
        );

        JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

        Name setMethodName = getSetMethodName(jcVariableDecl.getName());

        JCTree.JCExpression returnMethodType = treeMaker.Type(new Type.JCVoidType());

        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

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

        List<JCTree.JCVariableDecl> parameterList = List.of(params);

        List<JCTree.JCExpression> throwList = List.nil();

        return treeMaker.MethodDef(modifiers, setMethodName, returnMethodType, methodGenericParamList, parameterList, throwList, body, null);
    }

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

    private Name getSetMethodName(Name name){
        String filedName = name.toString();
        return names.fromString("set"+filedName.substring(0,1).toUpperCase()+filedName.substring(1));
    }

}

其實到這裡就編寫完畢了,這裡去動態修改位元組碼,然後生成了get和set方法,至於其他的方法那就後面再說,此案例參照於《深入jvm位元組碼》進行編寫。

最後在maven專案中打包

在別的專案直接使用即可,直接在別的專案的實體類上加上@Data註解即可生成get和set方法,但是沒有方法提升,但是能正常執行,這裡是idea的一個程式碼提示的問題,因為我們這個沒有對應的idea外掛,所以idea會提示報錯,但是能正常執行。

到此這篇關於利用Java手寫一個簡易的lombok的範例程式碼的文章就介紹到這了,更多相關Java手寫lombok內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com