91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Lombok 原理分析與功能實現

發布時間:2020-08-05 15:32:06 來源:網絡 閱讀:2441 作者:Java月亮呀 欄目:編程語言

前言

這兩天沒什么重要的事情做,但是想著還要春招總覺得得學點什么才行,正巧想起來前幾次面試的時候面試官總喜歡問一些框架的底層實現,但是我學東西比較傾向于用到啥學啥,因此在這些方面吃了很大的虧。而且其實很多框架也多而雜,代碼起來費勁,無非就是幾套設計模式套一套,用到的東西其實也就那么些,感覺沒啥新意。剛這兩天讀”深入理解JVM”的時候突然想起來有個叫Lombok的東西以前一直不能理解他的實現原理,現在正好趁著閑暇的時間研究研究。

Lombok

代碼

Lombok是一個開源項目,源代碼托管在GITHUB/rzwitserloot,如果需要在maven里引用,只需要添加下依賴:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.8</version>
</dependency>

功能

那么Lombok是做什么的呢?其實很簡單,一個最簡單的例子就是它能夠實現通過添加注解,能夠自動生成一些方法。比如這樣的類:

@Getter
class Test{
    private String value;
}

我們用Lombok提供的@Getter來注解這個類,這個類在編譯的時候就會變成:

class Test{
    private String value;
    public String getValue(){
        return this.value;
    }
}

當然Lombok也提供了很多其他的注解,這只是其中一個最典型的例子。其他的用法網上的資料已經很多了,這里就不啰嗦。
看上去是很方便的一個功能,尤其是在很多項目里有很多bean,每次都要手寫或自動生成setter getter方法,搞得代碼很長而且沒有啥意義,因此這個對簡化代碼的強迫癥們還是很有吸引力的。
但是,我們發現這個包跟一般的包有很大區別,絕大多數java包都工作在運行時,比如spring提供的那種注解,通過在運行時用反射來實現業務邏輯。Lombok這個東西工作卻在編譯期,在運行時是無法通過反射獲取到這個注解的。
而且由于他相當于是在編譯期對代碼進行了修改,因此從直觀上看,源代碼甚至是語法有問題的。
一個更直接的體現就是,普通的包在引用之后一般的IDE都能夠自動識別語法,但是Lombok的這些注解,一般的IDE都無法自動識別,比如我們上面的Test類,如果我們在其他地方這么調用了一下:

Test test=new Test();
test.getValue();

IDE的自動語法檢查就會報錯,說找不到這個getValue方法。因此如果要使用Lombok的話還需要配合安裝相應的插件,防止IDE的自動檢查報錯。
因此,可以說這個東西的設計初衷比較美好,但是用起來比較麻煩,而且破壞了代碼的完整性,很多項目組(包括我自己)都不高興用。但是他的實現原理卻還是比較好玩的,隨便搜了搜發現網上最多也只提到了他修改了抽象語法樹,雖說從感性上可以理解,但是還是想自己手敲一敲真正去實現一下。

原理

翻了翻現有的資料,再加上自己的一些猜想,Lombok的基本流程應該基本是這樣:

  • 定義編譯期的注解
  • 利用JSR269 api(Pluggable Annotation Processing API )創建編譯期的注解處理器
  • 利用tools.jar的javac api處理AST(抽象語法樹)
  • 將功能注冊進jar包

看起來還是比較簡單的,但是不得不說坑也不少,搞了兩天才把流程搞通。。。
下面就根據這個流程自己實現一個有類似功能的Getter類。

手擼Getter

實驗的目的是自定義一個針對類的Getter注解,它能夠讀取該類的成員方法并自動生成getter方法。

項目依賴

由于比較習慣用maven,我這里就用maven構建一下項目,修改下當前的pom.xml文件如下:

<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>com.mythsman.test</groupId>
    <artifactId>getter</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>test</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

主要定義了下項目名,除了默認依賴的junit之外(其實并沒有用),這里添加了tools.jar包。這個包實在jdk的lib下面,因此scope是system,由于${java.home}變量表示的是jre的位置,因此還要根據這個位置找到實際的tools.jar的路徑并寫在systemPath里。
由于防止在寫代碼的時候用到java8的一些語法,這里配置了下編譯插件使其支持java8。

