java插桩-javaassist

目录

Java插桩工具

简介

目录

源代码级别

ClassPool

CtClass

Loader 

CtField

CtMethod

$符号含义

使用步骤

举例

项目结构1

代码1

项目结构2

代码2

字节码级别

ClassFile

FieldInfo

MethodInfo

CodeAttribute

CodeIterator

举例

参考


Java插桩工具

字节码的抽象级别描述例子
低级库需要直接在字节码级别上进行操作。通常,它们提供大多数功能丰富的功能,但与其他字节码操作工具相比,它们的使用也最复杂。

ASM(ASM –主页

BCEL(https://commons.apache.org/proper/commons-bcel/ 

中级水平库提供了字节码的某种抽象级别,并简化了其修改。例如,代替修改字节码,可以使用类似于Java的语法进行更改,然后将其编译为字节码,然后由使用的库修改为原始字节码。通常,它们缺少修改后的代码验证的功能-这意味着,错误可能在修改准备过程中被忽略,然后在运行时被发现。Javassist(jboss-javassist的Javassist
高水平库使用高级指令进行操作,并且通常配备有用于语法验证的工具集。不幸的是,从修改后的字节码进行的最高抽象化通常会导致某些功能的丧失,这些功能仅在直接修改字节码时可用。

AspectJ(AspectJ项目

CGLib(CGLib GitHub,基于ASM)

简介

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):反射调用字节码生成类的方法。

对于setBody $0代表this $1、$2、...代表方法的第几个参数

setBody("{$0.name = $1;}");

$符号含义

$符号含义
符号含义
$0, $1, $2, ...  this,第几个参数
$args参数列表. $args的类型是Object[].
$$所有实参.例如, m($$) 等价于 m($1,$2,...)
$cflow(...)cflow变量
$r结果类型. 用于表达式转换.
$w包装类型. 用于表达式转换.
$_结果值
$sigjava.lang.Class列表,代表正式入参类型
$typejava.lang.Class对象,代表正式入参值.
$classjava.lang.Class对象,代表传入的代码段.

使用步骤

使用步骤

举例

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

项目结构1

项目结构

代码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);
    }
}
结果

项目结构2

项目结构

代码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方法插桩,演示了添加变量,访问变量,修改变量,方法前插,后插,任意位置插。

learn插桩结果
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参考博客

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

喜欢本文的请动动小手点个赞,收藏一下,有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系。如果您感觉有所收获,自愿打赏,可选择支付宝18833895206(小于),您的支持是我不断更新的动力。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lady_killer9

感谢您的打赏,我会加倍努力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值