一起单测引起的项目加载失败惨案 | 京东云技术团队

news2024/12/30 3:30:21

作者:京东科技 宋慧超

一、前言

最近在开发一个功能模块时,在功能自测阶段,通过使用单测测试功能的完整性,在测试单测联通性使用到静态方法测试时,发现单测报错,通过查阅解决方案发现需要对Javaassist包进行排包或者升版本处理。通过排包解决掉单测报错,在部署项目时发现频繁报bean注入失败问题,最终定位发现是因为对Javaassist包排包引起的bean加载失败。故而对Javaassist包相关知识进行学习整理文章如下。

单测相关报错信息如下:

Powermock - java.lang.IllegalStateException: Failed to transform class

解决单测报错的文章链接:

https://stackoverflow.com/questions/32854688/powermock-java-lang-illegalstateexception-failed-to-transform-class

二、问题复现

1、前期准备

首先使用了Spring框架新建一个demo,并写一个简单测试类对问题进行复现。

**UserService**的定义:

public interface UserService {
    void save(User user);
}




**UserServiceImpl**的实现代码:

@Service
public class UserServiceImpl implements UserService {
    private UserDao userDao;

    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void save(User user) {
        userDao.save(user);
    }
}






这里我们使用了Spring框架的**@Service****@Autowired**注解,以便让Spring框架自动装配**UserDao**实例。

但是,在我们的POM文件中,虽然我们添加了对Spring框架的依赖,但是并没有添加Javaassist库的依赖。而**UserServiceImpl**中确实使用了Javaassist库来进行字节码操作, **UserServiceImpl**的具体实现代码:

public class UserServiceImpl implements UserService {
    // ...
    private static final String USER_CLASS_NAME = "com.example.User";

    private static final Class<?> USER_CLASS;

