java程序什么时候需要在运行的时候动态修改字节码对象

一、java程序什么时候需要在运行的时候动态修改字节码对象

我认为有两种场景,一种是无法修改源代码的时候;另外一种是功能增强的时候。

1、无法修改源代码

举个例子,java程序依赖的第三方的jar包中发现了bug,但是官方还没有修复,本地通过debug已经发现了解决方法,该如何修复该问题呢?

在spring程序中,如果目标对象在spring容器中,可以通过Spring AOP创建切面解决。但是如果目标对象并没有在spring容器中,或者干脆程序根本不是spring技术栈中的,问题就比较麻烦了,因为无法创建切面拦截目标方法执行。

这时候很容易想到,如果能在不修改第三方源代码的基础上做到修复第三方的bug就好了,这时候使用字节码修改工具动态的修改字节码对象是比较常见的方法。

2、功能增强

在fastjson框架中就是用了asm工具直接操作字节码替代反射技术以加快执行速度。

二、如何在运行的时候修改字节码对象

常见的字节码修改工具有asm和javassist两种,asm工具是直接操作字节码对象底层的,使用它需要对字节码数据结构有很深入的理解;javassist相对于asm工具来说就很亲民了,它提供了两种级别的API:源级别和字节码级别,如果用户使用源代码级API,他们可以不需要了解Java字节码的规范的前提下编辑类文件,这得使操作Java字节码变得简单。

由于技术水平有限,这里使用javassist工具进行字节码修改的操作。

以下程序使用javassist工具演示如何在运行中动态的整体替换掉一个方法中的所有内容。

首先创建一个类Test1

package com.kdyzm;import lombok.extern.slf4j.Slf4j;/*** @author kdyzm* @date 2022/1/29*/
@Slf4j
public class Test1 {public void sayHi() {log.info("Hello,world");}
}

然后创建主类Main

package com.kdyzm;import javassist.*;
import lombok.extern.slf4j.Slf4j;/*** @author kdyzm* @date 2022/1/29*/
@Slf4j
public class Main {public static void main(String[] args) throws NotFoundException, CannotCompileException {ClassPool classPool = ClassPool.getDefault();classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));String clsName = "com.kdyzm.Test1";CtClass ctClass = classPool.get(clsName);CtMethod ctMethod = ctClass.getDeclaredMethod("sayHi");ctMethod.setBody("log.info(\"Hello,kdyzm\");");ctClass.toClass();// 释放对象ctClass.detach();new Test1().sayHi();}
}

在以上代码中,Test1对象本应当打印输出

Hello,world

但是在运行中被我将sayHi方法体替换成了

log.info("Hello,kdyzm");

所以,最终方法的执行结果是

Hello,kdyzm

当然,这是一个最简单的代码示例。更多的高级用法可以参考CtMethod使用文档:

Javassist Tutorial

三、使用Javassist的弊端

一个显而易见的弊端就是替换的方法内容不能过于复杂,否则代码的可读性会变的非常差,调试和修改会变的非常困难,比如下面一段代码

image-20220301155749929

这段代码不算很复杂,但是调试和修改已经非常困难(因为没法断点,编写代码逻辑的时候没有代码提示),而且由于代码作为字符串显示在源代码中,没有代码高亮,再加上换行符,如果没有代码格式化,整个就像一坨*一样,所以,不到万不得已,最好不要使用这种方式。

四、最佳实践

使用javassist工具修改字节码对象,由于替换内容的复杂性,使得维护和debug非常困难,我在实践的过程中发现,将要修改的点封装成单独的类,将核心修改点委托给该类执行是个挺不错的方法。

image-20220301160857666

五、报错和问题分析

1、出现的问题

将在二、如何在运行的时候修改字节码对象中的Main类的main方法中新增加一行代码: new Test1().sayHi();

