java插桩-javaassist

简介: java插桩-javaassist

Java插桩工具

image.png

image.png

image.png

简介

Javassist (JAVA programming Assistant,Java编程助手) 是一个用Java编辑字节码的类库。它使Java程序可以在运行时定义新类,并在JVM加载它时修改类文件。


与其他类似的字节码编辑器不同,Javassist提供两个级别的API:源代码级别和字节码级别。


如果使用源代码级API,则可以在不了解Java字节码规范的情况下编辑类文件。整个API仅使用Java语言的词汇表进行设计。甚至可以以源文本的形式指定插入的字节码。Javassist可以即时对其进行编译。


另一方面,字节码级API允许用户像其他编辑器一样直接编辑类文件。


我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassist。


类似的技术还有:bcel,asm等,他们相对于Javassit,偏向底层,效率较高,但编码难度更高(需要了解JVM指令)。


Javassist是Jboss的一个子项目,其特点是简单:不需要了解底层JVM指令,直接用Java代码编写,容易理解,并且现在生成代码效率和以上两种技术相差已经很小。目前,最新版本 3.27.0-GA (2020年03月19日).

目录

├── build.xml

├── javassist.jar

├── License.html

├── pom.xml

├── Readme.html

├── README.md

├── sample

│   ├── duplicate

│   ├── evolve

│   ├── hotswap

│   ├── preproc

│   ├── reflect

│   ├── rmi

│   ├── Test.java

│   └── vector

├── src

│   ├── main

│   └── test

└── tutorial

   ├── brown.css

   ├── tutorial2.html

   ├── tutorial3.html

   └── tutorial.html

  • sample  样例
  • src         源代码
  • tutorial   教程
  • Readme.html 自述文件。
  • License.html  许可证文件
  • tutorial 教程目录
  • javassist.jar  jar文件(类文件)
  • src        源代码目录

源代码级别

ClassPool

CtClass对象的容器。

  1. getDefault (): 返回默认的ClassPool ,单例模式,一般通过该方法创建我们的ClassPool;
  2. appendClassPath(ClassPath cp), insertClassPath(ClassPath cp) : 将一个ClassPath加到类搜索路径的末尾位置或插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类问题;
  3. importPackage(String packageName):导入包;
  4. makeClass(String classname):创建一个空类,没有变量和方法,后序通过CtClass的函数进行添加;
  5. get(String classname)、getCtClass(String classname) : 根据类路径名获取该类的CtClass对象,用于后续的编辑。

CtClass

  1. debugDump;String类型,如果生成。class文件,保存在这个目录下。
  2. setName(String name):给类重命名;
  3. setSuperclass(CtClass clazz):设置父类;
  4. addField(CtField f, Initializer init):添加字段(属性),初始值见CtField;
  5. addMethod(CtMethod m):添加方法(函数);
  6. toBytecode(): 返回修改后的字节码。需要注意的是一旦调用该方法,则无法继续修改CtClass
  7. toClass(): 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的CtClass
  8. writeFile(String directoryName):根据CtClass生成 .class 文件;
  9. defrost():解冻类,用于使用了toclass()、toBytecode、writeFile(),类已经被JVM加载,Javassist冻结CtClass后;
  10. detach():避免内存溢出,从ClassPool中移除一些不需要的CtClass。

Loader

类加载器

  1. loadClass(String name):加载类

CtField

字段

  1. CtField(CtClass type, String name, CtClass declaring) :构造函数,添加字段类型,名称,所属的类;
  2. CtField.Initializer constant():CtClass使用addField时初始值的设置;
  3. setModifiers(int mod):设置访问级别,一般使用Modifier调用常量。

CtMethod

方法

  1. insertBefore(String src):在方法的起始位置插入代码;
  2. insertAfter(String src):在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  3. insertAt(int lineNum, String src):在指定的位置插入代码;
  4. addCatch(String src, CtClass exceptionType):将方法内语句作为try的代码块,插入catch代码块src;
  5. setBody(String src):将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  6. setModifiers(int mod):设置访问级别,一般使用Modifier调用常量;
  7. invoke(Object obj, Object... args):反射调用字节码生成类的方法。
  8. 对于setBody $0代表this $1、$2、...代表方法的第几个参数
setBody("{$0.name = $1;}");

$符号含义

                                             $符号含义

             image.png

使用步骤

2020062310470442.png

                                                            使用步骤

举例

新建Java项目,导入javassist.jar。

项目结构1

2020062310470442.png

                                                   项目结构

代码1

Main无所谓,是我建项目时选择了Hello World模板

Test.java

