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);
                }
        }

        @Override
        Class<?> 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方法的时候,发现该类已经被加载过了就不再重新加载了,这样就实现了运行中修改字节码对象实现偷梁换柱的目的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

0-1 构建用户画像数仓

目录 前言 一、用户画像概述 1.1 用户画像 1.2 用户标签 1.3 用户群组 二、建设标签和标签体系 2.1 标签体系 2.1.1 统计类标签 2.1.2 规则类标签 2.1.3 机器学习挖掘类标签 2.2 标签建设流程 2.2.1 需求收集与分析 2.2.2 产出标签需求文档 2.2.3 标签的开发 H…

【Java】已解决:Java.lang.OutOfMemoryError: GC overhead limit exceeded

文章目录 问题背景可能出错的原因错误代码示例正确代码示例注意事项 问题背景 java.lang.OutOfMemoryError: GC overhead limit exceeded 是Java虚拟机&#xff08;JVM&#xff09;在运行时遇到的一种内存溢出错误。这种错误通常发生在应用程序的堆内存&#xff08;Heap Memor…

JavaFX 图像视图

JavaFX ImageView 控件可以在 JavaFX GUI 中显示图像。ImageView 控件必须添加到场景图中才能可见。JavaFX ImageView 控件由类表示 javafx.scene.image.ImageView。 创建一个 ImageView 通过创建类的实例来创建 ImageView 控件实例ImageView。类的构造函数ImageView需要一个…

数据结构错题答案汇总

王道学习 第一章 绪论 1.1 3.A 数据的逻辑结构是从面向实际问题的角度出发的&#xff0c;只采用抽象表达方式&#xff0c;独立于存储结构&#xff0c;数据的存储方式有多种不同的选择;而数据的存储结构是逻辑结构在计算机上的映射&#xff0c;它不能独立于逻辑结构而存在。数…

「51媒体」媒体邀约如何高效沟通?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 企业在做活动会议时&#xff0c;往往希望对活动信息或者公司品牌进行一个报道和曝光&#xff0c;那么如何有效且高效的完成与媒体的沟通呢&#xff1f;今天胡老师就来分享下这方面的一些…

五、在Qt下加载QVTKWidget控件,生成Visual Studio项目,显示点云(C++)

前言&#xff1a;因为项目需要通过Qt进行显示点云&#xff0c;参考了很多博文&#xff0c;但是并没有全部正确的&#xff0c;东拼西凑算是实现了&#xff0c;花费了两天时间&#xff0c;时间有点久&#xff0c;能力还有有待提升~~ 为此写篇博文记录一下。感谢各位大佬&#xff…

QT基础 - 常用按钮控件和快捷键

目录 一. QtCreator常用快捷键 二. QWidget 三. QPushButton 四. QRadioButton 五. QCheckBox 六. QToolButton 七. 总结 一. QtCreator常用快捷键 说明快捷键运行ctrl R编译ctrl B帮助文档F1 &#xff0c;点击F1两次跳到帮助界面跳到符号定义F2 或者ctrl 鼠标点击注释…

youlai-boot项目的学习—本地数据库安装与配置

数据库脚本 在项目代码的路径下&#xff0c;有两个版本的mysql数据库脚本&#xff0c;使用对应的脚本就安装对应的数据库版本&#xff0c;本文件选择了5 数据库安装 这里在iterm2下使用homebrew安装mysql5 brew install mysql5.7注&#xff1a;记得配置端终下的科学上网&a…

Mysql学习笔记-进阶篇

一、存储引擎 1、MYSQL体系结构 连接层、服务层、引擎层、存储层&#xff1b; 2、存储引擎简介 存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表的&#xff0c;而不是库的&#xff0c;所以存储引擎也可被称为表类型。 1&#xff09;在创…

智能网络组网天联是什么?