package com.kdyzm;import javassist.*;
import lombok.extern.slf4j.Slf4j;/*** @author kdyzm* @date 2022/1/29*/
@Slf4j
public class Main {public static void main(String[] args) throws NotFoundException, CannotCompileException {new Test1().sayHi();//此处新增加一行代码ClassPool classPool = ClassPool.getDefault();classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));String clsName = "com.kdyzm.Test1";CtClass ctClass = classPool.get(clsName);CtMethod ctMethod = ctClass.getDeclaredMethod("sayHi");ctMethod.setBody("log.info(\"Hello,kdyzm\");");ctClass.toClass();// 释放对象ctClass.detach();new Test1().sayHi();}
}

看似人畜无害的一行代码加完之后执行就会报错:

16:10:45.519 [main] INFO com.kdyzm.Test1 - Hello,world
Exception in thread "main" javassist.CannotCompileException: by java.lang.ClassFormatError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "com/kdyzm/Test1"at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:271)at javassist.ClassPool.toClass(ClassPool.java:1240)at javassist.ClassPool.toClass(ClassPool.java:1098)at javassist.ClassPool.toClass(ClassPool.java:1056)at javassist.CtClass.toClass(CtClass.java:1298)at com.kdyzm.Main.main(Main.java:21)
Caused by: java.lang.ClassFormatError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "com/kdyzm/Test1"at javassist.util.proxy.DefineClassHelper$Java7.defineClass(DefineClassHelper.java:182)at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:260)... 5 more

问题代码就出在:ctClass.toClass();这行代码上,从问题描述上来看,是重复加载了同一个类导致的。

2、异常分析

通过一步一步debug,最终看到了报错执行的方法是:javassist.util.proxy.DefineClassHelper.Java7#defineClass

image-20220304140313960

在截图中可以清楚的看到,实际上捕获到的异常类型是LinkeageError,但是捕获到之后被转换成了ClassFormatError抛出,ClassformatError类的定义如下:

image-20220304140540554

可以看出,ClassFormatError类是LinkageError类的子类,所以这里可能只是想要做到更加符合ClassFormatError的语义要求。

3、使用反射技术实现类加载

image-20220304141258211

截图中的代码

defineClass.invokeWithArguments(loader, name, b, off, len, protectionDomain)

实际上是使用反射调用了ClassLoader类的defineClass方法,看下defineClass的定义就知道了

private static class Java7 extends Helper {private final SecurityActions stack = SecurityActions.stack;private final MethodHandle defineClass = getDefineClassMethodHandle();private final MethodHandle getDefineClassMethodHandle() {if (privileged != null && stack.getCallerClass() != this.getClass())throw new IllegalAccessError("Access denied for caller.");try {return SecurityActions.getMethodHandle(ClassLoader.class, "defineClass",new Class[] {String.class, byte[].class, int.class, int.class,ProtectionDomain.class});} catch (NoSuchMethodException e) {throw new RuntimeException("cannot initialize", e);}}@OverrideClass<?> defineClass(String name, byte[] b, int off, int len, Class<?> neighbor,ClassLoader loader, ProtectionDomain protectionDomain)throws ClassFormatError{if (stack.getCallerClass() != DefineClassHelper.class)throw new IllegalAccessError("Access denied for caller.");try {return (Class<?>) defineClass.invokeWithArguments(loader, name, b, off, len, protectionDomain);} catch (Throwable e) {if (e instanceof RuntimeException) throw (RuntimeException) e;if (e instanceof ClassFormatError) throw (ClassFormatError) e;throw new ClassFormatError(e.getMessage());}}}

和常见的反射技术不同的是,这里使用的MethodHandle类实现反射,最终调用的方法是:java.lang.ClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.ProtectionDomain)

image-20220304141754544

该方法从一个字节数组中获取字节码数据并最终调用defineClass1方法解析成为类对象,该方法会抛出ClassFormatError、NoClassDefFoundError等异常,但是实际上不仅仅这些异常,还有本例中的LinkageError,这里并没有包含所有的异常种类。

这个方法有个特点,如果加载了重复的类对象,会抛出LinkageError异常,这是在defineClass1方法中发生的逻辑

image-20220304142408725

可以看到,defineClass1方法是一个本地方法,底层是C++实现的,没法直接看到

4、defineClass1源码解析

以jdk1.8为例,defineClass1的源码地址:https://github.com/openjdk/jdk/blob/jdk8-b81/jdk/src/share/native/java/lang/ClassLoader.c#L90

由于这玩意是C实现的,我看的也是云里来雾里去,大体上的调用链是:

Java_java_lang_ClassLoader_defineClass1->JVM_DefineClassWithSource->resolve_from_stream->SystemDictionary::find_or_define_instance_class或者SystemDictionary::define_instance_class

在find_or_define_instance_class方法上,有一段注释如下:

