一起单测引起的项目加载失败惨案 | 京东云技术团队
作者:京东科技 宋慧超
一、前言
最近在开发一个功能模块时,在功能自测阶段,通过使用单测测试功能的完整性,在测试单测联通性使用到静态方法测试时,发现单测报错,通过查阅解决方案发现需要对 Javaassist 包进行排包或者升版本处理。通过排包解决掉单测报错,在部署项目时发现频繁报 bean 注入失败问题,最终定位发现是因为对 Javaassist 包排包引起的 bean 加载失败。故而对 Javaassist 包相关知识进行学习整理文章如下。
单测相关报错信息如下:
解决单测报错的文章链接:
二、问题复现
1、前期准备
首先使用了 Spring 框架新建一个 demo,并写一个简单测试类对问题进行复现。
**UserService**
的定义:
**UserServiceImpl**
的实现代码:
这里我们使用了 Spring 框架的**@Service**
和**@Autowired**
注解,以便让 Spring 框架自动装配**UserDao**
实例。
但是,在我们的 POM 文件中,虽然我们添加了对 Spring 框架的依赖,但是并没有添加 Javaassist 库的依赖。而**UserServiceImpl**
中确实使用了 Javaassist 库来进行字节码操作, **UserServiceImpl**
的具体实现代码:
在这段代码中,我们通过 Javaassist 库生成了一个新的字节码,并使用反射机制将其实例化,并在调用**save()**
方法前后插入了一些代码。但是,由于 Javaassist 库缺失,导致项目在启动过程中无法正确加载**UserServiceImpl**
的实例,从而出现了下述错误信息。
2、报错信息
在部署程序时发现,应用无法正常启动,并出现如下错误信息:
从错误信息中我们可以看到,应用在创建**UserService**
的实例时遇到了问题,无法实例化成功。
3、解决方案
为了修复这个问题,我们需要在 POM 文件中加入对 Javaassist 库的依赖:
添加依赖后,重新编译并部署应用程序即可正常运行。
三、Javaassist 包
1、什么是 Javaassist?
Javaassist 是由东京工业大学数学和计算机科学系的 Shigeru Chiba (千叶滋)教授创造的。Javaassist 作为实现动态字节码生成的一个开源类库,极大地简化了 Java 开发者对底层字节码操作的难度,让开发者能够更加轻松地在运行时动态生成类、修改类文件来达到轻量级 AOP、ORM、基于代理的远程方法调用等功能。
(Javaassist 已加入了开放源代码 JBoss 应用服务器项目,通过使用 Javaassist 对字节码操作为 JBoss 实现动态 AOP 框架。)
2、什么是动态编程?
动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如 Java,与动态编程语言,例如 JavaScript。那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操作的技术,在 Java 中有如下几种方式:
•反射
这个搞 Java 的应该比较熟悉,原理也就是通过在运行时获得类型信息然后做相应的操作。由于 Java 执行过程中是将类型载入虚拟机中的,在运行时我们就可以动态获取到所有类型的信息。只能获取却不能修改类型信息。
•动态编译
动态编译是从 Java 6 开始支持的,主要是通过一个 JavaCompiler 接口来完成的。通过这种方式我们可以直接编译一个已经存在的 java 文件,也可以在内存中动态生成 Java 代码,动态编译执行。
•调用 JavaScript 引擎
早在 Java 6 就加入了对 Script(JSR223)的支持。这是一个脚本框架,提供了让脚本语言来访问 Java 内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本。这个脚本 API 允许你为脚本语言提供 Java 支持。
•动态生成字节码
这种技术通过操作 Java 字节码的方式在 JVM 中生成新类或者对已经加载的类动态添加元素。
3、动态编程解决什么问题?
在静态语言中引入动态特性,主要是为了解决一些使用场景的痛点。其实完全使用静态编程也办的到,只是付出的代价比较高,没有动态编程来的优雅。例如依赖注入框架 Spring 使用了反射,而 Dagger2 却使用了代码生成的方式(APT)。
例如:
a: 在那些依赖关系需要动态确认的场景: b: 需要在运行时动态插入代码的场景,比如动态代理的实现。 c: 通过配置文件来实现相关功能的场景
4、Javassit 使用方法
javassist 是 jboss 的一个子项目,其主要的优点,在于简单,而且快速。直接使用 java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
操作 java 字节码的工具有两个比较流行,一个是 ASM,一个是 Javassit 。
◦ASM :直接操作字节码指令,执行效率高,要求使用者掌握 Java 类字节码文件格式及指令,对使用者的要求比较高。
◦Javassit 提供了更高级的 API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。
应用层面来讲一般使用建议优先选择 Javassit,如果后续发现 Javassit 成为了整个应用的效率瓶颈的话可以再考虑 ASM。当然如果开发的是一个基础类库,或者基础平台,还是直接使用 ASM 吧,相信从事这方面工作的开发者能力应该比较高。
Javassist 中最为重要的是 ClassPool,CtClass ,CtMethod 以及 CtField 这几个类。
•ClassPool:一个基于 HashMap 实现的 CtClass 对象容器,其中键是类名称,值是表示该类的 CtClass 对象。默认的 ClassPool 使用与底层 JVM 相同的类路径,因此在某些情况下,可能需要向 ClassPool 添加类路径或类字节。
◦ getDefault (): 返回默认的 ClassPool ,单例模式,一般通过该方法创建我们的 ClassPool;
◦ appendClassPath(ClassPath cp), insertClassPath(ClassPath cp) : 将一个 ClassPath 加到类搜索路径的末尾位置或插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类问题;
◦ importPackage(String packageName):导入包;
◦ makeClass(String classname):创建一个空类,没有变量和方法,后序通过 CtClass 的函数进行添加;
◦ get(String classname)、getCtClass(String classname) : 根据类路径名获取该类的 CtClass 对象,用于后续的编辑。
•CtClass:表示一个类,这些 CtClass 对象可以从 ClassPool 获得。
◦debugDump; String 类型,如果生成**.class**文件,保存在这个目录下。
◦setName(String name): 给类重命名;
◦setSuperclass(CtClass clazz): 设置父类;
◦addField(CtField f, Initializer init): 添加字段(属性),初始值见 CtField;
◦addMethod(CtMethod m): 添加方法(函数);
◦toBytecode(): 返回修改后的字节码。需要注意的是一旦调用该方法,则无法继续修改 CtClass;
◦toClass(): 将修改后的 CtClass 加载至当前线程的上下文类加载器中,CtClass 的 toClass 方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的 CtClass;
◦writeFile(String directoryName): 根据 CtClass 生成 .class 文件;
◦defrost(): 解冻类,用于使用了 toclass()、toBytecode、writeFile(),类已经被 JVM 加载,Javassist 冻结 CtClass 后;
◦detach(): 避免内存溢出,从 ClassPool 中移除一些不需要的 CtClass。
•CtMethods:表示类中的方法。
◦insertBefore(String src):在方法的起始位置插入代码;
◦insertAfter(String src):在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到 exception;
◦insertAt(int lineNum, String src):在指定的位置插入代码;
◦addCatch(String src, CtClass exceptionType):将方法内语句作为 try 的代码块,插入 catch 代码块 src;
◦setBody(String src):将方法的内容设置为要写入的代码,当方法被 abstract 修饰时,该修饰符被移除;
◦setModifiers(int mod):设置访问级别,一般使用 Modifier 调用常量;
◦invoke(Object obj, Object... args):反射调用字节码生成类的方法。
•CtFields :表示类中的字段。
◦CtField(CtClass type, String name, CtClass declaring) :构造函数,添加字段类型,名称,所属的类;
◦CtField.Initializer constant():CtClass 使用 addField 时初始值的设置;
◦setModifiers(int mod):设置访问级别,一般使用 Modifier 调用常量。
•$开头的特殊字符
5、常用的 Java 插桩工具有哪些?
Java 插桩工具是一种能够修改 Java 字节码的工具,通过在应用程序运行时动态修改字节码来实现对程序的监控、跟踪、调试和优化等功能。
四、总结
本文通过对由于 Javaassist 包缺失导致项目启动过程中 bean 加载失败的问题进行复现,并通过 demo 进行实例分析,解释了因为缺失 Javaassist 库导致的应用程序启动失败问题。并对 Javaassist 包相关知识进行介绍,后续会继续对 Javaassist 相关知识进行学习补充。
建议大家在构建 Maven 项目时,仔细检查 POM 文件中的依赖,确保没有漏掉任何必要的库,以免因为遗漏而引起不必要的问题。
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/3214d311bca0ea088b379dcfa】。文章转载请联系作者。
评论