深入Java代码生成与AST操作——通过拓展Lombok实现@Nullable: 在长调用链下任意语句返回空值的处理

Lombok中,打个注解就能自动生成Getter,Setter方法,Builder类;而且还能通过注解在JDK8中使用var、val关键字,这是如何实现的?

基础原理,就是通过JDK提供的Annotation Processor实现:自定义Processor,继承AbstractProcessor,并在META-INF/services/javax.annotation.processing.Processor中,指定自定义的Processor的全限定类名(也可直接使用google的AutoService工具)。这样打包后,其它程序引入此包打包时,在编译期,就会由SPI机制加载自定义的Processor,对程序进行语法处理。

由于Java9中引入了模块化,并修改了众多API,并将javac相关的部分内部API设为了不可见,需要额外进行配置。Java8中的代码生成方式不再适。因此Lombok的核心层,在Java8与Java9不尽相同。如下图:
请添加图片描述
对Lombok原理的解读与模仿实现的博客很多,不再拾人牙慧,本文简单介绍如何通过拓展Lombok的方式,享受Lombok项目对抽象语法树(AST)的基建,更加方便的拓展自定义注解。
本文所提到的代码已托管至github LombokExtensions

下载并在IDE中配置原生Lombok

参考文章:
Lombok Execution Path
Contributing to Project Lombok’s development

  1. 克隆Lombok官方git仓库 本博客基于1.18.33版本的Lombok
  2. 下载ant
    Lombok使用ant构建,如果使用mac,可以直接使用brew install ant。Linux可换成apt或yum等。Windows自行下载并配置环境变量。
  3. 根据你使用的IDE,在Lombok工程根目录下执行ant eclipseant intellij
  4. 使用IDE打开Lombok项目
  5. (可选)执行ant dist构建Lombok,构建完成后,项目根目录/dist/lombok-1.18.xx.jar即我们平时通过Maven引入的Lombok的jar包。可cd到此目录下,使用jar -xf lombok-1.18.xx.jar查看jar包内容,与平时通过Maven引入的Lombok jar包内容一致。
  6. (可选)新建一个项目,引入本地打包的lombok jar包
    项目根目录下新建lib目录,将打包好的lombok jar包放进去,再在pom.xml里添加如下依赖即可。
    <dependency>
    	<groupId>org.projectlombok</groupId>
    	<artifactId>lombok</artifactId>
    	<scope>system</scope>
    	<systemPath>${project.basedir}/lib/lombok-1.18.xx.jar</systemPath>
    </dependency>
    

Lombok项目结构

Lombok定义了一套AnnocationProcessor,我们并不需要关注其AnnocationProcessor如何实现,需要明白的是,它会将打上自定义注解的AST节点 分派到对应到Handler上,自定义Annocation需要实现的就是这个Handler

拓展自定义注解及Handler

实现目标

  1. @Nullable 捕获长链调用中的NullPointerException,并返回null或基础类型的默认值
    before:
    // 获取学生的班级名称(可能引发NullPointerException的写法)
    String getStuClassName(String stuId) {
    	String classId = stuService.findById(stuId).getClassId();
    	return classService.findById(classId).getName();
    }
    // 获取学生的班级名称(不引发NullPointerException的写法)
    String getStuClassName(String stuId) {
    	try {
    		String classId = stuService.findById(stuId).getClassId();
    		return classService.findById(classId).getName();
    	} catch (NullPointerException e) {
    		return null;
    	}
    }
    
    after:
    String getStuClassName(String stuId) {
    	@Nullable
    	String classId = stuService.findById(stuId).getClassId();
    	@Nullable
    	String className = classService.findById(classId).getName();
    	return className;
    }
    // 或者也可以这样写
    @Nullable
    String getStuClassName(String stuId) {
    	String classId = stuService.findById(stuId).getClassId();
    	String className = classService.findById(classId).getName();
    	return className;
    }
    
    非常遗憾的是,Java的Annocation,不支持Statement级别的注解,否则我们可以写成这样,更加直观,并且也不必在赋值时才能使用此注解。
    String getStuClassName(String stuId) {
    	String classId = @Nullable stuService.findById(stuId).getClassId();
    	return @Nullable classService.findById(classId).getName();
    }
    

