在成都网站设计、成都做网站中从网站色彩、结构布局、栏目设置、关键词群组等细微处着手,突出企业的产品/服务/品牌,帮助企业锁定精准用户,提高在线咨询和转化,使成都网站营销成为有效果、有回报的无锡营销推广。创新互联公司专业成都网站建设十载了,客户满意度97.8%,欢迎成都创新互联客户联系。
监控是服务端应用需要具备的一个非常重要的能力,通过监控可以直观的看到核心业务指标、服务运行质量等,而要做到可监控就需要进行相应的监控埋点。大家在埋点过程中经常会编写大量重复代码,虽能实现基本功能,但耗时耗力,不够优雅。根据“DRY(Don't Repeater Yourself)"原则,这是代码中的“坏味道”,对有代码洁癖的人来讲,这种重复是不可接受的。那有什么方法解决这种“重复”吗?经过综合调研,基于前端编译器插桩技术,实现了一个埋点组件,通过织入埋点逻辑,让Java 编译器帮我们写代码。经过不断打磨,已经被包括京东APP主站服务端在内的很多团队广泛使用。
本文主要是结合监控埋点这个场景分享一种解决样板化代码的思路,希望能起到抛砖引玉的作用。下面将从组件介绍、技术选型过程、实现原理及部分源码实现逐步展开讲解。
京东内部监控系统叫UMP,与所有的监控系统一样,核心部分有埋点、上报、分析整合、报警、看板等等,本文讲的组件主要是为对监控埋点原生能力的增强,提供一种更优雅简洁的实现。
我们先来看下传统硬编码的埋点方式,主要分为创建埋点对象、可用率记录、提交埋点 3 个步骤:
通过上图可以看到,真正的逻辑只有红框中的范围,为了完成埋点要把这段代码都围绕起来,代码层级变深,可读性差,所有埋点都是这样的样板代码。
下面来看下使用组件后的埋点方式:
通过对比很容易看到,使用组件后的方式只要在方法上加一个注解就可以了,代码可读性有明显的提升。组件由埋点封装API和AST操作处理器 2 部分组成。
(注:结合京东实际业务场景,组件实现了fallback、自定义可用率、重名方法区分、配套的IDE插件、监控key自定义生成规则等细节功能,由于本文主要是讲解底层实现原理,详细功能不在此赘述,感兴趣的京东同事可以内网联系咨询:liushijie3)
通过上面的示例代码,相信很多人觉得这个功能很简单,用 Spring AOP 很快就能搞定了。的确很多团队也是这么做的,不过这个方案并不是那么完美,下面的选型分析中会有相关的解释,请耐心往下看。如下图,从软件的开发周期来看,可织入埋点的时机主要有 3 个阶段:编译期、编译后和运行期。
这里的编译期指将Java源文件编译为class字节码的过程。Java编译器提供了基于 JSR 269 规范[1]的注解处理器机制,通过操作AST (抽象语法树,Abstract Syntax Tree,下同)实现逻辑的织入。业内有不少基于此机制的应用,比如Lombok 、MapStruct 、JPA 等;此机制的优点是因为在编译期执行,可以将问题前置,没有多余依赖,因此做出来的工具使用起来比较方便。缺点也很明显,要熟练操作 AST并不是想的那么简单,不理解前后关联的流程写出来的代码不够稳定,因此要花大量时间熟悉编译器底层原理。当然这个过程对使用者来讲是没有感知的。
编译后是指编译成 class 字节码之后,通过字节码进行增强的过程。此阶段插桩需要适配不同的构建工具:Maven、Gradle、Ant、Ivy等,也需要使用方增加额外的构建配置,因此存在开发量大和使用不够方便的问题,首先要排除掉此选项。可能只有极少数场景下才会需要在此阶段插桩。
运行期是指在程序启动后,在运行时进行增强的过程,这个阶段有 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 最大的优点是使用简单,同样存在不少缺点:
3、 动态 Agent
动态加载jar包,调用MANIFEST.MF文件中声明的Agent-Class类的agentmain方法触发织入逻辑。这种方式主要用来线上动态调试,使用此机制的中间件也有很多,比如:Btrace、Arthas等,此方式不适合常驻内存使用,因此要排除掉。
选择通过上面的分析梳理可知,要实现重复代码的抽象有 3 种方式:基于JSR 269 的插桩、基于 Java Agent 的字节码增强、基于Spring AOP的自定义切面。接下来进一步的对比:
如上表所示,从实现成本上来看,AOP 最简单,但这个方案不能覆盖所有场景,存在一定的局限性,不符合我们追求极致的调性,因此首先排除。Java Agent 能达到的效果与 JSR 269 相同,但是启动参数里需要增加 -javaagent 配置,有少量的运维工作,同时还有 JDK 兼容性的坑需要趟,对非中间件团队来说,这种方式从长久看会带来负担,因此也要排除。
基于 JSR 269 的插桩方式,对Java编译器工作流程的理解和 AST 的操作会带来实现上的复杂性,前期投入比较大,但是组件一旦成型,会带来一劳永逸的解决方案,可以很自信的讲,插桩实现的组件是监控埋点场景里的银弹(事实证明了这点,不然也不敢这么吹)。
冰山之上,此组件给使用者带来了简洁优雅的体验,一个jar包,一行代码,妙笔生花。那冰山之下是如何实现的呢?那就要从原理说起了。
简单来讲,插桩是在编译期基于 JSR 269的注解处理器中操作AST的方式操纵语法节点,最终编译到class文件中。要做好插桩理解相关的底层原理是必要的。大多数读者对编译器相关内容比较陌生,这里会用较大的篇幅做个相对系统的介绍。
Java编译器是将源码翻译成 class 字节码的工具,Java编译器有多种实现:Open JDK的javac、Eclipse的ecj和ajc、IBM的jikes等,javac是公司内主要的编译器,本文是基于Open JDK 1.8 讲解。
作为一款工业级编译器内部实现比较复杂,其涵盖的内容足够写一本书了。结合本人对javac源码的理解,尝试通俗易懂的讲清楚插桩涉及到的知识,有不尽之处欢迎指正。有兴趣进一步研究的读者建议阅读 javac源码[6]。下面将讲解编译器执行流程,相关javac源码导航,以及注解处理器如何运作。
根据官网资料[3]javac 处理流程可以粗略的分为 3个部分:Parse and Enter、Annotation Processing、Analyse and Generate,如下图:
Parse and EnterParse
阶段主要通过词法分析器(Scanner)读取源码生产 token 流,被语法分析器(JavacParser)消费构造出AST,Java代码都可以通过AST表达出来,读者可以通过JCTree查看相关的实现。为了让读者能更直观的理解AST,本人做了一个源码解析成AST后的图形化展示:
示例源码:
token流:[ package ] <- [ com ] <- [ . ] <- …... <- [ } ]解析成AST后如下:
Enter阶段主要是根据AST填充符号表,此处为插桩之后的流程,因此不再展开。
Annotation Processing
注解处理阶段,此处会调用基于 JSR269 规范的注解处理器,是javac对外的扩展。通过注解处理器让开发者(指非javac开发者,下同)具备自定义执行逻辑的能力,这就是插桩的关键。在这个阶段,可以获取到前一阶段生成的AST,从而进行操作。
Analyse and Generate
分析AST并生成class字节码,此处为插桩之后的流程,不再展开。
javac触发入口类路径是:com.sun.tools.javac.Main,代码如下:
经验证Maven 执行构建调的是此类中的main方法。其他构建工具未做验证,猜测类似的。在JDK内部也提供了javax.tools.ToolProvider#getSystemJavaCompiler的入口,实际上内部实现也是调的这个类里的compile方法。
经过一系列的命令参数解析和初始化操作,最终调到真正的核心入口,方法是com.sun.tools.javac.main.JavaCompiler#compile,如下图:
这里有3个关键调用:
Java从JDK 1.6 开始,引入了基于JSR 269 规范的注解处理器,允许开发者在编译期间执行自己的代码逻辑。如本文讲的UMP监控埋点插桩组件一样,由此衍生出了很多优秀的技术组件,如前面提到的Lombok、Mapstruct等。注解处理器使用比较简单,后面示例代码有注解处理器简单实现也可以参考。这里重点讲一下注解处理器整体执行原理:
接下来将通过UMP监控埋点功能来展示怎么在注解处理器中操作AST。
关于AST 操作的探索,早在2008年就有相关资料了[4],Lombok、Mapstruct都是开源的工具,也可以用来参考学习。这里简单讲一个示例,展示如何插桩。
注解处理器使用框架
上图展示了注解处理器具体的基本使用框架,init、process是注解处理器的核心方法,前者是初始化注解处理器的入口,后者是操作AST的入口。javac还提供了一些有用的工具类,比如:
向类中织入一个import节点
这里举一个简单场景,向类中织入一个import节点:
为方便理解对代码实现做了简化,可以配合注释查看如何织入:
总的来说,织入逻辑是通过TreeMaker创建AST 节点,并操作现有AST织入创建的节点,从而达到了织入代码的目的。
到这里,讲了埋点组件的使用、技术选型、以及插桩相关的内容,最终开发出来的组件在工作中也起到了很好的效果。但是在这个过程中有一些反思。
插桩门槛高
通过前面的内容不难得出一个事实,要实现一个小小的功能,需要开发者花费大量的精力去学习理解编译器底层的一些原理。从ROI角度看,投入和产出是严重不成正比的。为了能提供可靠的实现,个人花费了大量业余时间去做技术选型分析和编译器相关知识,可以说是纯靠个人的兴趣和一股倔劲一点点搭建起来的,细节是魔鬼,这个踩坑的过程比较枯燥。实际上插桩机制有很多通用的场景可以探索,之所以一直很少见到此类机制的应用。主要是其门槛较高,对大多数开发者来说比较陌生。因此降低开发者使用门槛才能让一些想法变成现实。做一把好用的锤子,比砸入一个钉子要更有价值。在监控埋点插桩组件真正落地时,在项目内做了一定抽象,并支持了一些开关、自定义链路跟踪等功能。但从作用范围来讲是不够的,所以下一步计划做一个插桩方面的技术框架,从易用性、可维护性等方面做好进一步的抽象,同时做好可测试性相关工作,包含验证各版本JDK的支持、各种Java语法的覆盖等。
插桩是把双刃剑
javac官方对修改AST的方式持保守态度,也存在一些争议。然而时间是最好的验证工具,从Lombok 等组件的发展看出,插桩机制是能经住长久考验的。如何合理利用这种能力是非常重要的,合理使用可使系统简洁优雅,使用不当就等于在代码里下毒了。所以要有节制的修改AST,要懂前后运行机制,围绕通用的场景使用,避免滥用。
认识当前上下文环境的局限性
遇到问题时,如果在当前的上下文环境里找不到合适的解决方案,从这个环境跳出来换个维度也许能看到不同的风景。就像物理机到虚拟机再到现在的容器,都是打破了原来的规则逐步发展出新的技术生态。大多数的开发工作都是基于一个高层次的封装上面进行,而突破往往都是从底层开始的,适当的时候也可以向下做一些探索,可能会产生一些有价值的东西。
网站标题:如何让Java编译器帮你写代码
标题URL:http://www.mswzjz.cn/qtweb/news21/403271.html
攀枝花网站建设、攀枝花网站运维推广公司-贝锐智能,是专注品牌与效果的网络营销公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 贝锐智能