如何让Java编译器帮你写代码
Tech导读
本文结合京东监控埋点场景,对解决样板代码的技术选型方案进行分析,给出最终解决方案后,结合理论和实践进一步展开。通过关注文中的技术分析过程和技术场景,读者可收获一种样板代码思想过程和解决思路,并对Java编译器底层有初步了解。
导读
本文结合京东监控埋点场景,对解决样板代码的技术选型方案进行分析,给出最终解决方案后,结合理论和实践进一步展开。通过关注文中的技术分析过程和技术场景,读者可收获一种样板代码思想过程和解决思路,并对Java编译器底层有初步了解。01 背景
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
监控是服务端应用需要具备的一个非常重要的能力,通过监控可以直观的看到核心业务指标、服务运行质量等,而要做到可监控就需要进行相应的监控埋点。大家在埋点过程中经常会编写大量重复代码,虽能实现基本功能,但耗时耗力,不够优雅。根据“DRY(Don't Repeat Yourself)"原则,这是代码中的“坏味道”,对有代码洁癖的人来讲,这种重复是不可接受的。
本文主要是结合监控埋点这个场景分享一种解决样板化代码的思路,希望能起到抛砖引玉的作用。下面将从组件介绍、技术选型过程、实现原理及部分源码实现逐步展开讲解。
02 组件介绍
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕
京东内部监控系统叫UMP,与所有的监控系统一样,核心部分有埋点、上报、分析整合、报警、看板等等,本文讲的组件主要是为对监控埋点原生能力的增强,提供一种更优雅简洁的实现。
下面先来看下传统硬编码的埋点方式,主要分为创建埋点对象、可用率记录、提交埋点 3 个步骤:
(注:结合京东实际业务场景,组件实现了fallback、自定义可用率、重名方法区分、配套的IDE插件、监控key自定义生成规则等细节功能,由于本文主要是讲解底层实现原理,详细功能不在此赘述,感兴趣的京东同事可以内网联系咨询:liushijie3)
03 技术选型过程
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
3.1 编译期
这里的编译期指将Java源文件编译为class字节码的过程。Java编译器提供了基于 JSR 269 规范[1]的注解处理器机制,通过操作AST (抽象语法树,Abstract Syntax Tree,下同)实现逻辑的织入。业内有不少基于此机制的应用,比如Lombok 、MapStruct 、JPA 等;此机制的优点是因为在编译期执行,可以将问题前置,没有多余依赖,因此做出来的工具使用起来比较方便。缺点也很明显,要熟练操作 AST并不是想的那么简单,不理解前后关联的流程写出来的代码不够稳定,因此要花大量时间熟悉编译器底层原理。当然这个过程对使用者来讲是没有感知的。
3.2 编译后
编译后是指编译成 class 字节码之后,通过字节码进行增强的过程。此阶段插桩需要适配不同的构建工具:Maven、Gradle、Ant、Ivy等,也需要使用方增加额外的构建配置,因此存在开发量大和使用不够方便的问题,首先要排除掉此选项。可能只有极少数场景下才会需要在此阶段插桩。
3.3 运行期
3.4 最终方案选择
04 插桩实现原理
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
4.1 编译器执行流程
根据官网资料[3]javac 处理流程可以粗略的分为 3个部分:Parse and Enter、Annotation Processing、Analyse and Generate,如下图:
4.2 相关javac源码导航
javac触发入口类路径是:com. sun. tools. javac. Main,代码如下:
经验证Maven 执行构建调的是此类中的main方法。其他构建工具未做验证,猜测类似的。在JDK内部也提供了javax. tools. Tool Provider# get System Java Compiler的入口,实际上内部实现也是调的这个类里的compile方法。
经过一系列的命令参数解析和初始化操作,最终调到真正的核心入口,方法是com. sun. tools. javac. main. Java Compiler# compile,如下图:
4.3 注解处理器
Java从JDK 1.6 开始,引入了基于JSR 269 规范的注解处理器,允许开发者在编译期间执行自己的代码逻辑。如本文讲的UMP监控埋点插桩组件一样,由此衍生出了很多优秀的技术组件,如前面提到的Lombok、Mapstruct等。注解处理器使用比较简单,后面示例代码有注解处理器简单实现也可以参考。这里重点讲一下注解处理器整体执行原理:
05 源码示例
05 源码示例
关于AST 操作的探索,早在2008年就有相关资料了[4],Lombok、Mapstruct都是开源的工具,也可以用来参考学习。这里简单讲一个示例,展示如何插桩。
注解处理器使用框架
上图展示了注解处理器具体的基本使用框架,init、process是注解处理器的核心方法,前者是初始化注解处理器的入口,后者是操作AST的入口。javac还提供了一些有用的工具类,比如:
TreeMaker:创建AST的工厂类,所有的节点都是继承自JCTree,并通过TreeMaker完成创建。
JavacElements:操作Element的工具类,可以用来定位具体AST。
向类中织入一个import节点
这里举一个简单场景,向类中织入一个import节点:
为方便理解对代码实现做了简化,可以配合注释查看如何织入:
总的来说,织入逻辑是通过TreeMaker创建AST 节点,并操作现有AST织入创建的节点,从而达到了织入代码的目的。
06 反思与总结
06 反思与总结
到这里,讲了埋点组件的使用、技术选型、以及插桩相关的内容,最终开发出来的组件在工作中也起到了很好的效果。但是在这个过程中有一些反思。
1、插桩门槛高
通过前面的内容不难得出一个事实,要实现一个小小的功能,需要开发者花费大量的精力去学习理解编译器底层的一些原理。从ROI角度看,投入和产出是严重不成正比的。为了能提供可靠的实现,个人花费了大量业余时间去做技术选型分析和编译器相关知识,可以说是纯靠个人的兴趣和一股倔劲一点点搭建起来的,细节是魔鬼,这个踩坑的过程比较枯燥。实际上插桩机制有很多通用的场景可以探索,之所以一直很少见到此类机制的应用。主要是其门槛较高,对大多数开发者来说比较陌生。因此降低开发者使用门槛才能让一些想法变成现实。做一把好用的锤子,比砸入一个钉子要更有价值。
在监控埋点插桩组件真正落地时,在项目内做了一定抽象,并支持了一些开关、自定义链路跟踪等功能。但从作用范围来讲是不够的,所以下一步计划做一个插桩方面的技术框架,从易用性、可维护性等方面做好进一步的抽象,同时做好可测试性相关工作,包含验证各版本JDK的支持、各种Java语法的覆盖等。
2、插桩是把双刃剑
javac官方对修改AST的方式持保守态度,也存在一些争议。然而时间是最好的验证工具,从Lombok 等组件的发展看出,插桩机制是能经住长久考验的。如何合理利用这种能力是非常重要的,合理使用可使系统简洁优雅,使用不当就等于在代码里下毒了。所以要有节制的修改AST,要懂前后运行机制,围绕通用的场景使用,避免滥用。
3、认识当前上下文环境的局限性
遇到问题时,如果在当前的上下文环境里找不到合适的解决方案,从这个环境跳出来换个维度也许能看到不同的风景。就像物理机到虚拟机再到现在的容器,都是打破了原来的规则逐步发展出新的技术生态。大多数的开发工作都是基于一个高层次的封装上面进行,而突破往往都是从底层开始的,适当的时候也可以向下做一些探索,可能会产生一些有价值的东西。
参考文献
[1] JSR 269:
https://www.jcp.org/en/jsr/detail?id=269
[2] Understanding AOP Proxies:
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-understanding-aop-proxies
[3] Compilation Overview:
https://openjdk.org/groups/compiler/doc/compilation-overview/index.html
[4] The Hacker’s Guide to Javac:
http://scg.unibe.ch/archive/projects/Erni08b.pdf[5] JavaParser-AST-Inspector:
https://github.com/MysterAitch/JavaParser-AST-Inspector[6] OpenJDK source:
http://hg.openjdk.java.net/jdk8u/jdk8u60/langtools/[7] Graphviz Online:
https://dreampuf.github.io/GraphvizOnline/#digraph%20G%20%7B%7D前端工程化在WMS 6.0中的实践
可视化服务编排在金融APP中的实践
求分享
求点赞
求在看