您好,登錄后才能下訂單哦!
這兩天沒(méi)什么重要的事情做,但是想著還要春招總覺(jué)得得學(xué)點(diǎn)什么才行,正巧想起來(lái)前幾次面試的時(shí)候面試官總喜歡問(wèn)一些框架的底層實(shí)現(xiàn),但是我學(xué)東西比較傾向于用到啥學(xué)啥,因此在這些方面吃了很大的虧。而且其實(shí)很多框架也多而雜,代碼起來(lái)費(fèi)勁,無(wú)非就是幾套設(shè)計(jì)模式套一套,用到的東西其實(shí)也就那么些,感覺(jué)沒(méi)啥新意。剛這兩天讀”深入理解JVM”的時(shí)候突然想起來(lái)有個(gè)叫Lombok的東西以前一直不能理解他的實(shí)現(xiàn)原理,現(xiàn)在正好趁著閑暇的時(shí)間研究研究。
Lombok是一個(gè)開(kāi)源項(xiàng)目,源代碼托管在GITHUB/rzwitserloot,如果需要在maven里引用,只需要添加下依賴:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.8</version>
</dependency>
那么Lombok是做什么的呢?其實(shí)很簡(jiǎn)單,一個(gè)最簡(jiǎn)單的例子就是它能夠?qū)崿F(xiàn)通過(guò)添加注解,能夠自動(dòng)生成一些方法。比如這樣的類:
@Getter
class Test{
private String value;
}
我們用Lombok提供的@Getter來(lái)注解這個(gè)類,這個(gè)類在編譯的時(shí)候就會(huì)變成:
class Test{
private String value;
public String getValue(){
return this.value;
}
}
當(dāng)然Lombok也提供了很多其他的注解,這只是其中一個(gè)最典型的例子。其他的用法網(wǎng)上的資料已經(jīng)很多了,這里就不啰嗦。
看上去是很方便的一個(gè)功能,尤其是在很多項(xiàng)目里有很多bean,每次都要手寫(xiě)或自動(dòng)生成setter getter方法,搞得代碼很長(zhǎng)而且沒(méi)有啥意義,因此這個(gè)對(duì)簡(jiǎn)化代碼的強(qiáng)迫癥們還是很有吸引力的。
但是,我們發(fā)現(xiàn)這個(gè)包跟一般的包有很大區(qū)別,絕大多數(shù)java包都工作在運(yùn)行時(shí),比如spring提供的那種注解,通過(guò)在運(yùn)行時(shí)用反射來(lái)實(shí)現(xiàn)業(yè)務(wù)邏輯。Lombok這個(gè)東西工作卻在編譯期,在運(yùn)行時(shí)是無(wú)法通過(guò)反射獲取到這個(gè)注解的。
而且由于他相當(dāng)于是在編譯期對(duì)代碼進(jìn)行了修改,因此從直觀上看,源代碼甚至是語(yǔ)法有問(wèn)題的。
一個(gè)更直接的體現(xiàn)就是,普通的包在引用之后一般的IDE都能夠自動(dòng)識(shí)別語(yǔ)法,但是Lombok的這些注解,一般的IDE都無(wú)法自動(dòng)識(shí)別,比如我們上面的Test類,如果我們?cè)谄渌胤竭@么調(diào)用了一下:
Test test=new Test();
test.getValue();
IDE的自動(dòng)語(yǔ)法檢查就會(huì)報(bào)錯(cuò),說(shuō)找不到這個(gè)getValue方法。因此如果要使用Lombok的話還需要配合安裝相應(yīng)的插件,防止IDE的自動(dòng)檢查報(bào)錯(cuò)。
因此,可以說(shuō)這個(gè)東西的設(shè)計(jì)初衷比較美好,但是用起來(lái)比較麻煩,而且破壞了代碼的完整性,很多項(xiàng)目組(包括我自己)都不高興用。但是他的實(shí)現(xiàn)原理卻還是比較好玩的,隨便搜了搜發(fā)現(xiàn)網(wǎng)上最多也只提到了他修改了抽象語(yǔ)法樹(shù),雖說(shuō)從感性上可以理解,但是還是想自己手敲一敲真正去實(shí)現(xiàn)一下。
翻了翻現(xiàn)有的資料,再加上自己的一些猜想,Lombok的基本流程應(yīng)該基本是這樣:
看起來(lái)還是比較簡(jiǎn)單的,但是不得不說(shuō)坑也不少,搞了兩天才把流程搞通。。。
下面就根據(jù)這個(gè)流程自己實(shí)現(xiàn)一個(gè)有類似功能的Getter類。
實(shí)驗(yàn)的目的是自定義一個(gè)針對(duì)類的Getter注解,它能夠讀取該類的成員方法并自動(dòng)生成getter方法。
由于比較習(xí)慣用maven,我這里就用maven構(gòu)建一下項(xiàng)目,修改下當(dāng)前的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>
主要定義了下項(xiàng)目名,除了默認(rèn)依賴的junit之外(其實(shí)并沒(méi)有用),這里添加了tools.jar包。這個(gè)包實(shí)在jdk的lib下面,因此scope是system,由于${java.home}變量表示的是jre的位置,因此還要根據(jù)這個(gè)位置找到實(shí)際的tools.jar的路徑并寫(xiě)在systemPath里。
由于防止在寫(xiě)代碼的時(shí)候用到j(luò)ava8的一些語(yǔ)法,這里配置了下編譯插件使其支持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表示是對(duì)類的注解,Retention選擇了RententionPolicy.SOURCE,表示這個(gè)注解只在編譯期起作用,在運(yùn)行時(shí)將不存在。這個(gè)比較簡(jiǎn)單,稍微復(fù)雜點(diǎn)的是對(duì)這個(gè)注解的處理機(jī)制。像spring那種注解是通過(guò)反射來(lái)獲得注解對(duì)應(yīng)的元素并實(shí)現(xiàn)業(yè)務(wù)邏輯,但是我們顯然不希望在使用Lombok這種功能的時(shí)候還要編寫(xiě)其他的調(diào)用代碼,況且用反射也獲取不到編譯期才存在的注解。
幸運(yùn)的是Java早已支持了JSR269的規(guī)范,允許在編譯時(shí)指定一個(gè)processor類來(lái)對(duì)編譯階段的注解進(jìn)行干預(yù),下面就來(lái)解決下這個(gè)處理器。
自定義的處理器需要繼承AbstractProcessor這個(gè)類,基本的框架大體應(yīng)當(dāng)如下:
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;
}
}
需要定義兩個(gè)注解,一個(gè)表示該處理器需要處理的注解,另外一個(gè)表示該處理器支持的源碼版本。然后需要著重實(shí)現(xiàn)兩個(gè)方法,init跟process。init的主要用途是通過(guò)ProcessingEnvironment來(lái)獲取編譯階段的一些環(huán)境信息;process主要是實(shí)現(xiàn)具體邏輯的地方,也就是對(duì)AST進(jìn)行處理的地方。
具體怎么做呢?
首先我們要重寫(xiě)下init方法,從環(huán)境里提取一些關(guān)鍵的類:
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);
}
我們提取了四個(gè)主要的類:
process方法的邏輯比較簡(jiǎn)單,但是由于這里的api對(duì)于我們來(lái)說(shuō)比較陌生,因此寫(xiě)起來(lái)還是費(fèi)了不少勁的:
@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;
}
步驟大概是下面這樣:
接下來(lái)再實(shí)現(xiàn)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>
邏輯就是讀取變量的定義,并創(chuàng)建對(duì)應(yīng)的Getter方法,并試圖用駝峰命名法。
整體上難點(diǎn)還是集中在api的使用上,還有一些細(xì)微的注意點(diǎn):
首先,messager的printMessage方法在打印log的時(shí)候會(huì)自動(dòng)過(guò)濾重復(fù)的log信息。
其次,這里的list并不是java.util里面的list,而是一個(gè)自定義的list,這個(gè)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的數(shù)據(jù)結(jié)構(gòu),添加元素的時(shí)候就把自己賦給自己的tail,新來(lái)的元素放進(jìn)head。不過(guò)需要注意的是這個(gè)東西不支持鏈?zhǔn)秸{(diào)用,prepend之后還要將新值賦給自己。
而且這里在創(chuàng)建getter方法的時(shí)候還要把參數(shù)寫(xiě)全寫(xiě)對(duì)了,尤其是添加this指針的這種用法。
上面基本就是所有功能代碼了,接下來(lái)我們要寫(xiě)一個(gè)類來(lái)測(cè)試一下(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());
}
}
不過(guò),先不要急著構(gòu)建,構(gòu)建了肯定會(huì)失敗,因?yàn)檫@原則上應(yīng)該是兩個(gè)項(xiàng)目。Getter.java是注解類沒(méi)問(wèn)題,但是GetterProcessor.java是處理器,App.java需要在編譯期調(diào)用這個(gè)處理器,因此這兩個(gè)東西是不能一起編譯的,正確的編譯方法應(yīng)該是類似下面這樣,寫(xiě)成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
其實(shí)是五個(gè)步驟:
好了,進(jìn)入項(xiàng)目的根目錄,當(dāng)前的目錄結(jié)構(gòu)應(yīng)該是這樣的:
.
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ ├── com
│ │ │ │ └── mythsman
│ │ │ │ └── test
│ │ │ │ ├── App.java
│ │ │ │ ├── Getter.java
│ │ │ │ └── GetterProcessor.java
│ │ │ └── compile.sh
調(diào)用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類里通過(guò)messager打印的log,中間的是javap反編譯的結(jié)果,最后一行表示測(cè)試調(diào)用成功。
上面的測(cè)試部分其實(shí)是為了測(cè)試而測(cè)試,其實(shí)這應(yīng)當(dāng)是兩個(gè)項(xiàng)目,一個(gè)是processor項(xiàng)目,這個(gè)項(xiàng)目應(yīng)當(dāng)被打成一個(gè)jar包,供調(diào)用者使用;另一個(gè)項(xiàng)目是app項(xiàng)目,這個(gè)項(xiàng)目是專門(mén)使用jar包的,他并不希望添加任何額外編譯參數(shù),就跟lombok的用法一樣。
簡(jiǎn)單來(lái)說(shuō),就是我們希望把processor打成一個(gè)包,并且在使用時(shí)不需要添加額外參數(shù)。
那么如何在調(diào)用的時(shí)候不用加參數(shù)呢,其實(shí)我們知道java在編譯的時(shí)候會(huì)去資源文件夾下讀一個(gè)META-INF文件夾,這個(gè)文件夾下面除了MANIFEST.MF文件之外,還可以添加一個(gè)services文件夾,我們可以在這個(gè)文件夾下創(chuàng)建一個(gè)文件,文件名是javax.annotation.processing.Processor,文件內(nèi)容是com.mythsman.test.GetterProcessor。
我們知道m(xù)aven在編譯前會(huì)先拷貝資源文件夾,然后當(dāng)他在編譯時(shí)候發(fā)現(xiàn)了資源文件夾下的META-INF/serivces文件夾時(shí),他就會(huì)讀取里面的文件,并將文件名所代表的接口用文件內(nèi)容表示的類來(lái)實(shí)現(xiàn)。這就相當(dāng)于做了-processor參數(shù)該做的事了。
當(dāng)然這個(gè)文件我們并不希望調(diào)用者去寫(xiě),而是希望在processor項(xiàng)目里集成,調(diào)用的時(shí)候能直接繼承META-INF。
好了,我們先刪除App.java和compile.sh,添加下META-INF文件夾,當(dāng)前目錄結(jié)構(gòu)應(yīng)該是這樣的:
.
├── pom.xml
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── mythsman
│ │ └── test
│ │ ├── Getter.java
│ │ └── GetterProcessor.java
│ └── resources
│ └── META-INF
│ └── services
│ └── javax.annotation.processing.Processor
當(dāng)然,我們還不能編譯,因?yàn)閜rocessor項(xiàng)目并不需要把自己添加為processor(況且自己還沒(méi)編譯呢怎么調(diào)用自己)。。。完了,好像死循環(huán)了,自己在編譯的時(shí)候不能添加services文件夾,但是又需要打的包里有services文件夾,這該怎么搞呢?
其實(shí)很簡(jiǎn)單,配置一下maven的插件就行,打開(kāi)pom.xml,在project/build/標(biāo)簽里添加下面的配置:
<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>
我們知道m(xù)aven構(gòu)建的第一步就是調(diào)用maven-resources-plugin插件的resources命令,將resources文件夾復(fù)制到target/classes中,那么我們配置一下resources標(biāo)簽,過(guò)濾掉META-INF文件夾,這樣在編譯的時(shí)候就不會(huì)找到services的配置了。然后我們?cè)诖虬?prepare-package生命周期)再利用maven-resources-plugin插件的copy-resources命令把services文件夾重新拷貝過(guò)來(lái)不就好了么。
這樣配置好了,就可以直接執(zhí)行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作用生效。
重新創(chuàng)建一個(gè)測(cè)試項(xiàng)目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());
}
}
編譯并執(zhí)行:
mvn clean compile && java -cp target/classes com.mythsman.test.App
最后就會(huì)在構(gòu)建成功后打印”it works”。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。