創建Getter注解

定義注解Getter.java:

package com.mythsman.test;

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 Getter {
}

這里的Target我選擇了ElementType.TYPE表示是對類的注解,Retention選擇了RententionPolicy.SOURCE,表示這個注解只在編譯期起作用,在運行時將不存在。這個比較簡單,稍微復雜點的是對這個注解的處理機制。像spring那種注解是通過反射來獲得注解對應的元素并實現業務邏輯,但是我們顯然不希望在使用Lombok這種功能的時候還要編寫其他的調用代碼,況且用反射也獲取不到編譯期才存在的注解。
幸運的是Java早已支持了JSR269的規范,允許在編譯時指定一個processor類來對編譯階段的注解進行干預,下面就來解決下這個處理器。

創建Getter注解的處理器

基本框架

自定義的處理器需要繼承AbstractProcessor這個類,基本的框架大體應當如下:

package com.mythsman.test;

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

@SupportedAnnotationTypes("com.mythsman.test.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return true;
    }
}

需要定義兩個注解,一個表示該處理器需要處理的注解,另外一個表示該處理器支持的源碼版本。然后需要著重實現兩個方法,init跟process。init的主要用途是通過ProcessingEnvironment來獲取編譯階段的一些環境信息;process主要是實現具體邏輯的地方,也就是對AST進行處理的地方。

具體怎么做呢?

INIT方法

首先我們要重寫下init方法,從環境里提取一些關鍵的類:

private Messager messager;
private JavacTrees trees;
private TreeMaker treeMaker;
private Names names;

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

我們提取了四個主要的類:

  • Messager主要是用來在編譯期打log用的
  • JavacTrees提供了待處理的抽象語法樹
  • TreeMaker封裝了創建AST節點的一些方法
  • Names提供了創建標識符的方法
PROCESS方法

process方法的邏輯比較簡單,但是由于這里的api對于我們來說比較陌生,因此寫起來還是費了不少勁的:

