Javassist基本用法汇总
最近项目需要对基础架构做增强,需要基于字节码在不侵入原有代码的情况下实现, 故把javassist的基本用法过了一遍。这篇博客就是把主要讲讲为什么要用javassist以及javassist的基本用法。
1.为什么要使用javassist(上手成本低)
基于字节码增强的框架有两个ASM和javassit,下面是两个框架的特点以及对比
Javassist & ASM 对比
1.Javassist源代码级API比ASM中实际的字节码操作更容易使用
2.Javassist在复杂的字节码级操作上提供了更高级别的抽象层。Javassist源代码级API只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易、更快。
3.Javassist使用反射机制,这使得它比运行时使用Classworking技术的ASM慢。
总的来说ASM比Javassist快得多,并且提供了更好的性能。Javassist使用Java源代码的简化版本,然后将其编译成字节码。这使得Javassist非常容易使用,但是它也将字节码的使用限制在Javassist源代码的限制之内。
总之,如果有人需要更简单的方法来动态操作或创建Java类,那么应该使用Javassist API 。如果需要注重性能地方,应该使用ASM库。
2. javassist基本用法(基于3.28.0-GA的版本)
Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.
Javassist中最为重要的是ClassPool, CtClass, CtMethod以及CtField这几个类.
ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass对象
CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
CtMethods: 表示类中的方法
CtFields: 表示类中的字段
2.1 ClassPool对象
2.1.1 ClassPool的创建
// 获取ClassPool对象, 使用系统默认类路径 ClassPool pool = new ClassPool(true); // 效果与 new ClassPool(true) 一致 ClassPool pool1 = ClassPool.getDefault();
为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.
// 从ClassPool中删除CtClass对象 ctClass.detach(); // 也可以每次创建一个新的ClassPool, 而不是ClassPool.getDefault(), 避免内存溢出 ClassPool pool2 = new ClassPool(true);
2.1.2 classpath
通过 ClassPool.getDefault()获取的ClassPool使用JVM的classpath.在Tomcat等Web服务器运行时, 服务器会使用多个类加载器作为系统类加载器, 这可能导致ClassPool可能无法找到用户的类. 这时, ClassPool须添加额外的classpath才能搜索到用户的类.
// 将classpath插入到指定classpath之前 pool.insertClassPath(new ClassClassPath(this.getClass())); // 将classpath添加到指定classpath之后 pool.appendClassPath(new ClassClassPath(this.getClass())); // 将一个目录作为classpath pool.insertClassPath("/xxx/lib");
2.2 CtClass对象
2.2.1 获取CtClass
// 通过类名获取 CtClass, 未找到会抛出异常 CtClass ctClass = pool.get("com.kawa.ssist.JustRun"); // 通过类名获取 CtClass, 未找到返回 null, 不会抛出异常 CtClass ctClass1 = pool.getOrNull("com.kawa.ssist.JustRun");
2.2.2 创建CtClass
// 复制一个类 CtClass ctClass2 = pool.getAndRename("com.kawa.ssist.JustRun", "com.kawa.ssist.JustRunq"); // 创建一个新类 CtClass ctClass3 = pool.makeClass("com.kawa.ssist.JustRuna"); // 通过class文件创建一个新类 CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("/home/un/test/JustRun.class")));
2.2.3 CtClass基础信息
// 类名 String simpleName = ctClass.getSimpleName(); // 类全名 String name = ctClass.getName(); // 包名 String packageName = ctClass.getPackageName(); // 接口 CtClass[] interfaces = ctClass.getInterfaces(); // 继承类 CtClass superclass = ctClass.getSuperclass(); // 获取类方法 CtMethod ctMethod = ctClass.getDeclaredMethod("getName()", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())}); // 获取类字段 CtField ctField = ctClass.getField("name"); // 判断数组类型 ctClass.isArray(); // 判断原生类型 ctClass.isPrimitive(); // 判断接口类型 ctClass.isInterface(); // 判断枚举类型 ctClass.isEnum(); // 判断注解类型 ctClass.isAnnotation(); // 冻结一个类,使其不可修改 ctClass.freeze () // 判断一个类是否已被冻结 ctClass.isFrozen() // 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用 ctClass.prune() //解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用prune方法 ctClass.defrost()
2.2.4 CtClass类操作
// 添加接口 ctClass.addInterface(...); // 添加构造器 ctClass.addConstructor(...); // 添加字段 ctClass.addField(...); // 添加方法 ctClass.addMethod(...);
2.2.5 CtClass类编译
// 获取字节码文件 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class Class clazz = ctClass.toClass(); // 类的字节码文件 ClassFile classFile = ctClass.getClassFile(); // 编译成字节码文件, 使用当前线程上下文类加载器加载类, 如果类已存在或者编译失败将抛出异常 byte[] bytes = ctClass.toBytecode();
2.3 CtMethod对象
2.3.1 获取CtMethod属性
CtClass ctClass5 = pool.get(TestService.class.getName()); CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder"); // 方法名 String methodName = ctMethod.getName(); // 返回类型 CtClass returnType = ctMethod.getReturnType(); // 方法参数, 通过此种方式得到方法参数列表 // 格式: com.kawa.TestService.getOrder(java.lang.String,java.util.List) ctMethod.getLongName(); // 方法签名 格式: (Ljava/lang/String;Ljava/util/List;Lcom/test/Order;)Ljava/lang/Integer; ctMethod.getSignature(); // 获取方法参数名称, 可以通过这种方式得到方法真实参数名称 List<String> argKeys = new ArrayList<>(); MethodInfo methodInfo = ctMethod.getMethodInfo(); CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); int len = ctMethod.getParameterTypes().length; // 非静态的成员函数的第一个参数是this int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1; for (int i = pos; i < len; i++) { argKeys.add(attr.variableName(i)); }
2.3.2 CtMethod方法体修改
// 在方法体前插入代码块 ctMethod.insertBefore(""); // 在方法体后插入代码块 ctMethod.insertAfter(""); // 在某行 字节码 后插入代码块 ctMethod.insertAt(10, ""); // 添加参数 ctMethod.addParameter(CtClass); // 设置方法名 ctMethod.setName("newName"); // 设置方法体 $0=this / $1,$2,$3... 代表方法参数 ctMethod.setBody("{$0.name = $1;}"); //创建一个新的方法 ctMethod.make("kawa",CtClass);
2.3.3 异常块 addCatch()
在方法中加入try catch块, 需要注意的是, 必须在插入的代码中, 加入return值$e代表异常信息.插入的代码片段必须以throw或return语句结束
CtMethod m = ...; CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ System.out.println($e); throw $e; }", etype); // 等同于添加如下代码: try { // the original method body } catch (java.io.IOException e) { System.out.println(e); throw e; }
2.4 特殊标识
$0
方法调用的目标对象. 它不等于this, 它代表了调用者. 如果方法是静态的, 则$0为null.
$1, $2 ..
方法的参数 m.insertBefore("{ System.out.println($1); System.out.println($2); }");
$$
是所有方法参数的简写, 主要用在方法调用上. 例如: // 原方法 move(String a,String b) move($$) move($1,$2) // 如果新增一个方法, 方法含有move的所有参数, 则可以这些写: move($$, context) move($1, $2, context)
$args
$args 指的是方法所有参数的数组,类似Object[],如果参数中含有基本类型,则会转成其包装类型。需要注意的时候,$args[0]对应的是$1,而不是$0,$0!=$args[0],$0=this
$cflow
$cflow意思为控制流(control flow),是一个只读的变量,值为一个方法调用的深度。例 //原方法 int fact(int n) { if (n <= 1) return n; else return n * fact(n - 1); } //javassist调用 CtMethod cm = ...; //这里代表使用了cflow cm.useCflow("fact"); //这里用了cflow,说明当深度为0的时候,就是开始当第一次调用fact的方法的时候,打印方法的第一个参数 cm.insertBefore("if ($cflow(fact) == 0)" + " System.out.println(\"fact \" + $1);");
$_ 与 $r
$_是方法调用的结果;$r是返回结果的类型, 用于强制类型转换 Object result = ... ; $_ = ($r)result;
$w
基本类型的包装类 Integer i = ($w)5;
$class
一个 java.lang.Class 对象, 表示当前正在修改的类
$sig
类型为 java.lang.Class 的参数类型数组
$type
一个 java.lang.Class 对象, 表示返回值类型
$proceed
调用表达式中方法的名称
参考博客:https://zhuanlan.zhihu.com/p/349661837
参考文档:https://www.javassist.org/tutorial/tutorial.html