// Support parallel classloading
// All parallel class loaders, including bootstrap classloader
// lock a placeholder entry for this class/class_loader pair
// to allow parallel defines of different classes for this class loader
// With AllowParallelDefine flag==true, in case they do not synchronize around
// FindLoadedClass/DefineClass, calls, we check for parallel
// loading for them, wait if a defineClass is in progress
// and return the initial requestor's results
// This flag does not apply to the bootstrap classloader.
// With AllowParallelDefine flag==false, call through to define_instance_class
// which will throw LinkageError: duplicate class definition.
// False is the requested default.
// For better performance, the class loaders should synchronize
// findClass(), i.e. FindLoadedClass/DefineClassIfAbsent or they
// potentially waste time reading and parsing the bytestream.
// Note: VM callers should ensure consistency of k/class_name,class_loader

代码可能看不大懂,但是这段注释还是能看个几分明白,特别是这段

With AllowParallelDefine flag==false, call through to define_instance_class which will throw LinkageError: duplicate class definition.

define_instance_class方法会抛出LinkageError:duplicate class definition.这和java代码中看到的错误异常一模一样,而且,注释的最后,还贴心的给了一个提示:VM callers should ensure consistency of k/class_name,class_loader,这告诉我们,要确保目标类和加载的ClassLoader的一致性,否则会抛出异常:LinkageError。

下面的代码就看不懂了,但是基本上我也找到了答案:调用java.lang.ClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.ProtectionDomain)方法要确保一个类只会被同一个ClassLoader加载一次,否则就会报错:loader (instance of sun/misc/Launcher$AppClassLoader): attempted duplicate class definition for name xxx

5、问题复现

上面使用了javassist修改完字节码问题件之后出现了attempted duplicate class definition for name xxx的错误,现在不使用javassist,使用最简单的代码来重现这个问题

import lombok.extern.slf4j.Slf4j;import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;/*** @author kdyzm* @date 2022/3/2*/
@Slf4j
public class Main2 {public static void main(String[] args) throws Throwable {defineClass();defineClass();}private static void defineClass() throws Throwable {ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();MethodHandle methodHandle = null;try {methodHandle = getMethodHandle(ClassLoader.class, "defineClass", new Class[]{String.class,byte[].class,int.class,int.class,ProtectionDomain.class});} catch (Throwable e) {log.error("", e);return;}byte[] bytes = getClassBytes();try {Class<Test1> clazz = (Class<Test1>) methodHandle.invokeWithArguments(contextClassLoader,"com.kdyzm.Test1",bytes,0,bytes.length,null);log.info(clazz.toString());} catch (Throwable throwable) {log.error("",throwable);}}static MethodHandle getMethodHandle(final Class<?> clazz,final String name,final Class<?>[] params) throws NoSuchMethodException {try {return AccessController.doPrivileged((PrivilegedExceptionAction<MethodHandle>) () -> {Method rmet = clazz.getDeclaredMethod(name, params);rmet.setAccessible(true);MethodHandle meth = MethodHandles.lookup().unreflect(rmet);rmet.setAccessible(false);return meth;});} catch (PrivilegedActionException e) {if (e.getCause() instanceof NoSuchMethodException) {throw (NoSuchMethodException) e.getCause();}throw new RuntimeException(e.getCause());}}private static byte[] getClassBytes() throws IOException {FileInputStream fis = new FileInputStream("D:\\projects-my\\Main\\target\\classes\\com\\kdyzm\\Test1.class");ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();byte[] buff = new byte[1024];int length = -1;while ((length = fis.read(buff)) != -1) {byteArrayOutputStream.write(buff, 0, length);}return byteArrayOutputStream.toByteArray();}
}

结果报错如下:

15:12:21.799 [main] INFO com.kdyzm.Main2 - class com.kdyzm.Test1
15:12:21.803 [main] ERROR com.kdyzm.Main2 - 
java.lang.LinkageError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "com/kdyzm/Test1"at java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClass(ClassLoader.java:756)at java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:627)at com.kdyzm.Main2.defineClass(Main2.java:44)at com.kdyzm.Main2.main(Main2.java:25)

可以看到第一次加载成功后再次调用defineClass方法加载Test1类就会直接报错LinkageError,符合预期结果。

六、其它疑问的思考

上面只是说了javassist调用了ClassLoader的defineClass方法实现的类加载,但是类加载的方法有好几种,为什么要调用defineClass方法而不调用Class.forName方法或者ClassLoader.loadClass方法加载类?毕竟,调用defineClass方法必须通过反射调用,而且重复加载类还会报错异常。。。