实现过程

首先明确,我们只实现Javac的编译,不对eclipse的编译时做修改。
要实现@Nullable,需要对Java的Statement级别的AST进行修改,Javac的官方文档并未介绍AST相关的模块,Lombok也没有类似的“新手教程”文档。所以需要一个参考。
好在,Lombok中存在可参考的实现。

  1. @PrintAST (Lombok内部注解)可参考此注解的实现,方便在实现自定义注解时,打印AST,以Debug
  2. @Cleanup 可参考此注解的实现,明白如何替换、删除原本的语句,与在定义局部变量时,在AST中插入try-catch新节点(JavacTreeMaker::Try)
  3. @EqualsAndHashCode 可参考此注解的实现,明白如何获取当前定义的局部变量是基本类型还是对象类型
  4. @SneakyThrows 可参考此注解的实现,明白如何在定义方法时,在AST中插入try-catch新节点,并知道如何定义&引入一个错误类型。
  5. @Getter 可参考此注解的实现,了解如何定义一个赋值语句。(JavacTreeMaker::Assign)
package lombok.javac.handlers;

import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import lombok.Lombok;
import lombok.Nullable;
import lombok.core.AnnotationValues;
import lombok.core.HandlerPriority;
import lombok.javac.*;
import lombok.spi.Provides;


import java.io.PrintStream;


import static lombok.javac.Javac.*;
import static lombok.javac.handlers.HandleDelegate.HANDLE_DELEGATE_PRIORITY;
import static lombok.javac.handlers.JavacHandlerUtil.*;


@Provides
@HandlerPriority(HANDLE_DELEGATE_PRIORITY + 99)
public class HandleNullable extends JavacAnnotationHandler<Nullable> {

    private static boolean eq(String typeTreeToString, String key) {
        return typeTreeToString.equals(key) || typeTreeToString.equals("lombok." + key) || typeTreeToString.equals("lombok.experimental." + key);
    }

    @Override
    public void handle(final AnnotationValues<Nullable> annotation, final JCAnnotation ast, final JavacNode annotationNode) {
        if (inNetbeansEditor(annotationNode)) return;

        switch (annotationNode.up().getKind()) {
            case METHOD:
                handleMethodNullable(annotation, ast, annotationNode);
                break;
            case LOCAL:
                handleLocalNullable(annotation, ast, annotationNode);
                break;
            default:
                annotationNode.addError("@Nullable is only supported on method parameters and local variables.");
                return;
        }

    }

    private void handleMethodNullable(final AnnotationValues<Nullable> annotation, final JCAnnotation ast, final JavacNode annotationNode) {
        // TODO 整个方法可空的处理
    }