@Override
public synchronized boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Getter.class);
    set.forEach(element -> {
        JCTree jcTree = trees.getTree(element);
        jcTree.accept(new TreeTranslator() {
            @Override
            public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();

                for (JCTree tree : jcClassDecl.defs) {
                    if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
                        JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
                        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;
}

步驟大概是下面這樣:

  1. 利用roundEnv的getElementsAnnotatedWith方法過濾出被Getter這個注解標記的類,并存入set
  2. 遍歷這個set里的每一個元素,并生成jCTree這個語法樹
  3. 創建一個TreeTranslator,并重寫其中的visitClassDef方法,這個方法處理遍歷語法樹得到的類定義部分jcClassDecl
    1. 創建一個jcVariableDeclList保存類的成員變量
    2. 遍歷jcTree的所有成員(包括成員變量和成員函數和構造函數),過濾出其中的成員變量,并添加進jcVariableDeclList
    3. 將jcVariableDeclList的所有變量轉換成需要添加的getter方法,并添加進jcClassDecl的成員中
    4. 調用默認的遍歷方法遍歷處理后的jcClassDecl
  4. 利用上面的TreeTranslator去處理jcTree

接下來再實現makeGetterMethodDecl方法:

private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {

 ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
 statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
 JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
 return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null);
}

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

邏輯就是讀取變量的定義,并創建對應的Getter方法,并試圖用駝峰命名法。

整體上難點還是集中在api的使用上,還有一些細微的注意點:
首先,messager的printMessage方法在打印log的時候會自動過濾重復的log信息。
其次,這里的list并不是java.util里面的list,而是一個自定義的list,這個list的用法比較坑爹,他采用的是這樣的方式:

package com.sun.tools.javac.util;

public class List<A> extends AbstractCollection<A> implements java.util.List<A> {
    public A head;
    public List<A> tail;

    //...

    List(A var1, List<A> var2) {
        this.tail = var2;
        this.head = var1;
    }

    public List<A> prepend(A var1) {
        return new List(var1, this);
    }

    public static <A> List<A> of(A var0) {
        return new List(var0, nil());
    }

    public List<A> append(A var1) {
        return of(var1).prependList(this);
    }

    public static <A> List<A> nil() {
        return EMPTY_LIST;
    }
    //...
}

挺有趣的,用這種叫cons而不是list的數據結構,添加元素的時候就把自己賦給自己的tail,新來的元素放進head。不過需要注意的是這個東西不支持鏈式調用,prepend之后還要將新值賦給自己。
而且這里在創建getter方法的時候還要把參數寫全寫對了,尤其是添加this指針的這種用法。

測試類

上面基本就是所有功能代碼了,接下來我們要寫一個類來測試一下(App.java):

package com.mythsman.test;

@Getter
public class App {
    private String value;

    private String value2;

    public App(String value) {
        this.value = value;
    }

    public static void main(String[] args) {
        App app = new App("it works");
        System.out.println(app.getValue());
    }
}

不過,先不要急著構建,構建了肯定會失敗,因為這原則上應該是兩個項目。Getter.java是注解類沒問題,但是GetterProcessor.java是處理器,App.java需要在編譯期調用這個處理器,因此這兩個東西是不能一起編譯的,正確的編譯方法應該是類似下面這樣,寫成compile.sh腳本就是:

#!/usr/bin/env bash

if [ -d classes ]; then
    rm -rf classes;
fi
mkdir classes

javac -cp $JAVA_HOME/lib/tools.jar com/mythsman/test/Getter* -d classes/

javac -cp classes -d classes -processor com.mythsman.test.GetterProcessor com/mythsman/test/App.java

javap -p classes com/mythsman/test/App.class

java -cp classes com.mythsman.test.App

其實是五個步驟:

  1. 創建保存class文件的文件夾
  2. 導入tools.jar,編譯processor并輸出
  3. 編譯App.java,并使用javac的-processor參數指定編譯階段的處理器GetterProcessor
  4. 用javap顯示編譯后的App.class文件(非必須,方便看結果)
  5. 執行測試類

好了,進入項目的根目錄,當前的目錄結構應該是這樣的:

.
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   ├── com
│   │   │   │   └── mythsman
│   │   │   │       └── test
│   │   │   │           ├── App.java
│   │   │   │           ├── Getter.java
│   │   │   │           └── GetterProcessor.java
│   │   │   └── compile.sh

調用compile.sh,輸出如下:

Note: value has been processed
Note: value2 has been processed
Compiled from "App.java"
public class com.mythsman.test.App {
  private java.lang.String value;
  private java.lang.String value2;
  public java.lang.String getValue2();
  public java.lang.String getValue();
  public com.mythsman.test.App(java.lang.String);
  public static void main(java.lang.String[]);
}
it works

Note行就是在GetterProcessor類里通過messager打印的log,中間的是javap反編譯的結果,最后一行表示測試調用成功。

MAVEN構建并打包

上面的測試部分其實是為了測試而測試,其實這應當是兩個項目,一個是processor項目,這個項目應當被打成一個jar包,供調用者使用;另一個項目是app項目,這個項目是專門使用jar包的,他并不希望添加任何額外編譯參數,就跟lombok的用法一樣。
簡單來說,就是我們希望把processor打成一個包,并且在使用時不需要添加額外參數。
那么如何在調用的時候不用加參數呢,其實我們知道java在編譯的時候會去資源文件夾下讀一個META-INF文件夾,這個文件夾下面除了MANIFEST.MF文件之外,還可以添加一個services文件夾,我們可以在這個文件夾下創建一個文件,文件名是javax.annotation.processing.Processor,文件內容是com.mythsman.test.GetterProcessor。
我們知道maven在編譯前會先拷貝資源文件夾,然后當他在編譯時候發現了資源文件夾下的META-INF/serivces文件夾時,他就會讀取里面的文件,并將文件名所代表的接口用文件內容表示的類來實現。這就相當于做了-processor參數該做的事了。
當然這個文件我們并不希望調用者去寫,而是希望在processor項目里集成,調用的時候能直接繼承META-INF。

好了,我們先刪除App.java和compile.sh,添加下META-INF文件夾,當前目錄結構應該是這樣的:

.
├── pom.xml
├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── mythsman
│       │           └── test
│       │               ├── Getter.java
│       │               └── GetterProcessor.java
│       └── resources
│           └── META-INF
│               └── services
│                   └── javax.annotation.processing.Processor

當然,我們還不能編譯,因為processor項目并不需要把自己添加為processor(況且自己還沒編譯呢怎么調用自己)。。。完了,好像死循環了,自己在編譯的時候不能添加services文件夾,但是又需要打的包里有services文件夾,這該怎么搞呢?
其實很簡單,配置一下maven的插件就行,打開pom.xml,在project/build/標簽里添加下面的配置:

<build>
   <resources>
        <resource>
            <directory>src/main/resources</directory>
            <excludes>
                <exclude>META-INF/**/*</exclude>
            </excludes>
        </resource>
    </resources>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <version>2.6</version>
            <executions>
                <execution>
                    <id>process-META</id>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>copy-resources</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/classes</outputDirectory>
                        <resources>
                            <resource>
                                <directory>${basedir}/src/main/resources/</directory>
                                <includes>
                                    <include>**/*</include>
                                </includes>
                            </resource>
                        </resources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        ...
    </plugins>
</build>

我們知道maven構建的第一步就是調用maven-resources-plugin插件的resources命令,將resources文件夾復制到target/classes中,那么我們配置一下resources標簽,過濾掉META-INF文件夾,這樣在編譯的時候就不會找到services的配置了。然后我們在打包前(prepare-package生命周期)再利用maven-resources-plugin插件的copy-resources命令把services文件夾重新拷貝過來不就好了么。
這樣配置好了,就可以直接執行mvn clean install打包提交到本地私服:

myths@pc:~/Desktop/test$ mvn clean install
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building test 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ getter ---
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ getter ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ getter ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /home/myths/Desktop/test/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ getter ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/myths/Desktop/test/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ getter ---
[INFO] No sources to compile
[INFO] 
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ getter ---
[INFO] No tests to run.
[INFO] 
[INFO] --- maven-resources-plugin:2.6:copy-resources (process-META) @ getter ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] 
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ getter ---
[INFO] Building jar: /home/myths/Desktop/test/target/getter-1.0-SNAPSHOT.jar
[INFO] 
[INFO] --- maven-install-plugin:2.4:install (default-install) @ getter ---
[INFO] Installing /home/myths/Desktop/test/target/getter-1.0-SNAPSHOT.jar to /home/myths/.m2/repository/com/mythsman/test/getter/1.0-SNAPSHOT/getter-1.0-SNAPSHOT.jar
[INFO] Installing /home/myths/Desktop/test/pom.xml to /home/myths/.m2/repository/com/mythsman/test/getter/1.0-SNAPSHOT/getter-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.017 s
[INFO] Finished at: 2017-12-19T19:57:04+08:00
[INFO] Final Memory: 16M/201M
[INFO] ------------------------------------------------------------------------

可以看到這里的process-META作用生效。

調用JAR包測試

重新創建一個測試項目app:

.
├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── mythsman
                    └── test
                        └── App.java

pom.xml:

<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>com.mythsman.test</groupId>
    <artifactId>app</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>main</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.mythsman.test</groupId>
            <artifactId>getter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

App.java:

package com.mythsman.test;

@Getter
public class App {
    private String value;

    private String value2;

    public App(String value) {
        this.value = value;
    }

    public static void main(String[] args) {
        App app = new App("it works");
        System.out.println(app.getValue());
    }
}

編譯并執行:

mvn clean compile && java -cp target/classes com.mythsman.test.App

最后就會在構建成功后打印”it works”。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

武功县| 漳平市| 肇源县| 辽源市| 密山市| 保山市| 历史| 遂平县| 西昌市| 普兰店市| 若羌县| 临沭县| 屏山县| 横山县| 邢台市| 鸡泽县| 石狮市| 鄢陵县| 定南县| 麻城市| 莆田市| 和龙市| 亳州市| 鄂尔多斯市| 离岛区| 连州市| 闸北区| 合山市| 吉林省| 四会市| 大石桥市| 砚山县| 奉化市| 堆龙德庆县| 馆陶县| 达日县| 南溪县| 偏关县| 祁东县| 海口市| 丹寨县|