lombok原理_程序员,你知道Lombok 原理分析与功能实现吗?

9426e74901f89620c49810670f13ace9.png

前言

这两天没什么重要的事情做,但是想着还要春招总觉得得学点什么才行,正巧想起来前几次面试的时候面试官总喜欢问一些框架的底层实现,但是我学东西比较倾向于用到啥学啥,因此在这些方面吃了很大的亏。而且其实很多框架也多而杂,代码起来费劲,无非就是几套设计模式套一套,用到的东西其实也就那么些,感觉没啥新意。刚这两天读”深入理解JVM”的时候突然想起来有个叫Lombok的东西以前一直不能理解他的实现原理,现在正好趁着闲暇的时间研究研究。(因为代码量较大,所以文章会显得很长,但都是很硬的干货,希望对大家是有帮助的,文末还有小彩蛋哦)

Lombok

代码

Lombok是一个开源项目,源代码托管在GITHUB/rzwitserloot,如果需要在maven里引用,只需要添加下依赖:

org.projectlombok lombok 1.16.8

功能

那么Lombok是做什么的呢?其实很简单,一个最简单的例子就是它能够实现通过添加注解,能够自动生成一些方法。比如这样的类:

@Getterclass 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文件如下:

4.0.0com.mythsman.test getter 1.0-SNAPSHOTjartestUTF-8com.sun tools 1.8system${java.home}/../lib/tools.jarorg.apache.maven.plugins maven-compiler-plugin 3.11.81.8

主要定义了下项目名,除了默认依赖的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;@Overridepublic 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对于我们来说比较陌生,因此写起来还是费了不少劲的:

@Overridepublic 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 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
  4. 创建一个jcVariableDeclList保存类的成员变量
  5. 遍历jcTree的所有成员(包括成员变量和成员函数和构造函数),过滤出其中的成员变量,并添加进jcVariableDeclList
  6. 将jcVariableDeclList的所有变量转换成需要添加的getter方法,并添加进jcClassDecl的成员中
  7. 调用默认的遍历方法遍历处理后的jcClassDecl
  8. 利用上面的TreeTranslator去处理jcTree

接下来再实现makeGetterMethodDecl方法:

private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) { ListBuffer 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()));}

逻辑就是读取变量的定义,并创建对应的Getter方法,并试图用驼峰命名法。

整体上难点还是集中在api的使用上,还有一些细微的注意点:

首先,messager的printMessage方法在打印log的时候会自动过滤重复的log信息。

其次,这里的list并不是java.util里面的list,而是一个自定义的list,这个list的用法比较坑爹,他采用的是这样的方式:

package com.sun.tools.javac.util;public class List extends AbstractCollection implements java.util.List { public A head; public List tail;  //...  List(A var1, List var2) { this.tail = var2; this.head = var1; }  public List prepend(A var1) { return new List(var1, this); }  public static  List of(A var0) { return new List(var0, nil()); }  public List append(A var1) { return of(var1).prependList(this); }  public static  List nil() { return EMPTY_LIST; } //...}

挺有趣的,用这种叫cons而不是list的数据结构,添加元素的时候就把自己赋给自己的tail,新来的元素放进head。不过需要注意的是这个东西不支持链式调用,prepend之后还要将新值赋给自己。

而且这里在创建getter方法的时候还要把参数写全写对了,尤其是添加this指针的这种用法。

测试类

上面基本就是所有功能代码了,接下来我们要写一个类来测试一下(App.java):

package com.mythsman.test;@Getterpublic 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 bashif [ -d classes ]; then rm -rf classes;fimkdir classesjavac -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.javajavap -p classes com/mythsman/test/App.classjava -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 processedNote: value2 has been processedCompiled 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/标签里添加下面的配置:

src/main/resourcesMETA-INF/**/*org.apache.maven.plugins maven-resources-plugin 2.6process-METAprepare-packagecopy-resourcestarget/classes${basedir}/src/main/resources/**/* ... 

我们知道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:

4.0.0com.mythsman.test app 1.0-SNAPSHOTjarmainhttp://maven.apache.orgUTF-8com.mythsman.test getter 1.0-SNAPSHOTorg.apache.maven.plugins maven-compiler-plugin 3.11.81.8

App.java:

package com.mythsman.test;@Getterpublic 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”。

文末小彩蛋

也是因为"金三银四"的原因,小编也是整理了一些Dubbo、Redis、Netty、zookeeper

Spring cloud、分布式、高并发等架构技术的学习视频和一些2018经典面试题,今天大家能看到这个小彩蛋的话也是缘分,所以小编也是打算把这些学习视频和经典面试题免费送给这些有缘分的程序员朋友,也是希望能够帮助大家。

面试资料获取方式:

转发+关注我,后台私信回复【资料】免费领取面试资料(助你金三银四能跳槽涨薪)

35b927d7c2e72f6240235b37b7d8f5e1.png
ee4f4e198568b1bc72c084d6c5b8616e.png
深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值