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参考博客

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

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

相关文章
|
Java Windows
【Java】使用javaassist修改jar包
由于工作需要,可能会涉及到一些需要对第三方的一些jar包与源码进行修改的情况,这个时候javaassist就可以派上用场。 javaassist是一个开源的编辑、修改、创建字节码的类库,它在JBoos项目的AOP框架中发挥了很大的作用。不过我们这次仅仅使用它的修改class的功能。 1 准备工具 1.1 需要用到的工具 1.javaassist.jar 修改class字节码,修改类
7969 0
|
20天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
28天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
11天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
6天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
11天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
41 1
|
19天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
40 6
|
19天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
18天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
24天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
49 9