Java中的高性能字节码工具:Javassist

article/2025/9/15 5:51:05

前言

一般常见的动态方法调用使用Reflection或者字节码生成技术。虽然JDK已对反射进行了优化但在追求性能的场景中仍然显得性能不佳。本文即是介绍一个面向程序员友好的字节码操作类库javassist。根据benchmark其展现的性能已几乎无异于直接调用。

开源地址:javassist,简单地看一下官方介绍:

Javassist 使 Java 字节码操作变得简单。它是一个用于在 Java 中编辑字节码的类库。它使 Java 程序可以在运行时定义新类,并在 JVM 加载它时修改类文件。与其他类似的字节码编辑器不同,Javassist 提供了两个级别的 API:源级别和字节代码级别。如果用户使用源代码级 API,则他们可以在不了解 Java 字节码规范的情况下编辑类文件。整个 API 仅使用 Java 语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码。Javassist 可以即时对其进行编译。另一方面,字节码级别的 API 允许用户像其他编辑器一样直接编辑类文件。

说得直白一点就是,我们可以通过它的API来生成我们想要的字节码。下面演示如何进行使用。

正文

生成

首先需要引入Jar包,仓库地址为:

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

为了演示,我们直接创建一个main函数,用来构建一个不存在的对象。

public class JavassistInvoker {public static void main(String[] args) throws Exception {//创建类,这是一个单例对象ClassPool pool = ClassPool.getDefault();//我们需要构建的类CtClass ctClass = pool.makeClass("io.github.pleuvoir.prpc.invoker.Person");//新增字段CtField field$name = new CtField(pool.get("java.lang.String"), "name", ctClass);//设置访问级别field$name.setModifiers(Modifier.PRIVATE);//也可以给个初始值ctClass.addField(field$name, CtField.Initializer.constant("pleuvoir"));//生成get/set方法ctClass.addMethod(CtNewMethod.setter("setName", field$name));ctClass.addMethod(CtNewMethod.getter("getName", field$name));//新增构造函数//无参构造函数CtConstructor cons$noParams = new CtConstructor(new CtClass[]{}, ctClass);cons$noParams.setBody("{name = \"pleuvoir\";}");ctClass.addConstructor(cons$noParams);//有参构造函数CtConstructor cons$oneParams = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);// $0=this  $1,$2,$3... 代表方法参数cons$oneParams.setBody("{$0.name = $1;}");ctClass.addConstructor(cons$oneParams);// 创建一个名为 print 的方法,无参数,无返回值,输出name值CtMethod ctMethod = new CtMethod(CtClass.voidType, "print", new CtClass[]{}, ctClass);ctMethod.setModifiers(Modifier.PUBLIC);ctMethod.setBody("{System.out.println(name);}");ctClass.addMethod(ctMethod);//当前工程的target目录final String targetClassPath = Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath();//生成.class文件ctClass.writeFile(targetClassPath);}
}

可以看到生成的文件如下:

需要注意的是,如果我们大量地使用该方法生成类,可能会造成内存压力。因为ClassPool中存在哈希表用来缓存生成的CtClass对象。我们可以通过调用CtClass#detach()方法清除缓存。 

到此为止,我们的文件是生成了。需要如何调用呢?难道是通过ClassLoader加载吗?接着往下看。

调用

反射调用

有两种常用的方式,调用toClass()方法或者读取.class文件。

修改如上的方法:

这里可以看到已经获得了我们期待的对象,但是由于我们编译器并无此对象所以不能完成强制转换。也就是说还得通过反射来调用。

也可以读取文件进行加载:

//如果生成的类没有放在classpath下需要自己指定类加载器加载的位置,否则加载不到。这里我们不需要设置
//  pool.appendClassPath(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());
final CtClass loadCtClass = pool.get("io.github.pleuvoir.prpc.invoker.Person");
loadCtClass.toClass().newInstance();

注意:toClass不要重复调用。

以上两种形式都是通过反射调用,我们又不存在这样的实体。那么怎么样才能完成转换呢?聪明的小伙伴肯定想到了,那就是定义接口。生成后转换为接口就可以直接调用了。

OK,话不多说。我们先定义一个接口正好来演示一下使用javassist实现动态代理。

接口调用

在实现动态代理前,我们先来回顾一组静态代理。

我们先来定义目标接口,用来被代理。等会这也是我们程序强制转换的接口。

public interface HelloService {String sayHello(String name);
}

定义代理实现,传入接口实现类。

public class HelloServiceProxy implements  HelloService {private HelloService helloService;public HelloServiceProxy() {}public HelloServiceProxy(HelloService helloService) {this.helloService = helloService;}// 这里会做修改@Overridepublic String sayHello(String name) {System.out.println("静态代理前 ..");helloService.sayHello(name);System.out.println("静态代理后 ..");return name;}}

很简单,我们来测试一下:

public class ProxyTest {public static void main(String[] args) {final HelloServiceProxy serviceProxy = new HelloServiceProxy(new HelloService() {@Overridepublic String sayHello(String name) {System.out.println("目标接口实现:name=" + name);return "null";}});serviceProxy.sayHello("pleuvoir");}
}

输出的结果是:

静态代理前 ..
目标接口实现:name=pleuvoir
静态代理后 ..

不错,符合我们的预期。接下来我们就要开始生成真正的动态代理了。

先来思考一下,我们的静态代理是使用有参构造函数传入真正的实现类。如果我们动态生成的类去调用构造函数还是必须使用反射,所以我们增加一个新接口用来设置实现类。如下:

public interface IProxy {void setProxy(Object t);
}

我们期望生成的类是这样的:

public class HelloSericeProxyV2 implements IProxy, HelloService {private HelloService helloService;public HelloSericeProxyV2() {}@Overridepublic void setProxy(Object t) {this.helloService = (HelloService) t;}// 这里会做修改@Overridepublic String sayHello(String name) {System.out.println("静态代理前 ..");helloService.sayHello(name);System.out.println("静态代理后 ..");return name;}}

通过调用setProxy设置实现类,然后调用目标方法sayHello。获取到对象后可以分别转换为这两个接口进行方法调用。可能有的朋友会问,为什么void setProxy(Object t)不使用泛型。原因是对其支持不是很好,设置起来有点麻烦,所以就没设置。看一下具体的实现:

public class ProxyTest {public static void main(String[] args) throws Exception {//        final HelloServiceProxy serviceProxy = new HelloServiceProxy(new HelloService() {
//            @Override
//            public String sayHello(String name) {
//                System.out.println("目标接口实现:name=" + name);
//                return "null";
//            }
//        });
//
//        serviceProxy.sayHello("pleuvoir");//创建类,这是一个单例对象ClassPool pool = ClassPool.getDefault();pool.appendClassPath(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());//我们需要构建的类CtClass ctClass = pool.makeClass("io.github.pleuvoir.prpc.invoker.HelloServiceJavassistProxy");//这个类实现了哪些接口ctClass.setInterfaces(new CtClass[]{pool.getCtClass("io.github.pleuvoir.prpc.invoker.HelloService"),pool.getCtClass("io.github.pleuvoir.prpc.invoker.IProxy")});//新增字段CtField field$name = new CtField(pool.get("io.github.pleuvoir.prpc.invoker.HelloService"), "helloService", ctClass);//设置访问级别field$name.setModifiers(Modifier.PRIVATE);ctClass.addField(field$name);//新增构造函数//无参构造函数CtConstructor cons$noParams = new CtConstructor(new CtClass[]{}, ctClass);cons$noParams.setBody("{}");ctClass.addConstructor(cons$noParams);//重写sayHello方方法,可以通过构造字符串的形式CtMethod m = CtNewMethod.make(buildSayHello(), ctClass);ctClass.addMethod(m);// 创建一个名为 setProxy 的方法CtMethod ctMethod = new CtMethod(CtClass.voidType, "setProxy",new CtClass[]{pool.getCtClass("java.lang.Object")}, ctClass);ctMethod.setModifiers(Modifier.PUBLIC);// // $0=this  $1,$2,$3... 代表方法参数ctMethod.setBody("{$0.helloService =   $1;}");ctClass.addMethod(ctMethod);ctClass.writeFile(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());//获取实例对象final Object instance = ctClass.toClass().newInstance();System.out.println(Arrays.toString(instance.getClass().getDeclaredMethods()));//设置目标方法if (instance instanceof IProxy) {IProxy proxy = (IProxy) instance;proxy.setProxy(new HelloService() {@Overridepublic String sayHello(String name) {System.out.println("目标接口实现:name=" + name);return "null";}});}if (instance instanceof HelloService) {HelloService service = (HelloService) instance;service.sayHello("pleuvoir");}}private static String buildSayHello() {String methodString = "   public String sayHello(String name) {\n"+ "        System.out.println(\"静态代理前 ..\");\n"+ "        helloService.sayHello(name);\n"+ "        System.out.println(\"静态代理后 ..\");\n"+ "        return name;\n"+ "    }";return methodString;}
}

 

生成的.class和我们预期的一样。

参考