智能网络组网是指利用智能技术实现网络设备之间的连接和数据交流。随着科技的不断发展&#xff0c;智能网络组网在现代社会中发挥着越来越重要的作用。其中&#xff0c;天联是一种智能网络组网技术&#xff0c;具有许多优势。 天联组网的优势 天联组网技术拥有以下优势&#…

2024数据库期末综合解析(部分题)

目录 第4关&#xff1a;数据记录修改 任务描述 补充 答案&#xff1a; 第6关&#xff1a;数据查询二 任务描述 补充 答案&#xff1a; 第4关&#xff1a;数据记录修改 任务描述 湖南人口hnpeople数据表如下所示 各字段含义如下 cs&#xff08;城市)、qx(区县)、rk(人口)、man(男…

2024 年最新 windows 操作系统部署安装 redis 数据库详细教程(更新中)

Redis 数据库概述 Redis 是一个开源的&#xff0c;内存中的数据结构存储系统&#xff0c;它可以用作数据库、缓存和消息中介。Redis&#xff08;Remote Dictionary Server &#xff09;&#xff0c;即远程字典服务&#xff0c;是一个开源的使用ANSI C语言编写、支持网络、可基…

JS 实现Date日期格式的本地化

为了更好的更新多语言日期的显示&#xff0c;所以希望实现日期的本地化格式显示要求&#xff0c;常规的特殊字符型格式化无法满足显示要求&#xff0c;这里整理了几种我思考实现的本地化实现功能。 通过多方查找&#xff0c;总结了实现的思路主要有如下三个方向&#xff1a; 官…

基于Django + Web + MySQL的智慧校园系统

基于Django Web MySQL的智慧校园系统 由于时间紧迫&#xff0c;好多功能没实现&#xff0c;只是个半吊子的后台管理系统&#xff0c;亮点是项目安全性还算完整&#xff0c;权限保护加密功能检索功能有实现&#xff0c;可参考修改 功能如下&#xff08;服务为超链接&#xff0…

SSM整合使用

文章目录 1. 项目创建2. spring(1) 导包(2) 配置类 3. mybatis(1) maven导包(2) mybatis配置文件(3) 连接配置文件(4) mapper映射文件(5) 在spring配置类中注册sqlsession的bean springMVC(1) maven导包(2) springMVC配置类(3) 初始化类 5. 测试(1) 创建3层架构(2) 编写Control…

C语言标准库

目录 引言 一、C标准库概述 常用标准库函数 字符串处理 数学运算 动态内存分配 标准库的扩展与限制 扩展功能 使用限制 使用自定义库与第三方库 创建自定义库 使用第三方库 表格总结 标准库头文件及功能 常用标准库函数 总结 引言 C标准库是C编程语言的重要组成…

dp练习题

先来一个简单dp练习 class Solution { public:int rob(vector<int>& nums) {int n nums.size();vector<int> a(n 1);int ans nums[0]; a[0] nums[0];if (n 1) return ans;a[1] max(nums[0], nums[1]);ans max(ans, a[1]);if (n 2) return ans;for (i…

机器学习中的监督学习介绍

In this post well go into the concept of supervised learning, the requirements for machines to learn, and the process of learning and enhancing prediction accuracy. 在这篇文章中&#xff0c;我们将深入探讨监督学习的概念、机器学习的要求以及学习和提高预测准确…

汽车数据应用构想(四)

车只要在路上跑&#xff0c;就可以感知到道路上的各种情况对于车辆的影响。这些数据都具有一定的特征&#xff0c;通过对数据特征的分析&#xff0c;并结合位置信息&#xff0c;即可得到有价值的POI信源。 近几年的新车&#xff0c;基本上都有智能网联功能&#xff0c;也就是说…

【学习笔记】C++每日一记[20240612]

给定两个有序的数组&#xff0c;计算两者的交集 给定两个有序整型数组&#xff0c;数组中 的元素是递增的&#xff0c;且各数组中没有重复元素。 第一时间解法&#xff1a;通过一个循环扫描array_1中的每一个元素&#xff0c;然后利用该元素去比较array_2中的每一个元素&…