package bupt.edu.cn;
import javassist.*;
import java.lang.reflect.Method;
public class Test {
    public static void createStudent() throws  Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("E:\\Workspace\\IDEA_workspace\\JavassistTest\\src\\bupt.edu.cn.Student");
        // 字段名为name
        CtField param = new CtField(pool.get("java.lang.String"),"name", cc);
        // 访问级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "Frankyu"
        cc.addField(param, CtField.Initializer.constant("Frankyu"));
        // 生成 getter、setter 方法
        cc.addMethod(CtNewMethod.setter("setName", param));
        cc.addMethod(CtNewMethod.getter("getName", param));
        // 添加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"yubo\";}");
        cc.addConstructor(cons);
        // 5. 添加有参的构造函数
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0=this / $1,$2,$3... 代表方法参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);
        // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        cc.addMethod(ctMethod);
        //这里会将这个创建的类对象编译为.class文件
        cc.writeFile("");
    }
    public static void usingStudent() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.getCtClass("E:\\Workspace\\IDEA_workspace\\JavassistTest\\src\\bupt.edu.cn.Student");
        // 实例化
        Object student = cc.toClass().newInstance();
        // 设置值
        Method setName = student.getClass().getMethod("setName", String.class);
        setName.invoke(student, "junjie");
        // 输出值
        Method execute = student.getClass().getMethod("printName");
        execute.invoke(student);
    }
    public static void main(String[] args) throws Exception{
        createStudent();
        usingStudent();
    }
}

注意:.class文件的绝对路径需要修改

Student.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package E:\Workspace\IDEA_workspace\JavassistTest\src\bupt.edu.cn;
public class Student {
    private String name = "Frankyu";
    public void setName(String var1) {
        this.name = var1;
    }
    public String getName() {
        return this.name;
    }
    public Student() {
        this.name = "yubo";
    }
    public Student(String var1) {
        this.name = var1;
    }
    public void printName() {
        System.out.println(this.name);
    }
}

2020062310470442.png

                                             结果

项目结构2

 2020062310470442.png

                                                  项目结构

代码2

Student.java

package bupt.edu.cn;
import java.util.Random;
public class Student {
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    private int age;
    public int getGrade() {
        return grade;
    }
    public void setGrade(int grade) {
        this.grade = grade;
    }
    private int grade;
    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", grade=" + grade +
                '}';
    }
    public void display() {
        System.out.println(this.toString());
    }
    public void learn() throws Exception{
//        long start = System.currentTimeMillis();
        Random rand = new Random();
        int time =  rand.nextInt(1000)+3000;
        Thread.sleep(time);
//        long end = System.currentTimeMillis();
//        System.out.println(end-start);
    }
    public static void main(String[] args) {
        Student s  = new Student();
        s.display();
        try{
            s.learn();
        }
        catch (Exception e){
        }
    }
}

Monitor.java

import bupt.edu.cn.Student;
import javassist.*;
public class Monitor {
   final static ClassPool pool = ClassPool.getDefault();
   final static String classname = "bupt.edu.cn.Student";
    public void studentLearnMonitor() throws Exception{
        CtClass ss = pool.getCtClass(classname);
        CtClass.debugDump="./dump";
        String methodname = "learn";
        CtMethod learn_ori = ss.getDeclaredMethod(methodname);
        //拷贝一份learn方法
        CtMethod learn_cp = CtNewMethod.copy(learn_ori,learn_ori.getName()+"_cp",ss,null);
        //添加拷贝后的方法
        ss.addMethod(learn_cp);
        //修改learn方法:原代码前后添加时间
        String src = "{"+
                "long start = System.currentTimeMillis();" +
                learn_ori.getName()+"_cp($$);"+
                "long end = System.currentTimeMillis();"+
                "System.out.println(end-start);"+
                "}";
        learn_ori.setBody(src);
        ss.toClass();
        //生成.class文件,主要用于调试,查看是否有代码片段被忽略
        //ss.writeFile();
        Student s = new Student();
        s.learn();
    }
    public void studentDisplayMonitor() throws Exception{
        CtClass ss = pool.getCtClass(classname);
        CtClass.debugDump="./dump";
        //添加字段name
        CtField param = new CtField(pool.get("java.lang.String"),"name", ss);
        // 访问级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "Frankyu"
        ss.addField(param, CtField.Initializer.constant("Frankyu"));
        String methodname = "display";
        CtMethod display = ss.getDeclaredMethod(methodname);
        String src = "{"+
                "System.out.println($0.age);" +
                "$0.name=\"frankyu\";" +
                "System.out.println($0.name);"+
                "}";
        display.insertBefore(src);
        if(true){
            System.out.println("Hello javassist");
        }
        src ="if(true){" +
                "System.out.println(\"Hello javassist\");" +
                "}";
        display.insertAt(7,src);
        src = "{"+
                "System.out.println($0.grade);"+
                "}";
        display.insertAfter(src);
        ss.toClass();
        //ss.writeFile();
        Student s = new Student();
        s.display();
    }
    public static void main(String[] args) throws Exception{
            Monitor m = new Monitor();
            // 由于类冻结问题,两个方法不可同时调用
//            m.studentLearnMonitor();
            m.studentDisplayMonitor();
    }
}

