Lombok经常用,但是你知道它的原理是什么吗?(二)

在上一篇Lombok经常用,但是你知道它的原理是什么吗?简单介绍了注解处理器,是用来处理编译期的注解的一个工具,我们只是自己生成了一些代码,但是和Lombok却不一样,因为Lombok是在原有类的基础上增加了一些类,你那么Lombok是如何做到修改原有类的内容呢?接下来我们就再进一步了解Lombok的原理。

Javac原理

既然我们是在编译期对类进行操作了,那么我们就需要了解在Java中Javac到底对程序做了什么。Javac对代码编译的过程其实就是用Java来写的,我们可以查看其源码对其简单的分析,如何下载源码,Debug源码这里我就不进行分析了,推荐一篇文章写的挺好的。Javac 源码调试教程

编译过程大致分为了三个阶段

  • 解析与填充符号表
  • 注解处理
  • 分析与字节码生成

这三个阶段的交互过程如下图所示。

image

解析与填充符号表

这一步骤是两个步骤,包括了解析和填充符号,其中解析是分为词法分析语法分析两个步骤。

词法分析和语法分析

词法分析就是将源代码的字符流转变为Java中的标记(Token)集合,单个字符是程序编写过程中最小的元素,而标记(Token)则是编译过程中最小的元素,关键字、变量名、字面量、运算符都可以成为标记(Token)。比如在Java中int a = b+2,这段代码则表示了6个标记Token,分别是int、a、=、b、+、2。虽然关键字int是由三个字符构成的,但是它只是一个Token,不可以再拆分了。

语法分析是根据Token序列构造抽象对象树的过程,抽象语法树(Abstract syntax tree),是一种用来描述代码语法结构的树形表示方法,语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至是代码注释都是一个语法结构。

语法分析分析出来的树结构是由JCTree来表示的,我们可以看一下它的子类有哪些。

image

我们自己建一个类,可以观察它在编译过程中用树结构表示是一种怎样的结构。

public class HelloJvm {

    private String a;
    private String b;

    public static void main(String[] args) {
        int c = 1+2;
        System.out.println(c);
        print();
    }

    private static void print(){

    }
}

大家注意我划红线的地方,可以看到这些都是JCTree的子类。我们可以知道编译期的树是以JCCompilationUnit为根节点,然后作为类的构成元素例如方法、私有变量、class类,这些都是作为树的构成一种。

image

填充符号表

填充符号表和我们的Lombok原理关联不大,这里了解即可。

完成了语法分析和词法分析以后,下一步就是填充符号表的过程,符号表是由一组符号地址和符号信息构成的表格,可以将它想象成哈希表中的K-V值对的形式(符号表不一定是哈希表实现,可以使有序符号表,树状符号表、栈结构符号表等)。符号表中所登记的信息在编译的不同阶段都要用到,在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

注解处理器

第一步的解析和填充符号表完成以后,接下来就是我们的重头戏注解处理器了。因为在这一步就是Lombok实现原理的关键。

在JDK1.5之后,Java语言提供了对注解的支持,这些注解与普通的Java代码一样,是在运行期间发挥作用的。在JDK1.6中实现了对JSR-269的规范,提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,我们可以把它看作是一组编译器的插件,在这些插件里面,可以读取,修改,添加抽象语法树中的任意元素。

如果这些插件在处理注解期间对语法树进行了修改,那么编译器将回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有了再对语法树进行修改为止。每一次循环成为一个Round。

有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件之中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。只要有足够多的创意,程序员可以使用插入式注解处理器来实现许多原本只能在编码中完成的事情。

语义分析与字节码生成

语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源程序的抽象,但是无法保证源程序是符合逻辑的。而语义分析的主要任务就是对结构上正确的源程序进行上下文有关性质的审查,如进行类型检查。

比如我们有以下代码

int a = 1;
boolean b = false;
char c = 2;

下面我们有可能出现如下运算


int d = b+c;