我的理解是:使用javassist并没有修改字节码文件,而只是修改了字节码对象,举个例子,我们通过jar包运行的程序,根本不可能在运行中修改jar包中打包的class文件。提前调用defineClass方法加载好被修改该过的类,这样运行中正常调用Class.forName或者ClassLoader.loadClass方法的时候,发现该类已经被加载过了就不再重新加载了,这样就实现了运行中修改字节码对象实现偷梁换柱的目的。

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

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

相关文章

工程设计问题-步进锥滑轮问题

该问题的主要目标是用5个变量使4阶锥皮带轮的重量最小&#xff0c;其中4个变量是皮带轮每个台阶的直径&#xff0c;最后一个变量是滑轮的宽度。该问题包含11个非线性约束&#xff0c;以保证传动功率必须为0.75马力。 Abhishek Kumar, Guohua Wu, Mostafa Z. Ali, Rammohan Mall…

启动mysql 3.5时出现 MySql 服务正在启动 . MySql 服务无法启动。

有可能是端口冲突 netstat -ano | findstr :3306运行这段代码出现类似&#xff1a; 可以看到端口 3306 已经被进程 ID 为 6284 的进程占用。为了启动新的 MySQL 服务&#xff0c;我们需要停止这个进程或更改新服务的端口&#xff1a; 1、终止进程 taskkill /PID 6284 /F2、确…

【计算机毕业设计】基于Springboot的车辆管理系统【源码+lw+部署文档】

包含论文源码的压缩包较大&#xff0c;请私信或者加我的绿色小软件获取 免责声明&#xff1a;资料部分来源于合法的互联网渠道收集和整理&#xff0c;部分自己学习积累成果&#xff0c;供大家学习参考与交流。收取的费用仅用于收集和整理资料耗费时间的酬劳。 本人尊重原创作者…

贷款投资决策和常用财务函数

前段时间上了一门excel操作的课&#xff0c;本文结合其中介绍财务函数以及投资决策分析相关的部分&#xff0c;对贷款中的现金流计算进行深入的分析。 以等额本息产品为例进行实操计算&#xff0c;假设某产品本金12000元&#xff0c;期限12&#xff0c;IRR利率24%。每期还款113…

项目:双人五子棋对战-对战模块(6)

