您好,登錄后才能下訂單哦!
這兩天沒什么重要的事情做,但是想著還要春招總覺得得學點什么才行,正巧想起來前幾次面試的時候面試官總喜歡問一些框架的底層實現,但是我學東西比較傾向于用到啥學啥,因此在這些方面吃了很大的虧。而且其實很多框架也多而雜,代碼起來費勁,無非就是幾套設計模式套一套,用到的東西其實也就那么些,感覺沒啥新意。剛這兩天讀”深入理解JVM”的時候突然想起來有個叫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的基本流程應該基本是這樣:
看起來還是比較簡單的,但是不得不說坑也不少,搞了兩天才把流程搞通。。。
下面就根據這個流程自己實現一個有類似功能的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.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類來對編譯階段的注解進行干預,下面就來解決下這個處理器。
自定義的處理器需要繼承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方法,從環境里提取一些關鍵的類:
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);
}
我們提取了四個主要的類:
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;
}
步驟大概是下面這樣:
接下來再實現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
其實是五個步驟:
好了,進入項目的根目錄,當前的目錄結構應該是這樣的:
.
├── 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反編譯的結果,最后一行表示測試調用成功。
上面的測試部分其實是為了測試而測試,其實這應當是兩個項目,一個是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作用生效。
重新創建一個測試項目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”。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。