    private void handleLocalNullable(final AnnotationValues<Nullable> annotation, final JCAnnotation ast, final JavacNode annotationNode) {
        JCVariableDecl decl = (JCVariableDecl) annotationNode.up().get();
        if (decl.init == null) {
            annotationNode.addError("@Nullable variable declarations need to be initialized.");
            return;
        }

        // 获取被Nullable修饰的语句的上级语句块
        JavacNode ancestor = annotationNode.up().directUp();
        JCTree blockNode = ancestor.get();
        final List<JCStatement> statements;
        if (blockNode instanceof JCBlock) {
            statements = ((JCBlock) blockNode).stats;
        } else if (blockNode instanceof JCCase) {
            statements = ((JCCase) blockNode).stats;
        } else if (blockNode instanceof JCMethodDecl) {
            statements = ((JCMethodDecl) blockNode).body.stats;
        } else {
            annotationNode.addError("@Nullable is legal only on a local variable declaration inside a block.");
            return;
        }

        // 获取被 @Nullable 修饰的语句前后的语句
        boolean seenDeclaration = false;
        ListBuffer<JCStatement> beforeStatements = new ListBuffer<JCStatement>();
        ListBuffer<JCStatement> afterStatements = new ListBuffer<JCStatement>();
        for (JCStatement statement : statements) {
            if (statement == decl){
                seenDeclaration = true;
                continue;
            }
            if (!seenDeclaration) {
                beforeStatements.append(statement);
            } else {
                afterStatements.append(statement);
            }
        }

        JavacTreeMaker maker = annotationNode.getTreeMaker();

        // 构建 try-catch 语句
        ListBuffer<JCAnnotation> newAnnotations = new ListBuffer<JCAnnotation>();
        for (JCAnnotation anno : decl.mods.annotations) {
            if (anno.getAnnotationType().toString().endsWith("Nullable")) {
                continue;
            }
            newAnnotations.append(anno);
        }
        decl.mods.annotations = newAnnotations.toList();
        JCVariableDecl defDecl = maker.VarDef(decl.mods, decl.name, decl.vartype, null);

        JCStatement tryAssign = maker.Exec(maker.Assign(maker.Ident(decl.name), decl.getInitializer()));

        JCStatement catchAssign = maker.Exec(maker.Assign(maker.Ident(decl.name), createDefaultInitializer(defDecl, maker)));

        JCVariableDecl exceptionDef = maker.VarDef(
                maker.Modifiers(Flags.FINAL | Flags.PARAMETER),
                annotationNode.toName("e"),
                chainDots(annotationNode, "java.lang.NullPointerException".split("\\.")),
                null
        );

        JCTry tryStatement = maker.Try(
                maker.Block(0, List.of(tryAssign)),
                List.of(maker.Catch(exceptionDef, maker.Block(0, List.of(catchAssign)))),
                maker.Block(0, List.<JCStatement>nil())
        );

        // 构建新代码块
        ListBuffer<JCStatement> newStatements = new ListBuffer<JCStatement>();
        newStatements.appendList(beforeStatements);
        newStatements.append(defDecl);
        newStatements.append(tryStatement);
        newStatements.appendList(afterStatements);


        if (blockNode instanceof JCBlock) {
            ((JCBlock)blockNode).stats = newStatements.toList();
        } else if (blockNode instanceof JCCase) {
            ((JCCase)blockNode).stats = newStatements.toList();
        } else if (blockNode instanceof JCMethodDecl) {
            ((JCMethodDecl)blockNode).body.stats = newStatements.toList();
        } else throw new AssertionError("Should not get here");
        System.out.println(annotationNode.up().up().get());
        ancestor.rebuild();
    }

    private JCExpression createDefaultInitializer(JCVariableDecl defDecl, JavacTreeMaker maker) {
        if (defDecl.vartype instanceof JCPrimitiveTypeTree) {
            switch (((JCPrimitiveTypeTree) defDecl.vartype).getPrimitiveTypeKind()) {
                case BOOLEAN:
                    return maker.Literal(CTC_BOOLEAN, 0);
                case CHAR:
                    return maker.Literal(CTC_CHAR, 0);
                default:
                case BYTE:
                case SHORT:
                case INT:
                    return maker.Literal(CTC_INT, 0);
                case LONG:
                    return maker.Literal(CTC_LONG, 0L);
                case FLOAT:
                    return maker.Literal(CTC_FLOAT, 0F);
                case DOUBLE:
                    return maker.Literal(CTC_DOUBLE, 0D);
            }
        }
        return maker.Literal(CTC_BOT, null);
    }


    private void printAST(final AnnotationValues<Nullable> annotation, final JCAnnotation ast, final JavacNode annotationNode) {
        PrintStream stream = System.out;
        try {
            annotationNode.up().traverse(new JavacASTVisitor.Printer(true, stream));
        } finally {
            if (stream != System.out) {
                try {
                    stream.close();
                } catch (Exception e) {
                    throw Lombok.sneakyThrow(e);
                }
            }
        }
    }
}

重新编译并引入自己的项目中

  1. 在Lombok的项目中运行ant dist` 编译
  2. 在其它项目中引入编译后的jar包
    	<dependency>
    	            <groupId>org.projectlombok</groupId>
    	            <artifactId>lombok</artifactId>
    	            <version>1.18.33</version>
    	            <scope>system</scope>
    	            <systemPath>/Users/xxx/project/lombok/dist/lombok-1.18.33.jar</systemPath>
    	</dependency>
    
  3. 使用自定义的@Nullable注解即可
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值