作者: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 文件,ClassPool
是 CtClass
对象的容器。它按需读取类文件来结构 CtClass
对象,并且保留 CtClass
对象以便当前应用。
须要留神的是 ClassPool 会在内存中保护所有被它创立过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API 中给出的解决方案是 无意识的调用 CtClass
的detach()
办法以开释内存。
ClassPool
须要关注的办法:
- getDefault : 返回默认的
ClassPool
是单例模式的,个别通过该办法创立咱们的 ClassPool; - appendClassPath, insertClassPath : 将一个
ClassPath
加到类搜寻门路的开端地位 或 插入到起始地位。通常通过该办法写入额定的类搜寻门路,以解决多个类加载器环境中找不到类的难堪; - toClass : 将批改后的 CtClass 加载至以后线程的上下文类加载器中,CtClass 的
toClass
办法是通过调用本办法实现。须要留神的是一旦调用该办法,则无奈持续批改曾经被加载的 class; - get , getCtClass : 依据类路径名获取该类的 CtClass 对象,用于后续的编辑。
CtClass
须要关注的办法:
- freeze : 解冻一个类,使其不可批改;
- isFrozen : 判断一个类是否已被解冻;
- prune : 删除类不必要的属性,以缩小内存占用。调用该办法后,许多办法无奈将无奈失常应用,慎用;
- defrost : 冻结一个类,使其能够被批改。如果当时晓得一个类会被 defrost,则禁止调用 prune 办法;
- detach : 将该 class 从 ClassPool 中删除;
- writeFile : 依据 CtClass 生成
.class
文件; - toClass : 通过类加载器加载该 CtClass。
下面咱们创立一个新的办法应用了 CtMethod
类。CtMthod 代表类中的某个办法,能够通过 CtClass 提供的 API 获取或者 CtNewMethod 新建,通过 CtMethod 对象能够实现对办法的批改。
CtMethod
中的一些重要办法:
- insertBefore : 在办法的起始地位插入代码;
- insterAfter : 在办法的所有 return 语句前插入代码以确保语句可能被执行,除非遇到 exception;
- insertAt : 在指定的地位插入代码;
- setBody : 将办法的内容设置为要写入的代码,当办法被 abstract 润饰时,该修饰符被移除;
- 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 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!