作者: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开发手册(嵩山版)》最新公布,速速下载!

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