APT?注解處理器實(shí)現(xiàn)?Lombok?常用注解功能詳解
1 背景
在開發(fā)中我們常常會(huì)用到類似 lombok 、mapstruct 或者 mybatisplus 的框架,只要加入幾個(gè)注解即可生成對(duì)應(yīng)的方法,既然被很多框架使用,了解其中的原理還是非常有必要的。
2 生成字節(jié)碼原理
2.1 APT(Annotation Processing Tool )注解處理器
基于 JSR 269(Pluggable Annotation Processing API)規(guī)范,提供插入式注解處理接口,Java 6 開始支持,它的主要功能是在 Java 編譯期對(duì)源碼進(jìn)行處理, 通過這些規(guī)范插件,可以讀取、修改、添加抽象語法樹中的任意元素。
如上圖 Javac 在編器期間,如果使用注解處理器對(duì)語法樹進(jìn)行了修改,編譯器將回到解析及填充符號(hào)表的過程重新處理,直至處理完成,再對(duì)語法樹進(jìn)行修改。
2.2 AbstractProcessor 注解處理器的使用
創(chuàng)建一個(gè)注解處理器分為如下幾步:
- 創(chuàng)建注解類 : 比如
@Data
類 - 創(chuàng)建
AbstractProcessor
的繼承類, APT 的核心類 - 修改生成字節(jié)碼
- SPI配置: 在
META-INF\services
創(chuàng)建名為javax.annotation.processing.Processor
配置文件添加 SPI 實(shí)現(xiàn)
2.3 APT 、 AOP、 JavaAgent 優(yōu)缺點(diǎn)
在我們?nèi)粘i_發(fā)中,如果需要做一些埋點(diǎn),AOP 并非唯一選擇,APT 在有些場(chǎng)景下也可以使用的,支持靜態(tài)方法和私有方法,同時(shí)穩(wěn)定性也比較好,覆蓋的場(chǎng)景比較全。
2.4 lombok 原理
1 APT(Annotation Processing Tool )注解處理器 2 javac api處理AST(抽象語法樹)
大致原理如下圖所示:
如想具體分析 lombok 的實(shí)現(xiàn),可以從 Processor
和AnnotationProcessor
這兩個(gè)類的 process 方法入手,通過 lombok.javac.JavacAnnotationHandler
處理器找到對(duì)應(yīng)的注解實(shí)現(xiàn)。
3 自己實(shí)現(xiàn)Lombok
3.1 創(chuàng)建Data注解
@Documented @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE}) public @interface Data { }
該 Data 注解只能在編譯期的時(shí)候獲取到,在運(yùn)行期是無法獲取到的。
3.2 自定義注解處理器
通過實(shí)現(xiàn)Processor
接口可以自定義注解處理器,這里我們采用更簡(jiǎn)單的方法通過繼承AbstractProcessor
類實(shí)現(xiàn)自定義注解處理器, 實(shí)現(xiàn)抽象方法 process 處理我們想要的功能。
3.2.1 APT簡(jiǎn)單介紹
@SupportedAnnotationTypes({"com.nicky.lombok.annotation.Data"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) public class DataProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnv) { } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { } }
@SupportedAnnotationTypes 注解表示哪些注解需要注解處理器處理,可以多個(gè)注解校驗(yàn) @SupportedSourceVersion 注解 用于指定jdk使用版本
如果不使用注解也可以在重寫父類方法
Set<String> getSupportedAnnotationTypes() SourceVersion getSupportedSourceVersion ...
- init 方法
主要是用于初始化上下文等信息
- process方法
具體處理注解的業(yè)務(wù)方法
3.2.2 具體實(shí)現(xiàn)
- 1 重寫init方法
/** * 抽象語法樹 */ private JavacTrees trees; /** * AST */ private TreeMaker treeMaker; /** * 標(biāo)識(shí)符 */ private Names names; /** * 日志處理 */ private Messager messager; private Filer filer; public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); this.trees = JavacTrees.instance(processingEnv); Context context = ((JavacProcessingEnvironment)processingEnv).getContext(); this.treeMaker = TreeMaker.instance(context); messager = processingEnvironment.getMessager(); this.names = Names.instance(context); filer = processingEnvironment.getFiler(); }
基本成員變量說明:
- 1 JavacTrees 這個(gè)是當(dāng)前的java語法樹變量
- 2 TreeMaker 這個(gè)是創(chuàng)建或修改方法的AST變量
- 3 Names 這個(gè)是獲取變量用的
- 4 Messager 這個(gè)是打印日志的變量
- 5 Filer 做一些過濾使用的
注: 使用AST語法需要使用本地包 tools.jar 包
<dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8</version> <scope>system</scope> <systemPath>${java.home}/../lib/tools.jar</systemPath> </dependency>
- 2 重寫process方法
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { Set<? extends Element> annotation = roundEnv.getElementsAnnotatedWith(Data.class); annotation.stream().map(element -> trees.getTree(element)).forEach(tree -> tree.accept(new TreeTranslator() { @Override public void visitClassDef(JCClassDecl jcClass) { //過濾屬性 Map<Name, JCVariableDecl> treeMap = jcClass.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE)) .map(tree -> (JCVariableDecl)tree) .collect(Collectors.toMap(JCVariableDecl::getName, Function.identity())); //處理變量 treeMap.forEach((k, jcVariable) -> { messager.printMessage(Diagnostic.Kind.NOTE, String.format("fields:%s", k)); try { //增加get方法 jcClass.defs = jcClass.defs.prepend(generateGetterMethod(jcVariable)); //增加set方法 jcClass.defs = jcClass.defs.prepend(generateSetterMethod(jcVariable)); } catch (Exception e) { messager.printMessage(Diagnostic.Kind.ERROR, Throwables.getStackTraceAsString(e)); } }); //增加toString方法 jcClass.defs = jcClass.defs.prepend(generateToStringBuilderMethod()); super.visitClassDef(jcClass); } @Override public void visitMethodDef(JCMethodDecl jcMethod) { //打印所有方法 messager.printMessage(Diagnostic.Kind.NOTE, jcMethod.toString()); //修改方法 if ("getTest".equals(jcMethod.getName().toString())) { result = treeMaker .MethodDef(jcMethod.getModifiers(), getNameFromString("testMethod"), jcMethod.restype, jcMethod.getTypeParameters(), jcMethod.getParameters(), jcMethod.getThrows(), jcMethod.getBody(), jcMethod.defaultValue); } super.visitMethodDef(jcMethod); } })); return true; }
上面邏輯分別實(shí)現(xiàn)了getter方法 setter方法 toString方法
大致邏輯:
1 過濾包含Data的 Element 變量 2 根據(jù) Element 獲取AST語法樹 3 創(chuàng)建語法翻譯器重寫 visitClassDef
和 visitMethodDef
方法 4 過濾變量生成 get方法 set方法 和 toString方法
- 3 get方法實(shí)現(xiàn)
private JCMethodDecl generateGetterMethod(JCVariableDecl jcVariable) { //修改方法級(jí)別 JCModifiers jcModifiers = treeMaker.Modifiers(Flags.PUBLIC); //添加方法名稱 Name methodName = handleMethodSignature(jcVariable.getName(), "get"); //添加方法內(nèi)容 ListBuffer<JCStatement> jcStatements = new ListBuffer<>(); jcStatements.append( treeMaker.Return(treeMaker.Select(treeMaker.Ident(getNameFromString("this")), jcVariable.getName()))); JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList()); //添加返回值類型 JCExpression returnType = jcVariable.vartype; //參數(shù)類型 List<JCTypeParameter> typeParameters = List.nil(); //參數(shù)變量 List<JCVariableDecl> parameters = List.nil(); //聲明異常 List<JCExpression> throwsClauses = List.nil(); //構(gòu)建方法 return treeMaker .MethodDef(jcModifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null); }
- 4 set方法實(shí)現(xiàn)
private JCMethodDecl generateSetterMethod(JCVariableDecl jcVariable) throws ReflectiveOperationException { //修改方法級(jí)別 JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC); //添加方法名稱 Name variableName = jcVariable.getName(); Name methodName = handleMethodSignature(variableName, "set"); //設(shè)置方法體 ListBuffer<JCStatement> jcStatements = new ListBuffer<>(); jcStatements.append(treeMaker.Exec(treeMaker .Assign(treeMaker.Select(treeMaker.Ident(getNameFromString("this")), variableName), treeMaker.Ident(variableName)))); //定義方法體 JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList()); //添加返回值類型 JCExpression returnType = treeMaker.Type((Type)(Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance())); List<JCTypeParameter> typeParameters = List.nil(); //定義參數(shù) JCVariableDecl variableDecl = treeMaker .VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariable.name, jcVariable.vartype, null); List<JCVariableDecl> parameters = List.of(variableDecl); //聲明異常 List<JCExpression> throwsClauses = List.nil(); return treeMaker .MethodDef(modifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null); }
- 5 toString方法實(shí)現(xiàn)
private JCMethodDecl generateToStringBuilderMethod() { //修改方法級(jí)別 JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC); //添加方法名稱 Name methodName = getNameFromString("toString"); //設(shè)置調(diào)用方法函數(shù)類型和調(diào)用函數(shù) JCExpressionStatement statement = treeMaker.Exec(treeMaker.Apply(List.of(memberAccess("java.lang.Object")), memberAccess("com.nicky.lombok.adapter.AdapterFactory.builderStyleAdapter"), List.of(treeMaker.Ident(getNameFromString("this"))))); ListBuffer<JCStatement> jcStatements = new ListBuffer<>(); jcStatements.append(treeMaker.Return(statement.getExpression())); //設(shè)置方法體 JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList()); //添加返回值類型 JCExpression returnType = memberAccess("java.lang.String"); //參數(shù)類型 List<JCTypeParameter> typeParameters = List.nil(); //參數(shù)變量 List<JCVariableDecl> parameters = List.nil(); //聲明異常 List<JCExpression> throwsClauses = List.nil(); return treeMaker .MethodDef(modifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null); } private JCExpression memberAccess(String components) { String[] componentArray = components.split("\\."); JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0])); for (int i = 1; i < componentArray.length; i++) { expr = treeMaker.Select(expr, getNameFromString(componentArray[i])); } return expr; } private Name handleMethodSignature(Name name, String prefix) { return names.fromString(prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, name.toString())); } private Name getNameFromString(String s) { return names.fromString(s); }
最后是通過 SPI 的方式加載注解處理器,spi 可以用 java 自帶的方式,具體用法可以參考我的文章:框架基礎(chǔ)之SPI機(jī)制 , 這里我們使用 google 封裝的 auto-service 框架來實(shí)現(xiàn)。
在pom文件中引入
<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
注解
@SupportedAnnotationTypes({"com.nicky.lombok.annotation.Data"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) @AutoService(Processor.class) public class DataProcessor extends AbstractProcessor { }
最后就是 mvn clean install
打包到本地倉庫作為一個(gè)公共包
[INFO] Installing /Users/chenxing/Documents/sourcecode/id-generator-spring-boot-starter/lombok-enchance/target/java-feature.jar to /Users/chenxing/m2repository/com/nicky/lombok-enchance/1.0.4/lombok-enchance-1.0.4.jar [INFO] Installing /Users/chenxing/Documents/sourcecode/id-generator-spring-boot-starter/lombok-enchance/pom.xml to /Users/chenxing/m2repository/com/nicky/lombok-enchance/1.0.4/lombok-enchance-1.0.4.pom [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.372 s [INFO] Finished at: 2022-09-03T10:44:27+08:00 [INFO] ------------------------------------------------------------------------ ? lombok-enchance git:(master) ?
我們測(cè)試下,我們的注解處理器是否按所想的那樣,實(shí)現(xiàn)了相應(yīng)功能。
在項(xiàng)目中引入本地依賴 例如我的倉庫依賴坐標(biāo):
<dependency> <groupId>com.nicky</groupId> <artifactId>lombok-enchance</artifactId> <version>1.0.4</version> </dependency>
給LombokTest 類添加 @Data 注解
@Data public class LombokTest { private String name; private int age; public LombokTest(String name) { this.name = name; } public static void main(String[] args) { LombokTest lombokTest = new LombokTest("nicky"); lombokTest.age = 18; System.out.println(lombokTest.toString()); } }
我們編譯上面的類,查看 class文件是否生成了getField() setField() toString()
方法
public class LombokTest { private java.lang.String name; private int age; public java.lang.String toString() { /* compiled code */ } public void setName(java.lang.String name) { /* compiled code */ } public java.lang.String getName() { /* compiled code */ } public void setAge(int age) { /* compiled code */ } public int getAge() { /* compiled code */ } public LombokTest(java.lang.String name) { /* compiled code */ } public static void main(java.lang.String[] args) { /* compiled code */ } }
成功啦 ??
最后測(cè)試下main方法
打印結(jié)果如下:
{"name":"清水","age":18}
說明toString
方法生效了。
當(dāng)然對(duì)于 get 和 set 方法 直接在IDE工具里還是無法調(diào)用的,需要編寫 IDE 的 Lombok 插件,這里就不去擴(kuò)展了。
Reference
以上就是APT 注解處理器實(shí)現(xiàn) Lombok 常用注解功能詳解的詳細(xì)內(nèi)容,更多關(guān)于APT 實(shí)現(xiàn)Lombok注解功能的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MybatisPlus?自定義插件實(shí)現(xiàn)攔截SQL修改功能(實(shí)例詳解)
這篇文章主要介紹了MybatisPlus?自定義插件實(shí)現(xiàn)攔截SQL修改功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-11-11Java System類詳解_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
System類是jdk提供的一個(gè)工具類,有final修飾,不可繼承,由名字可以看出來,其中的操作多數(shù)和系統(tǒng)相關(guān)。這篇文章主要介紹了Java System類詳解_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理,需要的朋友可以參考下2017-04-04使用java打印心型、圓形圖案的實(shí)現(xiàn)代碼
這篇文章主要介紹了使用java打印心型、圓形圖案的實(shí)現(xiàn)代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12Java基于循環(huán)遞歸回溯實(shí)現(xiàn)八皇后問題算法示例
這篇文章主要介紹了Java基于循環(huán)遞歸回溯實(shí)現(xiàn)八皇后問題算法,結(jié)合具體實(shí)例形式分析了java的遍歷、遞歸、回溯等算法實(shí)現(xiàn)八皇后問題的具體步驟與相關(guān)操作技巧,需要的朋友可以參考下2017-06-06mybatis?實(shí)體類字段大小寫問題?字段獲取不到值的解決
這篇文章主要介紹了mybatis?實(shí)體類字段大小寫問題?字段獲取不到值的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
Spring Security是一個(gè)可以為Java應(yīng)用程序提供全面安全服務(wù)的框架,同時(shí)它也可以輕松擴(kuò)展以滿足自定義需求,本文主要介紹了SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證,感興趣的可以了解一下2023-11-11