完整代码见: 邹锦辉个人所有代码: 测试仓库 - Gitee.com 当玩家进入到游戏房间后, 就要开始一局紧张而又刺激的五子棋对战了, 本文将就前端后端的落子与判断胜负的部分作详细讲解. 模块详细讲解 约定前后端交互的接口 首先是建立连接后, 服务器需要生成一些游戏的初始信息(可…

c语言——扫雷游戏(简易版)

目录 前言游戏设计 前言 什么是扫雷游戏&#xff1f; 游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子&#xff0c;同时避免踩雷&#xff0c;踩到一个雷即全盘皆输。 这个游戏对于c语言的初学者来说难度还是挺大的&#xff0c;那我就实现一个初学者也能快速学…

黄仁勋最新建议:找到一门技艺,用一生去完善、磨炼!

“你可能会找到你的英伟达。我希望你们将挫折视为新的机遇。” 黄仁勋职业生涯中最大的教诲并非来自导师或科技公司 CEO&#xff0c;而是来自他在国际旅行时遇到的一位园丁。 近日在加州理工学院毕业典礼上发表演讲时&#xff0c;黄仁勋向毕业生分享了自己在日本京都的小故事。…

2012-2022年各省新质生产力指数数据(含原始数据+结果)

2012-2022年各省新质生产力指数数据&#xff08;含原始数据结果&#xff09; 1、时间&#xff1a;2012-2022年 2、指标&#xff1a;province、year、平均受教育年限、劳动者人力资本结构、高等院校在校学生结构、人均GDP元、在岗职工工资&#xff1a;元、三产从业人员比重、机…

各种机器学习算法的应用场景分别是什么(比如朴素贝叶斯、决策树、K 近邻、SVM、逻辑回归最大熵模型)?

2023简直被人工智能相关话题席卷的一年。关于机器学习算法的热度&#xff0c;也再次飙升&#xff0c;网络上一些分享已经比较老了。那么今天借着查询和学习的机会&#xff0c;我也来浅浅分享下目前各种机器学习算法及其应用场景。 为了方便非专业的朋友阅读&#xff0c;我会从算…

电子设计教程基础篇(电容)

文章目录 前言一、电容原理1.原理2.公式 二、电容种类1.结构1、固定电容2、可变电容3、微调电容 2.介质材料1、气体介质电容1、空气电容2、真空电容3、充气式电容 2、固体介质电容1、无机1、云母电容2、陶瓷电容1、瓷片电容2、独石电容 3、玻璃釉电容&#xff08;CI&#xff09…

爆火的治愈系插画工具又来了,额度居然有18w,根本花不完?

AI治愈插画又又又来了 今天给大家推荐一款完全免费的软件&#xff0c;用过的人都说好&#xff01; 先来看看我生成的图 制作过程非常简单&#xff0c;输入你想要生成的画面咒语。 工具地址&#xff1a;https://www.qiyuai.net/ 模型目前有两种 我上面的图就是用的第一种通用…

数据可视化案例

数据可视化案例 使用豆瓣电影中的数据来进行可视化&#xff0c;网址&#xff1a;豆瓣电影 Top 250 (douban.com) 一、网页数据分析 我们需要爬取的是豆瓣电影Top250网页每一页的电影名称、图片链接、导演、年份、国家、电影类型、电影评分这些数据。 在待爬取的网页中&#x…

通义千问调用笔记

如何使用通义千问API_模型服务灵积(DashScope)-阿里云帮助中心 package com.ruoyi.webapp.utils;import com.alibaba.dashscope.aigc.generation.Generation; import com.alibaba.dashscope.aigc.generation.GenerationOutput; import com.alibaba.dashscope.aigc.generation.G…

移动硬盘打不开怎么办?原因解析!

移动硬盘是一种方便携带、快速传输大量数据的存储设备。但有时我们会遇到这样的问题&#xff1a;插上电脑后&#xff0c;移动硬盘无法打开&#xff0c;出现各种错误提示。这时候我们该怎么办呢&#xff1f; 以下是一些可能导致移动硬盘打不开的原因及解决方法&#xff1a; 1.硬…

初始-Nativefier

--无奈只能靠自己 Nativefier 是什么&#xff1a; Nativefier 是一个命令行工具&#xff0c;仅仅通过一行代码就可以轻松地为任何的网站创建桌面应用程序&#xff0c;应用程序通过 Electron 打包成系统可执行文件&#xff08;如.app, .exe 等&#xff09;&#xff0c;可以运行在…

xx销售公司IT建设目标及IT规划方案(69页PPT)

方案介绍&#xff1a; 随着市场竞争的日益激烈&#xff0c;XX销售公司认识到信息化建设对于提升公司竞争力、优化业务流程、提高管理效率的重要性。次IT建设方案为XX销售公司带来了显著的业务效益和管理提升。我们将继续致力于推动公司的信息化建设&#xff0c;为公司的发展提…

Arthas线上环境问题排查定位工具

一、Arthas简介 Arthas是alibaba推出的一款JVM性能诊断调优的工具&#xff0c;也可以称之为是线上监控诊断产品&#xff0c;通过全局的视角可以实时的查看应用load、内存、GC、线程的状态信息&#xff0c;并且还可以在不修改应用代码的前提下&#xff0c;对业务问题进行诊断&a…

手把手教你如何在Windows11下安装Docker容器

文章的主要要点&#xff1a; 为什么使用Docker&#xff1a;Docker可以简化部署过程&#xff0c;特别适合新手或在学习新技能&#xff08;如Redis、MySQL、消息队列、Nginx等&#xff09;时使用。 安装前的准备&#xff1a;在安装Docker之前&#xff0c;需要在Windows中开启一些…

2024都市解压爆笑喜剧《脑洞大开》6月28日上映

随着暑期档的临近&#xff0c;电影市场迎来了一剂强心针——由何欢、王迅、克拉拉、卜钰、孙越、九孔等众多实力派笑星联袂主演的都市解压爆笑喜剧《脑洞大开》正式宣布定档&#xff0c;将于6月28日在全国各大影院欢乐上映&#xff0c;誓为观众带来今夏最畅快淋漓的笑声风暴。 …

代码随想录-Day32

122. 买卖股票的最佳时机 II 给你一个整数数组 prices &#xff0c;其中 prices[i] 表示某支股票第 i 天的价格。 在每一天&#xff0c;你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买&#xff0c;然后在 同一天 出售。 返回 你能…