CharLemAznable / blog

When I was young, I used to think that money was the most important thing in life, now that I am old, I know it is.
MIT License
4 stars 0 forks source link

Lombok it! #24

Open CharLemAznable opened 2 years ago

CharLemAznable commented 2 years ago

最近做了个小玩意儿, 要在后端的Java应用里嵌码埋点日志.

手动埋点日志比较简单, 思路就是创建一个独立的logback-LoggerContext, 获取一个干净的logger, 单独配置其Appender处理日志, 只要按slf4j的接口封装出API, 就能基本满足大部分场景的日志需求.

void log(org.slf4j.event.Level level, String msg);
void log(org.slf4j.event.Level level, String format, Object arg);
void log(org.slf4j.event.Level level, String format, Object arg1, Object arg2);
void log(org.slf4j.event.Level level, String format, Object... argArray);
void log(org.slf4j.event.Level level, String msg, Throwable t);

难点在于自动埋点.

第一时间想起的是Instrumentation, 之前也尝试过, [#9 浅尝Java Instrumentation], 只要配合ASM或者Javassist就能实现. 但是有两点不爽的: 一是目标应用启动时要添加agent参数, 而且附带需要分发埋点工具的jar包; 二是已经玩过了, 炒冷饭有点难受.

这时候就想起来一直很好奇的lombok, 它可以SneakyThrows, 可以Cleanup, 还可以Slf4j, 它是如何完成那些神奇的功能的呢?

还是要多学多练.

CharLemAznable commented 2 years ago

Java的编译过程大致可以分为三个阶段:

而lombok正是利用注解处理这一步进行实现的. lombok使用的是JDK6实现的JSR 269: Pluggable Annotation Processing API(编译期的注解处理器), 它是在编译期时把lombok的注解代码, 通过AST(抽象语法树)处理转换为常规的Java代码而实现优雅地编程的.

知道原理远远不够, 所谓"绝知此事要躬行", 利用lombok实现自己的功能才是目的.

参考lombok的文档: Contributing to Project Lombok's development

先获取lombok的源码: lombok git repository on github 本地执行ant eclipse或者ant intellij, 再用eclipse或者intellij打开工程 执行ant dist进行打包, 成功后在dist路径下就能看到打包后的lombok.jar

PS: 需使用JDK11环境打包

CharLemAznable commented 2 years ago

参考文档: Lombok Execution Path

可以看到lombok的加载过程:

首先根据META-INF/services/javax.annotation.processing.Processor文件的指示, 加载lombok.launch.AnnotationProcessorHider$AnnotationProcessor处理器, 这个处理器实际上只是一个入口, 主要的功能是启动ShadowClassLoader用以加载其他的实现类.

ShadowClassLoader作为lombok的中枢, 并不加载.class后缀的类文件, 而是加载.SCL.lombok后缀的类文件, lombok使用这个特殊的后缀是为了向IDE等隐藏其内部的类以及实现, 参考lombok源码中的buildScripts/compile.ant.xml文件可以看到大部分的.class文件被做了处理:

<include name="module-info.class" />
<include name="lombok/*.class" />
<include name="lombok/experimental/**" />
<include name="lombok/extern/**" />
<include name="lombok/launch/**" />
<include name="lombok/delombok/ant/Tasks*" />
<include name="lombok/javac/apt/Processor.class" />
<include name="lombok/META-INF/**" />

ShadowClassLoader加载的lombok.core.AnnotationProcessor其实也只是一个代理处理器, 依据lombok的执行环境, 在javac环境中启动lombok.javac.apt.LombokProcessor, 或者在ecj(eclipse's compiler)环境提示警告/错误. PS: ecj环境中需要使用javaagent方式使用lombok

最后, lombok.javac.apt.LombokProcessor才是最终完成lombok功能的注解处理器.

lombok使用Java SPI机制加载各注解的对应处理器, 根据优先级顺序进行处理, 例如SneakyThrows需要在NonNull后处理.

CharLemAznable commented 2 years ago

现在可以实现嵌码埋点工具了.

新建一个@Retention(RetentionPolicy.SOURCE)标注的注解 新建一个继承lombok.javac.JavacAnnotationHandler的注解处理器, 添加@Provides注解使其可以被加载 抄抄改改HandleSneakyThrowsHandleCleanup, 都是一些简单的AST操作

比较特殊的是void类型的方法, 最后一句return;可省略, 确实有点让人头疼

还是得吐槽一句: 语法糖真不是个好东西!

CharLemAznable commented 2 years ago

埋点工具做完了, 接下来就要打包分发了.

打包时需要注意, 因为注解处理器由ShadowClassLoader加载, 所以如果注解处理器类不在compile.ant.xml中配置的类文件转换的路径下, 需要修改compile.ant.xml文件后再执行ant dist打包.

为了不替代公共的org.projectlombok.lombok, 仅作为扩展功能包进行分发, 需要将编译好的类文件提取出来.

在lombok工程的dist路径下, unzip解压lombok-${version}.jar, 提取注解的.class文件和注解处理器的.SCL.lombok文件.

新建工程, 在resources目录中按类路径放置提取的类文件, 添加META-INF/services/lombok.javac.JavacAnnotationHandler文件, 填入注解处理器的完整类名保证SPI加载.

此外, 参考ShadowClassLoader的javadoc: check any jar files other than our own, loading them via this classloader, if they have a file META-INF/ShadowClassLoader that contains a line of text with lombok 所以添加META-INF/ShadowClassLoader文件, 填入lombok.

剩下的就是朴实无华且枯燥的日常操作了, maven打包, 和手动埋点工具包一起发布到nexus.

业务工程依赖埋点lombok扩展时, 和lombok一样添加<scope>provided</scope>即可兔死狗烹 鸟尽弓藏.