关于java:一个牛逼的-Java-字节码类库

60次阅读

共计 6849 个字符,预计需要花费 18 分钟才能阅读完成。

作者:rickiyang

出处:www.cnblogs.com/rickiyang/p/11336268.html

Java 字节码以二进制的模式存储在 .class 文件中,每一个 .class 文件蕴含一个 Java 类或接口。

Javaassist 就是一个用来解决 Java 字节码的类库。它能够在一个曾经编译好的类中增加新的办法,或者是批改已有的办法,并且不须要对字节码方面有深刻的理解。同时也能够去生成一个新的类对象,通过齐全手动的形式。

1. 应用 Javassist 创立一个 class 文件

首先须要引入 jar 包:

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

编写创建对象的类:

package com.rickiyang.learn.javassist;

import javassist.*;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class CreatePerson {

    /**
     * 创立一个 Person 对象
     *
     * @throws Exception
     */
    public static void createPseson() throws Exception {ClassPool pool = ClassPool.getDefault();

        // 1. 创立一个空类
        CtClass cc = pool.makeClass("com.rickiyang.learn.javassist.Person");

        // 2. 新增一个字段 private String name;
        // 字段名为 name
        CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
        // 拜访级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "xiaoming"
        cc.addField(param, CtField.Initializer.constant("xiaoming"));

        // 3. 生成 getter、setter 办法
        cc.addMethod(CtNewMethod.setter("setName", param));
        cc.addMethod(CtNewMethod.getter("getName", param));

        // 4. 增加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"xiaohong\";}");
        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("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
    }

    public static void main(String[] args) {
        try {createPseson();
        } catch (Exception e) {e.printStackTrace();
        }
    }
}

执行下面的 main 函数之后,会在指定的目录内生成 Person.class 文件:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.rickiyang.learn.javassist;

public class Person {
    private String name = "xiaoming";

    public void setName(String var1) {this.name = var1;}

    public String getName() {return this.name;}

    public Person() {this.name = "xiaohong";}

    public Person(String var1) {this.name = var1;}

    public void printName() {System.out.println(this.name);
    }
}

跟咱们料想的一样。

在 Javassist 中,类 Javaassit.CtClass 示意 class 文件。一个 GtClass (编译时类)对象能够解决一个 class 文件,ClassPoolCtClass 对象的容器。它按需读取类文件来结构 CtClass 对象,并且保留 CtClass 对象以便当前应用。

须要留神的是 ClassPool 会在内存中保护所有被它创立过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API 中给出的解决方案是 无意识的调用 CtClassdetach()办法以开释内存

ClassPool须要关注的办法:

  1. getDefault : 返回默认的ClassPool 是单例模式的,个别通过该办法创立咱们的 ClassPool;
  2. appendClassPath, insertClassPath : 将一个 ClassPath 加到类搜寻门路的开端地位 或 插入到起始地位。通常通过该办法写入额定的类搜寻门路,以解决多个类加载器环境中找不到类的难堪;
  3. toClass : 将批改后的 CtClass 加载至以后线程的上下文类加载器中,CtClass 的 toClass 办法是通过调用本办法实现。须要留神的是一旦调用该办法,则无奈持续批改曾经被加载的 class
  4. get , getCtClass : 依据类路径名获取该类的 CtClass 对象,用于后续的编辑。

CtClass须要关注的办法:

  1. freeze : 解冻一个类,使其不可批改;
  2. isFrozen : 判断一个类是否已被解冻;
  3. prune : 删除类不必要的属性,以缩小内存占用。调用该办法后,许多办法无奈将无奈失常应用,慎用;
  4. defrost : 冻结一个类,使其能够被批改。如果当时晓得一个类会被 defrost,则禁止调用 prune 办法;
  5. detach : 将该 class 从 ClassPool 中删除;
  6. writeFile : 依据 CtClass 生成 .class 文件;
  7. toClass : 通过类加载器加载该 CtClass。

下面咱们创立一个新的办法应用了 CtMethod 类。CtMthod 代表类中的某个办法,能够通过 CtClass 提供的 API 获取或者 CtNewMethod 新建,通过 CtMethod 对象能够实现对办法的批改。

CtMethod中的一些重要办法:

  1. insertBefore : 在办法的起始地位插入代码;
  2. insterAfter : 在办法的所有 return 语句前插入代码以确保语句可能被执行,除非遇到 exception;
  3. insertAt : 在指定的地位插入代码;
  4. setBody : 将办法的内容设置为要写入的代码,当办法被 abstract 润饰时,该修饰符被移除;
  5. make : 创立一个新的办法。

留神到在下面代码中的:setBody()的时候咱们应用了一些符号:

// $0=this / $1,$2,$3... 代表办法参数
cons.setBody("{$0.name = $1;}");

具体还有很多的符号能够应用,然而不同符号在不同的场景下会有不同的含意,所以在这里就不在赘述,能够看 javassist 的阐明文档。http://www.javassist.org/tuto…

Java 核心技术教程和示例源码:https://github.com/javastacks…

2. 调用生成的类对象

1. 通过反射的形式调用

下面的案例是创立一个类对象而后输入该对象编译完之后的 .class 文件。那如果咱们想调用生成的类对象中的属性或者办法应该怎么去做呢?javassist 也提供了相应的 api,生成类对象的代码还是和第一段一样,将最初写入文件的代码替换为如下:

// 这里不写入文件,间接实例化
Object person = cc.toClass().newInstance();
// 设置值
Method setName = person.getClass().getMethod("setName", String.class);
setName.invoke(person, "cunhua");
// 输入值
Method execute = person.getClass().getMethod("printName");
execute.invoke(person);

而后执行 main 办法就能够看到调用了 printName办法。

2. 通过读取 .class 文件的形式调用
ClassPool pool = ClassPool.getDefault();
// 设置类门路
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
Object person = ctClass.toClass().newInstance();
//  ...... 上面和通过反射的形式一样去应用
3. 通过接口的形式

下面两种其实都是通过反射的形式去调用,问题在于咱们的工程中其实并没有这个类对象,所以反射的形式比拟麻烦,并且开销也很大。那么如果你的类对象能够形象为一些办法得合集,就能够思考为该类生成一个接口类。这样在 newInstance() 的时候咱们就能够强转为接口,能够将反射的那一套省略掉了。

还拿下面的 Person 类来说,新建一个 PersonI 接口类:

package com.rickiyang.learn.javassist;

/**
 * @author rickiyang
 * @date 2019-08-07
 * @Desc
 */
public interface PersonI {void setName(String name);

    String getName();

    void printName();}

实现局部的代码如下:

ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");

// 获取接口
CtClass codeClassI = pool.get("com.rickiyang.learn.javassist.PersonI");
// 获取下面生成的类
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
// 使代码生成的类,实现 PersonI 接口
ctClass.setInterfaces(new CtClass[]{codeClassI});

// 以下通过接口间接调用 强转
PersonI person = (PersonI)ctClass.toClass().newInstance();
System.out.println(person.getName());
person.setName("xiaolv");
person.printName();

应用起来很轻松。

2. 批改现有的类对象 #

后面说到新增一个类对象。这个应用场景目前还没有遇到过,个别会遇到的应用场景应该是批改已有的类。比方常见的日志切面,权限切面。咱们利用 javassist 来实现这个性能。

有如下类对象:

package com.rickiyang.learn.javassist;

/**
 * @author rickiyang
 * @date 2019-08-07
 * @Desc
 */
public class PersonService {public void getPerson(){System.out.println("get Person");
    }

    public void personFly(){System.out.println("oh my god,I can fly");
    }
}

而后对他进行批改:

package com.rickiyang.learn.javassist;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;

import java.lang.reflect.Method;

/**
 * @author rickiyang
 * @date 2019-08-07
 * @Desc
 */
public class UpdatePerson {public static void update() throws Exception {ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("com.rickiyang.learn.javassist.PersonService");

        CtMethod personFly = cc.getDeclaredMethod("personFly");
        personFly.insertBefore("System.out.println(\" 腾飞之前筹备降落伞 \");");
        personFly.insertAfter("System.out.println(\" 胜利落地。。。。\");");


        // 新增一个办法
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "joinFriend", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(\"i want to be your friend\");}");
        cc.addMethod(ctMethod);

        Object person = cc.toClass().newInstance();
        // 调用 personFly 办法
        Method personFlyMethod = person.getClass().getMethod("personFly");
        personFlyMethod.invoke(person);
        // 调用 joinFriend 办法
        Method execute = person.getClass().getMethod("joinFriend");
        execute.invoke(person);
    }

    public static void main(String[] args) {
        try {update();
        } catch (Exception e) {e.printStackTrace();
        }
    }
}

personFly 办法前后加上了打印日志。而后新增了一个办法joinFriend。执行 main 函数能够发现曾经增加上了。

另外须要留神的是:下面的 insertBefore()setBody() 中的语句,如果你是单行语句能够间接用双引号,然而有多行语句的状况下,你须要将多行语句用 {} 括起来。javassist 只承受单个语句或用大括号括起来的语句块。

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0