  • javassist tutorial
  • javassist 使用全解析

后语

OK,我们看到了javassist在生成代码上还是很方便高效的。注意事项是尽量不要使用泛型比较麻烦,另外如果生成的字节码会被其它对象缓存可以选择new ClassPool(true)的方式进行创建,而不是使用ClassPool.getDefault()单例对象。


http://chatgpt.dhexx.cn/article/cHls4v7M.shtml

相关文章

Java字节码技术javassist

一、Javassist入门 &#xff08;一&#xff09;Javassist是什么 Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类&#xff0c;并加载到JVM中&#xff1b;还可以在JVM加载时修改一个类文件。Javassist使用户不必关心字节码相关的规范也是可以编…

java--javassist学习

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba &#xff08;千叶 滋&#xff09;所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jb…

javassist使用指南

目录 一、快速入门1.1 创建class文件1.2 ClassPool的相关方法1.3 CtClass的相关方法1.4 CtMethod的相关方法1.5 调用生成的类对象1.5.1 通过反射调用1.5.2 通过接口调用 1.6 修改现有的类对象 二、将类冻结三、类搜索路径四、$开头的特殊字符五、ProxyFactory的使用 我们知道Ja…

systemd介绍

由来 Linux 的启动一直采用init进程&#xff0c;这种方法有两个缺点。一是启动时间长。init进程是串行启动&#xff0c;只有前一个进程启动完&#xff0c;才会启动下一个进程&#xff0c;二是启动脚本复杂。init进程只是执行启动脚本&#xff0c;不管其他事情。脚本需要自己处理…

systemd man手册

SYSTEMD&#xff08;1&#xff09;systemd SYSTEMD&#xff08;1&#xff09; 名称 systemd&#xff0c;init-systemd系统和服务管理器 概要 /lib/systemd/systemd [OPTIONS...]init [OPTIONS...] {COMMAND}描述 systemd是Linux操作系统的系统和服务管理器。在启动时作为PID的…

systemd wsl 测试笔记

文章目录 systemd 简介WSL systemdsystemctljournalctlhello serviceSleep 与 Timeout 测试Requires 测试After 测试 systemd 简介 Linux 从关闭到运行, 完整的启动和启动过程有三个主要部分: 硬件启动(Hardware boot): 初始化系统硬件Linux 引导(Linux boot): 加载 Linux 内核…

Linux systemd启动流程

以Ubuntu 18.04.2 LTS为例&#xff0c;列出Systemd启动target中涉及单元(Unit). default.target ( graphical.target by default) graphical.targetRequiresmulti-user.targetWantsdisplay-manager.serviceConflictsrescue.service rescue.targetAftermulti-user.target rescu…

关于 Linux中systemd的一些笔记

写在前面 嗯&#xff0c;准备RHCA&#xff0c;学习整理这部分知识博文内容涉及&#xff1a; systemd简述对于unit的信息的介绍通过systemctl命令控制Service unit的DemoService unit配置文件内容,权值的一些介绍 傍晚时分&#xff0c;你坐在屋檐下&#xff0c;看着天慢慢地黑下…

Linux-Systemd

目录 一、Systemd概述 二、Systemd优势 2.1兼容性 2.2启动速度 2.3systemd 提供按需启动能力 2.4采用 linux 的 cgroups 跟踪和管理进程的生命周期 2.5启动挂载点和自动挂载的管理 2.6实现事务性依赖关系管理 2.7日志服务 2.8 依赖关系 2.9systemd 事务 三、unit(单…

Systemd 简介

一 概述 Linux 服务管理有两种方式service和systemctl。而systemd是Linux系统最新的初始化系统(init)&#xff0c;作用是提高系统的启动速度&#xff0c;尽可能启动较少的进程&#xff0c;尽可能更多进程并发启动&#xff0c;systemd对应的进程管理命令就是systemctl。值得一提…

systemd简介

Systemd 是一个专用于 Linux 操作系统的系统与服务管理器,其目的是要取代Unix时代以来一直在使用的init系统。 systemd概述Systemd 是 Linux 系统中最新的初始化系统(init),它主要的设计目标是克服 sysvinit 固有的缺点,提高系统的启动速度。 systemd框架图 根据 Linux 惯…

Systemd 入门及常用命令

目录 Systemdsystemd架构systemd 系统管理管理系统查看启动耗时查看当前主机的信息 Unit 资源查看当前系统的所有 Unit查看系统状态和单个 Unit 的状态 Unit 管理依赖关系Unit 配置文件查看配置文件的内容 Targettarget&#xff08;Systemd&#xff09; 与 runlevel&#xff08…

systemd 介绍

转自&#xff1a;https://www.linuxidc.com/Linux/2018-03/151291.htm 从 init 系统说起 Linux 操作系统的启动首先从 BIOS 开始&#xff0c;接下来进入 boot loader&#xff0c;由 bootloader 载入内核&#xff0c;进行内核初始化。内核初始化的最后一步就是启动 PID 为 1 的 …

[2020.1.10]systemd介绍

1 systemd基本概念 systemd:a system daemon&#xff0c;相当于以前的init进程&#xff0c;pid1&#xff0c;systemd是1号进程!!! sbin/init --> /lib/systemd/systemd unit: 一个进程,例如lightdm.service job: 一个动作,启动是个job,关闭是个job。开机启动时,systemd…

systemd教程(三)

下来会通过示例来描述不同Service Type值的应用场景。在此之前&#xff0c;强烈建议先阅读前后台进程父子关系和daemon类进程来搞懂进程之间的关系和Daemon类进程的特性。 systemd service&#xff1a;Typeforking 当使用systemd去管理一个长久运行的服务进程时&#xff0c;最…

dpkg打包笔记

dpkg 安装软件 dpkg -i xxx.deb 查看安装目录 dpkg -L xxx 显示版本 dpkg -l xxx 详细信息 dpkg -s xxx 罗列内容 dpkg -c xxxx 卸载软件 dpkg -r xxxx 构建deb包 dpkg -p

dpkg和apt

文章目录 Ubuntu软件安装Deb包安装Deb包简介dpkg常用命令 apt-get源安装apt-get简介apt工作原理apt相关文件常用的apt命令参数apt-getapt-cache dpkg和apt-get的区别 Ubuntu软件安装 Linux有很多种发行版本&#xff0c;各种发行版本之间安装软件方式和命令不一样&#xff0c;同…

dpkg安装

目录 转载&#xff1a;https://blog.csdn.net/u012300744/article/details/80267225 1.安装 &#xff08;3&#xff09;安装 sudo dpkg -i deb文件名 &#xff08;4&#xff09;根据经验&#xff0c;通常情况下会报依赖关系的错误&#xff0c;我们可以使用以下的命令修复安…

dpkg制作deb包详解

1 deb包文件结构 deb 软件包里面的结构&#xff0c;它具有DEBIAN和软件具体安装目录&#xff08;如etc, usr, opt, tmp等&#xff09;。在DEBIAN目录中至少必须包括control文件&#xff0c;还有可能postinst(postinstallation)、postrm(postremove)、preinst(preinstallation)、…

dpkg命令的用法

dpkg命令的用法 dpkg 是Debian package的简写&#xff0c;为”Debian“ 操作系统 专门开发的套件管理系统&#xff0c;用于软件的安装&#xff0c;更新和移除。 所有源自"Debian"的Linux的发行版都使用 dpkg, 例如"Ubuntu" 阅读目录 安装软件列出与该…