其实上面的代码在结构上能构成准确的语法树,但是在语义上下面的运算是错误的。所以如果运行的话就会出现编译不通过,无法编译。

自己实现一个简单的Lombok

上面我们了解了javac的过程,那么我们直接来自己写一个简单的在已有类中添加代码的小工具,我们就只生成set方法。首先写一个自定义的注解类。

@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface MySetter {
}

然后写对于此注解类的注解处理器类

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("aboutjava.annotion.MySetter")
public class MySetterProcessor extends AbstractProcessor {

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

    /**
     * @Description: 1. Message 主要是用来在编译时期打log用的
     *              2. JavacTrees 提供了待处理的抽象语法树
     *              3. TreeMaker 封装了创建AST节点的一些方法
     *              4. Names 提供了创建标识符的方法
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.javacTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment)processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

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

此处我们注意我们在init方法中获得一些编译阶段的一些环境信息。我们从环境中提取出一些关键的类,描述如下。

  • JavacTrees:提供了待处理的抽象语法树
  • TreeMaker:封装了操作AST抽象语法树的一些方法
  • Names:提供了创建标识符的方法
  • Messager:主要是在编译器打日志用的

然后接下来我们利用所提供的工具类对已存在的AST抽象语法树进行修改。主要的修改逻辑存在于process方法中,如果返回是true的话,那么javac过程会再次重新从解析与填充符号表处开始进行。process方法的逻辑主要如下

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MySetter.class);
        elementsAnnotatedWith.forEach(e->{
            JCTree tree = javacTrees.getTree(e);
            tree.accept(new TreeTranslator(){
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    // 在抽象树中找出所有的变量
                    for (JCTree jcTree : jcClassDecl.defs){
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)){
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    // 对于变量进行生成方法的操作
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE,jcVariableDecl.getName()+"has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }
            });
        });
        return true;
    }

其实看起来比较难,原理比较简单,主要是我们对于API的不熟悉所以看起来不好懂,但是主要意思就是如下

  1. 找到@MySetter注解所标注的类,获得其语法树
  2. 遍历其语法树,找到其参数节点
  3. 自己建一个方法节点,并添加到语法树中

用图表示的话,我们建了一个测试类TestMySetter,我们知道其语法树的大致结构如下图所示。

image

那么我们的目标就是将其语法树变成下图所示,因为最终生成字节码是根据语法树来生成的,所以我们在语法树中添加了方法的节点,那么在生成字节码的时候就会生成对应方法的字节码。

image

其中生成方法节点的代码如下


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

    ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
    // 生成表达式 例如 this.a = a;
    JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
    statements.append(aThis);
    JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

    // 生成入参
    JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null);
    List<JCTree.JCVariableDecl> parameters = List.of(param);

    // 生成返回对象
    JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
    return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),getNewMethodName(jcVariableDecl.getName()),methodType,List.nil(),parameters,List.nil(),block,null);

}

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

private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
    return treeMaker.Exec(
            treeMaker.Assign(
                    lhs,
                    rhs
            )
    );
}

最后我们执行下面三个命令

javac -cp $JAVA_HOME/lib/tools.jar aboutjava/annotion/MySetter* -d
javac -processor aboutjava.annotion.MySetterProcessor aboutjava/annotion//TestMySetter.java
javap -p aboutjava/annotion/TestMySetter.class

可以看到输出的内容如下

Compiled from "TestMySetter.java"
public class aboutjava.annotion.TestMySetter {
  private java.lang.String name;
  public void setName(java.lang.String);
  public aboutjava.annotion.TestMySetter();
}

可以看到字节码中已经生成了我们需要的setName方法。

代码地址

总结

到目前为止大概将Lombok的原理讲明白了,其实就是对于抽象语法树的各种操作。其实大家还可以利用编译期做许多的事情,例如代码规范的检查之类的。这里我只写了关于set方法的创建,大家有兴趣的可以自己写代码自己试一下关于Lombok的get方法的创建。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容