    static {
        try {
            USER_CLASS = Class.forName(USER_CLASS_NAME);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public void save(User user) {
        try {
            // 创建一个ClassPool对象
            ClassPool cp = ClassPool.getDefault();

            // 从ClassPool中获取一个CtClass对象
            CtClass ctClass = cp.get(USER_CLASS_NAME);

            // 获取无参构造器
            CtConstructor ctConstructor = ctClass.getDeclaredConstructor(new CtClass[]{});

            // 获取save方法
            CtMethod saveMethod = ctClass.getDeclaredMethod("save");

            // 生成代码
            saveMethod.insertBefore("{System.out.println(\"插入代码前\");}");
            saveMethod.insertAfter("{System.out.println(\"插入代码后\");}");

            // 生成新的字节码并装载到内存
            Class<?> targetClass = ctClass.toClass();
            Object instance = targetClass.newInstance();

            // 调用save方法
            Method method = targetClass.getMethod("save", USER_CLASS);
            method.invoke(instance, user);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}






在这段代码中,我们通过Javaassist库生成了一个新的字节码,并使用反射机制将其实例化,并在调用**save()**方法前后插入了一些代码。但是,由于Javaassist库缺失,导致项目在启动过程中无法正确加载**UserServiceImpl**的实例,从而出现了下述错误信息。

2、报错信息

在部署程序时发现,应用无法正常启动,并出现如下错误信息:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in file [C:\workspace\project\target\classes\com\example\UserServiceImpl.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.example.UserService]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.example.UserService.<init>()




从错误信息中我们可以看到,应用在创建**UserService**的实例时遇到了问题,无法实例化成功。

3、解决方案

为了修复这个问题,我们需要在POM文件中加入对Javaassist库的依赖:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.27.0-GA</version>
</dependency>






添加依赖后,重新编译并部署应用程序即可正常运行

三、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使用方法

javassistjboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

操作java字节码的工具有两个比较流行,一个是ASM,一个是Javassit

ASM直接操作字节码指令,执行效率高,要求使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。

Javassit 提供了更高级的API,执行效率相对较,但无需掌握字节码指令的知识,对使用者要求较低。

应用层面来讲一般使用建议优先选择Javassit,如果后续发现Javassit 成为了整个应用的效率瓶颈的话可以再考虑ASM。当然如果开发的是一个基础类库,或者基础平台,还是直接使用ASM吧,相信从事这方面工作的开发者能力应该比较高。

Javassist中最为重要的是ClassPoolCtClassCtMethod 以及 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加载至当前线程的上下文类加载器中,CtClasstoClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的CtClass

writeFile(String directoryName): 根据CtClass生成 .class 文件;

defrost(): 解冻类,用于使用了toclass()toBytecodewriteFile(),类已经被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调用常量。

$开头的特殊字符

符号具体含义
$0, $1, $2, …$0=this,$1表示方法的第一个参数,依次类推,如果方法是静态的,则 $0 不可用
$args方法参数数组.它的类型为 Object[],$args[0]=1 , 1,1,args[1]=$2
$r返回结果的类型,用于强制类型转换
$w包装器类型,用于强制类型转换,当返回值是包装类型时,可以用此来强转
$_返回值,一般在insertAfter中用到,用于得到原方法的返回值
$slg参数类型数组,$sig[0]表示第一个参数类型
$type返回值类型,一般在insertAfter中用到,即$_的类型
$class$0或this的类型
$e异常类型

5、常用的Java插桩工具有哪些?

Java 插桩工具是一种能够修改 Java 字节码的工具,通过在应用程序运行时动态修改字节码来实现对程序的监控跟踪调试优化等功能。

工具字节码抽象级别具体描述
ASM、BCEL低级库需要直接在字节码级别上进行操作。通常,它们提供大多数功能丰富的功能,但与其他字节码操作工具相比,它们的使用也最复杂。
Javaassist中级库提供了字节码的某种抽象级别,并简化了其修改。例如,代替修改字节码,可以使用类似于Java的语法进行更改,然后将其编译为字节码,然后由使用的库修改为原始字节码。通常,它们缺少修改后的代码验证的功能-这意味着,错误可能在修改准备过程中被忽略,然后在运行时被发现。
AspectJ、CGLib高级库使用高级指令进行操作,并且通常配备有用于语法验证的工具集。不幸的是,从修改后的字节码进行的最高抽象化通常会导致某些功能的丧失,这些功能仅在直接修改字节码时可用。

四、总结

本文通过对由于Javaassist包缺失导致项目启动过程中bean加载失败的问题进行复现,并通过demo进行实例分析,解释了因为缺失Javaassist库导致的应用程序启动失败问题。并对Javaassist包相关知识进行介绍,后续会继续对Javaassist相关知识进行学习补充。

建议大家在构建Maven项目时,仔细检查POM文件中的依赖,确保没有漏掉任何必要的库,以免因为遗漏而引起不必要的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/490651.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue3+webpack4 前端优化首屏时间

项目背景 中小项目&#xff0c;Vue-cli3 vue2 webpack4 目标 缩短白屏时间&#xff0c;用户能够更快的看到我的页面&#xff01; 白屏时间&#xff1a;从打开页面到看到页面&#xff0c;中间白屏停留的时间。 方向 1.减少资源体积&#xff0c;从而缩短请求时间 2.减少资…

企业数字化转型为什么难?低代码平台能为企业带来什么?

企业数字化转型困难原因是多方便的&#xff0c;比如&#xff1a; 遗留系统&#xff1a;许多企业在难以替换或与新技术集成的遗留技术系统上投入了大量资金。 变革阻力&#xff1a;企业越大&#xff0c;参与决策的人就越多&#xff0c;让每个人都接受新工作方式的难度就越大。 …

前端架构师-week4-Node多进程开发入门

目录 学习路径 官方文档 什么是进程 child_process 用法 exec & execFile 用法 spawn 用法以及与exec & execFile的区别 fork用法及父子进程通信机制讲解 child_process同步方法使用教程 学习路径 官方文档 中文版&#xff1a;http://nodejs.cn/api/child_pr…

Flutter 如何将代码显示到界面上

前言 如何优雅的将项目中的代码&#xff0c;亦或是你的demo代码展示到界面上&#xff1f;本文对使用简单、便于维护且通用的解决方案&#xff0c;进行相关的对比和探究 为了节省大家的时间&#xff0c;把最终解决方案的相关接入和用法写在前面 预览代码 快速开始 接入&…

C. Permutation Game(博弈 + 拓扑的思想)

Problem - C - Codeforces 经过漫长的一天&#xff0c; Aice和Bob决定玩一个小游戏。游戏棋盘由n个格子组成&#xff0c;在一条直线上&#xff0c;编号从1到n,每个格子包含一个数字4;,qy在1到n.之间&#xff0c;而且没有两个格子包含相同的数字。 一个棋子被放在其中一个格子里…

nestJS入门cli 创建项目以及集成swagger和mysql

nestJs 1. 简介 介绍 NestJS NestJS 是一个基于 TypeScript 的渐进式 Node.js 框架&#xff0c;它结合了 OOP、FP 和 FRP 的元素&#xff0c;以提供一种现代且可扩展的开发体验。NestJS 建立在 Express.js 之上&#xff0c;但是提供了更加抽象和模块化的方式来编写应用程序。…

蓝牙资讯|苹果与谷歌起草蓝牙定位追踪设备行业规范

苹果与谷歌于当地时间5月2日联合提交了一份行业规范草案&#xff0c;以帮助应对蓝牙定位追踪设备遭滥用的问题。目前已有包括三星在内的追踪设备制造厂商宣布支持该草案。 据了解&#xff0c;苹果与谷歌此次联合提交的行业规范草案将云熙蓝牙定位追踪设备兼容跨iOS以及Android平…

【LeetCode: 518. 零钱兑换 II | 暴力递归=>记忆化搜索=>动态规划 | 背包模型】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

十五周算法训练营——二叉搜索树(BST)

今天是十五周算法训练营的第五周&#xff0c;主要讲二叉搜索树专题&#xff0c;包含&#xff1a;验证二叉搜索树、不同的二叉搜索树、二叉树的最近公共祖先、二叉搜索树的最近公共祖先。&#xff08;欢迎加入十五周算法训练营&#xff0c;与小伙伴一起卷算法&#xff09; BST的…

在SQL中使用AI【EVA】

EVA 旨在支持使用深度学习模型对结构化数据&#xff08;表格、特征向量&#xff09;和非结构化数据&#xff08;视频、播客、PDF 等&#xff09;进行操作的数据库应用程序。 它使用一系列受久经考验的关系数据库系统启发的优化&#xff0c;包括函数缓存、采样和基于成本的谓词重…

mybatisPlus初识

文章目录 什么是mybatisplus依赖入门案例自动填充乐观锁悲观锁乐观锁 mybatisPlus实现乐观锁批量查询根据指定条件查询 什么是mybatisplus mybatisplus是mybatis的增强工具&#xff0c;支持多种类型的数据库。 依赖 <dependency><groupId>com.baomidou</group…

我的服务器被挖矿了,原因竟是。。。

「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 挖矿木马应急响应 一、什么是挖矿二、被挖矿主机现象三、挖矿木马处置思路1&#xff09;隔…

Git 使用教程:最详细、最正宗手把手教学(万字长文)

目录 一&#xff1a;Git二&#xff1a;SVN与Git的的区别三、安装Git四&#xff1a;常规操作五&#xff1a;远程仓库六&#xff1a;创建与合并分支七&#xff1a;bug分支八&#xff1a;多人协作九&#xff1a;git可视化工具 Git Git 是一种分布式版本控制系统&#xff0c;用于…

搭建免费的Plex媒体服务器 - 打造超级多媒体中心【异地远程连接】

文章目录 1.前言2. Plex网站搭建2.1 Plex下载和安装2.2 Plex网页测试2.3 cpolar的安装和注册 3. 本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1.前言 用手机或者平板电脑看视频&#xff0c;已经算是生活中稀松平常的场景了&#xff0c;特别是各…

科普, API 是这么演变而来的

API&#xff0c;全称为 Application Programming Interface&#xff0c;中文翻译为应用程序编程接口&#xff0c;是为了方便应用程序之间的数据和功能交互而设计的一些标准方法。API 的使用让开发者能够快速、高效地构建应用程序&#xff0c;从而加速了应用程序的开发速度。在 …

第二章物理层

1.物理层的基本概念 物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流。物理层为数据链路层屏蔽了各种传输媒体的差异&#xff0c;使数据链路层只需要考虑如何完成本层的协议和服务&#xff0c;而不必考虑网络具体的传输媒体是什么。 物理层协议的主要任务 机…

21.ThreadLocal有哪些内存泄漏问题?如何避免?

ThreadLocal有以下几个内存泄漏问题&#xff1a; 长期不清理&#xff1a;如果ThreadLocal对象被长期占用&#xff0c;且不及时清理&#xff0c;会导致内存泄漏&#xff1b;使用static ThreadLocal&#xff1a;如果将ThreadLocal定义为static变量&#xff0c;就会导致它的生命周…

Java程序设计-案例:自由落体

程序模拟物体从10000米高空掉落后的反弹行为。 球体每落地一次&#xff0c;就会反弹至原高度的一半。按用户输入的弹跳次数&#xff0c;计算球体每次弹跳的高度。 实现过程&#xff1a; 1. 新建项目&#xff1b; 2. 接收 用户输入的弹跳次数&#xff1a; &#xff08;1&#…

全国快递物流 API 实现快递单号自动识别的原理解析

概述 全国快递物流 API 是一种提供快递物流单号查询的接口&#xff0c;涵盖了包括申通、顺丰、圆通、韵达、中通、汇通等600快递公司的数据。该 API 的目标是为快递公司、电商、物流平台等提供便捷、快速、准确的快递物流信息查询服务。 数据采集和处理 全国快递物流 API 的…

[深度学习]Ring All-reduce的数学性质

分布式深度学习里的通信严重依赖于规则的集群通信诸如 all-reduce, reduce-scatter, all-gather 等&#xff0c;因此&#xff0c;实现高度优化的集群通信&#xff0c;以及根据任务特点和通信拓扑选择合适的集群通信算法至关重要。 本文以数据并行经常使用的 all-reduce 为例来…