studentLearnMonitor对learn方法插桩,通过拷贝的方式,使start和end属于同一个代码块,解决不同代码块之间变量无法使用问题。


studentDisplayMonitor对display方法插桩,演示了添加变量,访问变量,修改变量,方法前插,后插,任意位置插。

2020062310470442.png

                                  learn插桩结果

2020062310470442.png

                      display插桩结果

调用studentDisplayMonitor时,dump目录下,Student.class部分代码

    public void display() {
        System.out.println(this.age);
        this.name = "frankyu";
        System.out.println(this.name);
        if (true) {
            System.out.println("Hello javassist");
        }
        System.out.println(this.toString());
        Object var2 = null;
        System.out.println(this.grade);
    }

调用studentLearnMonitor时,dump目录下,Student.class部分代码

    public void learn() throws Exception {
        long var1 = System.currentTimeMillis();
        this.learn_cp();
        long var3 = System.currentTimeMillis();
        System.out.println(var3 - var1);
    }

字节码级别

ClassFile

  1. getFields():返回字段列表;
  2. addField(FieldInfo finfo):添加字段;
  3. addMethod(MethodInfo minfo) :添加方法;
  4. getMethod(String name):根据方法名返回MethodInfo对象;

FieldInfo

字段

  1. setAccessFlags(int acc):设置访问级别,通过类AccessFlag调用它的常量PUBLIC等。

MethodInfo

方法

  1. getCodeAttribute():返回CodeAttribute对象。

CodeAttribute

代码属性

  1. iterator():返回代码迭代器

CodeIterator

代码指令迭代器

  1. void begin()
    移到第一个指令处.
  2. void move(int index)
    移到指定索引处
  3. boolean hasNext()
    如果存在指令的话,返回true
  4. int next()
    返回下一个指令的索引
    需要注意的是,此方法并不会返回下一个指令的操作码
  5. int byteAt(int index)
    返回指定索引处的无符号8bit位长值.
  6. int u16bitAt(int index)
    返回指定索引处的无符号16bit位长值.
  7. int write(byte[] code, int index)
    在指定索引处写入字节数组.
  8. void insert(int index, byte[] code)
    在指定索引处写入字节数组,其他字节码的offset等将会自适应更改。

举例

参考

javassist官网

javassist的Github

javassist在线API手册

javassist参考博客

更多内容查看:网络安全-自学笔记

喜欢本文的请动动小手点个赞,收藏一下,有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系

相关文章
|
5月前
|
数据采集 监控 Oracle
GraalVM 24 正式发布阿里巴巴贡献重要特性 —— 支持 Java Agent 插桩
阿里巴巴是 GraalVM 全球顾问委员会的唯一中国代表,阿里云程序语言与编译器团队和可观测团队合作实现了 GraalVM 应用的无侵入可观测能力,并在 ARMS 平台上线了该功能。目前在 GraalVM 24 中发布的是支持 Java agent 的第一步,其余能力将在 GraalVM 的后续版本中陆续发布。
396 22
|
XML 网络协议 数据可视化
java插桩-Jacoco java代码覆盖率可视化
java插桩-Jacoco java代码覆盖率可视化
937 0
java插桩-Jacoco java代码覆盖率可视化
|
Java Windows
【Java】使用javaassist修改jar包
由于工作需要,可能会涉及到一些需要对第三方的一些jar包与源码进行修改的情况,这个时候javaassist就可以派上用场。 javaassist是一个开源的编辑、修改、创建字节码的类库,它在JBoos项目的AOP框架中发挥了很大的作用。不过我们这次仅仅使用它的修改class的功能。 1 准备工具 1.1 需要用到的工具 1.javaassist.jar 修改class字节码,修改类
8145 0
|
2月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
127 0
|
2月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
282 83
|
3月前
|
存储 SQL 安全
Java 无锁方式实现高性能线程实战操作指南
本文深入探讨了现代高并发Java应用中单例模式的实现方式,分析了传统单例(如DCL)的局限性,并提出了多种无锁实现方案。包括基于ThreadLocal的延迟初始化、VarHandle原子操作、Record不可变对象、响应式编程(Reactor)以及CDI依赖注入等实现方式。每种方案均附有代码示例及适用场景,同时通过JMH性能测试对比各实现的优劣。最后,结合实际案例设计了一个高性能配置中心,展示了无锁单例在实际开发中的应用。总结中提出根据场景选择合适的实现方式,并遵循现代单例设计原则以优化性能和安全性。文中还提供了代码获取链接,便于读者实践与学习。
94 0
|
2月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
230 83
|
4月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
183 0
|
3月前
|
存储 Java
说一说 JAVA 内存模型与线程
我是小假 期待与你的下一次相遇 ~