迟慧智,孔德智
(工业和信息化部电子第五研究所,广东 广州 511370)
近年来,随着计算机与互联网技术的快速发展,软件系统被广泛地应用于各行各业中,为传统行业带来新的机遇与发展。但是,随着人们生活水平和科学文化素养的日益提高,用户对软件开发交付的质量与速度也有了更高的要求。尤其是在规模和业务需求不断增加的情况下,大多数企业选择微服务架构,使得开发周期与成本增加。因此,在软件工程中如何保质保量地完成开发,让开发者更加专注于业务处理方面成为了一个研究热点。
根据JetBrains多方面收集的数据显示[1],全球大约有520万专业Java开发者,虽然已有25岁高龄,但Java仍是具有活力的开发语言。在Java项目开发中,开发人员应该更加专注于业务逻辑处理,每个方法应该遵循单一原则,日志事务等功能应作为通用功能附加于主功能之上,因此诞生了方法增强技术。方法增强是指在不改动源代码的情况下对方法进行包装以附加额外的功能。Java开发中方法增强常用以下几种方式:类继承、装饰者模式和动态代理。类继承是在子类方法中调用父类方法,通过方法覆盖以实现父类方法的增强,但使用时必须控制对象创建;装饰者模式则利用包装对象与目标对象实现相同接口或继承相同父类,通过构造器调用的方法来实现方法增强,但当继承或实现的接口较多时各个方法容易互相干扰;动态代理方式则通过实现Java中的特定接口实现方法增强。上述方式均可以实现方法增强,但与项目的耦合度较高,难以实现高内聚低耦合。本文通过对面向切面编程(AOP:Aspect Orient Programming)和字节码插桩这两种非侵入式方法增强方式对比,分析了这些技术的优缺点和使用范围,并探讨了非侵入式方法增强的新进展与应用。
AOP可以看做面向对象编程(OOP)的一种扩展,一种颗粒度更细的面向对象的思想延伸。AOP利用横切的方法,将封装对象的内部暴露出来,以方法为单位,将多个方法的公共行为封装到一个模块,实现公共方法的重用,如图1所示。这个模块就被称为切面(Aspect),即与业务逻辑无关,但却是系统逻辑中所需要的代码。将这些代码封装起来可以减少系统代码的冗余,实现各个模块的高内聚低耦合。AOP是软件开发中的一个热点,其中包含众多的组成元素:连接点(Join point),程序执行的一个点,也就是需要切入的方法;切入点(Pointcut),连接点的集合,需要切入点的位置;增强(Advice),需要抽取出来的具体逻辑,运行在切入点前后;切面(Aspect),连接点和增强结合形成了切面。AOP存在多种增强方式,包括:前置增强、后置增强、异常后增强、前置环绕增强和后置环绕增强,如图2所示。通常Java中AOP的实现方式有静态AOP和动态AOP两种。其中,静态AOP是在Java文件编译期间将增强方法编织进被代理对象的生成class文件中[2];动态AOP[3]则是在内存中动态地生成增强后的class文件加载进Java虚拟机中。使用AOP时切面与增强方法在同一个工程中,这种强关联性使AOP在生产环境中的应用有一定的限制。
图1 系统实现AOP方式
图2 AOP增强方式
SpringAOP是利用JDK动态代理或CGLIB基于代理模式对需要增强的方法进行代理实现方法增强。在Spring中,有两种AOP的使用方式:即基于XML配置的方式和基于注解的方式。基于XML配置方式首先定义增强方法类:
在Java项目中,Java文件需要编译为class文件后装载到Java虚拟机(JVM)中才可以发挥作用,JVM通过读取class文件并解释class文件中的各个字节码含义转义为操作系统可以识别的数据[4]。字节码插桩则是在运行时动态修改class文件,IDEA的debug功能就是以此技术为基本原理实现的。字节码插桩一般是由Javaagent+Javaassist/ByteBuddy两种技术组合实现的。Javaagent技术在运行jar包时指定JVM的-Javaagent参数与Javaagent工程相关联,在主工程的main方法运行前调用Javaagent工程里Premain类的premain方法[5]。Javaagent技术的使用需要定义包含被Javaagent代理类的项目工程和Javaagent工程,Javaagent工程内部修改MANIFEST.MF文件以指定premain方法的全路径,将Javaagent工程单独打包后再项目工程启动时利用-Javaagent参数与项目工程关联[6]。JVM运行时先调用premain方法,再通过premain方法中的Instrumentation对象获取类转换器,将目标字节码文件替换成增强后的字节码文件到类加载器中,实现方法增强。
Javaassist是在Java中编辑字节码文件的类库,通过干预JVM内部类加载过程,Javaassist可以在加载过程中修改原有的类或自定义新类,包括类名、方法名、方法签名、具体执行字节码、返回语句和操作数栈等,class文件中的构成部分大多可以使用此框架进行操作。但Javaassist自身不能把生成的类加载到JVM中,因此通过Javaagent+Javaassist技术,在实际运行中利用Javaagent执行premain方法获取将要被加载的class文件,利用Javaassist分析转换类并返回修改后的class文件,最终利用Javaagent将返回的class文件加载到JVM中实现类的动态功能增强。具体使用时则主要利用Javaassist中的ClassPool获取字节码文件对象CtClass,通过CtClass的getDeclareMethod方法获取到字节码内部的方法对象CtMethod,获取到方法对象后可对指定的方法进行改写以达到增强的目的。
ByteBuddy是一个代码生成和操作库,可在Java应用程序运行时不借助编译器创建和修改Java类。除了Java类库附带的代码生成程序外,ByteBuddy还允许创建任意类,不限于实现用于创建运行时代理的接口。同时,ByteBuddy提供了一种方便的API,可以使用Java代理或在构建过程中手动更改类[7]。相较于其他字节码操作框架,ByteBuddy的使用过程无需理解字节码指令,通过简单的API就可以操作字节码。ByteBuddy作为一款轻量级框架,使用时仅依赖Java字节码解析库ASM,同时支持各个版本的Java。通过官方文档提供的样例可以看出,ByteBuddy相比其他字节码操作框架使用更为简洁且可读性高。
AOP和字节码插桩都可以实现非侵入式方法增强,但使用步骤和实现方式有较大的不同,对系统资源的消耗也不同,下面将通过一个web样例对比不同增强方式的性能。
图4 AOP、JavaAssist、ByteBuddy工程结构
测试工程利用AOP、Javaassist和ByteBuddy技术,实现不同方式的方法增强,整体项目结构如图3所示。
图3 测试项目结构
其中,test是基于SpringBoot的简单测试工程,test工程中的controller如下:
对于不同的技术,实现方法增强的方式也是不同的。AOP主要是利用注解形式,将自定义注解自定义注解的方法;Javaassist则是利用Javaagent的premain方法在main方法前执行,premian方法通过对ClassFileTransformer接口的实现类MyMonitorTransformer中的方法transform中进行字节码的获取及处理;ByteBuddy与Javaassist方法类似,利用premain方法在main方法前执行,利用ByteBuddy的框架将带有特定注解@Domonitor添加至test工程中的controller,利用DoJoinPoint拦截和处理带有(@RuntimeType)的方法用于拦截和处理运行时的字节码。使用命令行Java-Javaagent:<增强工程jar包路径>即可在test工程启动前需关联增强工程与test工程。
项目启动后利用浏览器访问controller中的地址,返回结果如图5所示。
图5 AOP工程、JavaAssist工程、ByteBuddy工程的测试结果
通过试验的返回结果可以看出:
1)利用Javaassist方式的耗时最长,这是因为Javaassist是将ASM框架封装后向外暴露接口,降低了使用难度,同时也增加了资源损耗;
2)AOP方式耗时适中且操作简单,但是需在测试工程上进行硬编码,使用场景会受限;
3)ByteBuddy的使用时间最短,测试结果与ByteBuddy官方提供的性能测试的结果(如图6所示)相似。
图6 ByteBuddy官方测试字节码操作的运行时间
本文对Java开发中的方法增强方式AOP、Javaagent+Javaassist和Javaagent+ByteBuddy技术进行了分析研究,论述了不同技术的原理及优缺点。随着人们对软件应用的要求不断提高,方法增强技术也将得到长足的发展,尤其是字节码插桩技术,其与被关联系统完全解耦的优点,将有更多的应用场景。