如何让Java编译器帮你写代码
背景
监控是服务端应用需要具备的一个非常重要的能力,通过监控可以直观的看到核心业务指标、服务运行质量等,而要做到可监控就需要进行相应的监控埋点。大家在埋点过程中经常会编写大量重复代码,虽能实现基本功能,但耗时耗力,不够优雅。根据“DRY(Dont Repeater Yourself)"原则,这是代码中的“坏味道”,对有代码洁癖的人来讲,这种重复是不可接受的。那有什么方法解决这种“重复”吗?经过综合调研,基于前端编译器插桩技术,实现了一个埋点组件,通过织入埋点逻辑,让Java 编译器帮我们写代码。经过不断打磨,已经被包括京东APP主站服务端在内的很多团队广泛使用。
本文主要是结合监控埋点这个场景分享一种解决样板化代码的思路,希望能起到抛砖引玉的作用。下面将从组件介绍、技术选型过程、实现原理及部分源码实现逐步展开讲解。
组件介绍
京东内部监控系统叫UMP,与所有的监控系统一样,核心部分有埋点、上报、分析整合、报警、看板等等,本文讲的组件主要是为对监控埋点原生能力的增强,提供一种更优雅简洁的实现。
我们先来看下传统硬编码的埋点方式,主要分为创建埋点对象、可用率记录、提交埋点 3 个步骤:
通过上图可以看到,真正的逻辑只有红框中的范围,为了完成埋点要把这段代码都围绕起来,代码层级变深,可读性差,所有埋点都是这样的样板代码。
下面来看下使用组件后的埋点方式:
通过对比很容易看到,使用组件后的方式只要在方法上加一个注解就可以了,代码可读性有明显的提升。组件由埋点封装API和AST操作处理器 2 部分组成。
埋点API封装:在运行时被调用,对原生埋点做了封装和抽象,方便使用者进行监控KEY的扩展。AST操作处理器:在编译期调用,它将根据注解@UMP把埋点封装API按照规则织入方法体内。
(注:结合京东实际业务场景,组件实现了fallback、自定义可用率、重名方法区分、配套的IDE插件、监控key自定义生成规则等细节功能,由于本文主要是讲解底层实现原理,详细功能不在此赘述,感兴趣的京东同事可以内网联系咨询:liushijie3)
技术选型过程
通过上面的示例代码,相信很多人觉得这个功能很简单,用 Spring AOP 很快就能搞定了。的确很多团队也是这么做的,不过这个方案并不是那么完美,下面的选型分析中会有相关的解释,请耐心往下看。如下图,从软件的开发周期来看,可织入埋点的时机主要有 3 个阶段:编译期、编译后和运行期。
01编译期
这里的编译期指将Java源文件编译为class字节码的过程。Java编译器提供了基于 JSR 269 规范[1]的注解处理器机制,通过操作AST (抽象语法树,Abstract Syntax Tree,下同)实现逻辑的织入。业内有不少基于此机制的应用,比如Lombok 、MapStruct 、JPA 等;此机制的优点是因为在编译期执行,可以将问题前置,没有多余依赖,因此做出来的工具使用起来比较方便。缺点也很明显,要熟练操作 AST并不是想的那么简单,不理解前后关联的流程写出来的代码不够稳定,因此要花大量时间熟悉编译器底层原理。当然这个过程对使用者来讲是没有感知的。
02编译后
编译后是指编译成 class 字节码之后,通过字节码进行增强的过程。此阶段插桩需要适配不同的构建工具:Maven、Gradle、Ant、Ivy等,也需要使用方增加额外的构建配置,因此存在开发量大和使用不够方便的问题,首先要排除掉此选项。可能只有极少数场景下才会需要在此阶段插桩。
03运行期
运行期是指在程序启动后,在运行时进行增强的过程,这个阶段有 3 种方式可以织入逻辑,按照启动顺序,可以分为:静态 Agent、AOP 和动态 Agent。
1、 静态 Agent
JVM 启动时使用 -javaagent 载入指定 jar 包,调用 MANIFEST.MF 文件里的 Premain-Class 类的 premain 方法触发织入逻辑。是技术中间件最常使用的方式,借助字节码工具完成相关工作。应用此机制的中间件有很多,比如:京东内部的链路监控 pfinder、外部开源的 skywalking 的探针、阿里的 TTL 等等。这种方式优点是整体比较成熟,缺点主要是兼容性问题,要测试不同的 JDK 版本代价较大,出现问题只能在线上发现。同时如果不是专业的中间件团队,还是存在一定的技术门槛,维护成本比较高;
2、 Spring AOP
Spring AOP大家都不陌生,通过 Spring 代理机制,可以在方法调用前后织入逻辑。AOP 最大的优点是使用简单,同样存在不少缺点:
同一类内方法A调用方法B时,是无法走到切面的,这是Spring 官方文档的解释[2]