vlambda博客
学习文章列表

Javassist | 字节码增强技术

Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.


一. Javassist的重要组成


Javassist中最为重要的是ClassPool, CtClass, CtMethod以及CtField这几个类.

ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass 对象.
CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得.
CtMethods: 表示类中的方法.
CtFields: 表示类中的字段.


二. Hello world


2.1

原始类

例如, 我们有如下类:

package com.in.aop.javassist;public class TestObject { public static String getValue(String key) { return key; }}

2.2

增强

如何在不修改代码的前提下, 增加一段简单逻辑: 在返回结果前, 将参数打印出来?

public static void main(String[] args) throws NotFoundException, CannotCompileException { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.in.aop.javassist.TestObject"); CtMethod m = cc.getDeclaredMethod("getValue", new CtClass[]{pool.get(String.class.getName())}); m.insertBefore("System.out.println(\"hello:\"+ $1 );"); cc.toClass(); System.out.println(TestObject.getValue("world"));}

2.3

执行结果

hello:worldworld

2.4

pom依赖

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

2.5

过程简析

1. 先在池中, 找到我们要黑掉的类和方法.
2. 在方法体中插入增强语句
整个流程虽然非常简单, 但合理利用javassist API会完成更多功能.


三. 常用API


3.1

ClassPool对象

// 获取ClassPool对象, 使用系统默认类路径ClassPool pool = new ClassPool(true);// 效果与 new ClassPool(true) 一致ClassPool pool1 = ClassPool.getDefault();

3.2

类对象

3.2.1

获取类对象

// 通过类名获取 CtClass, 未找到会抛出异常CtClass ctClass = pool.get("test.DemoService");// 通过类名获取 CtClass, 未找到返回 null, 不会抛出异常CtClass ctClass1 = pool.getOrNull("test.DemoService");

3.2.2

创建新类

// 复制一个类CtClass ctClass2 = pool.getAndRename("test.DemoService", "test.CopyService");// 创建一个新类CtClass ctClass3 = pool.makeClass("test.NewDemoService");// 通过class文件创建一个新类CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("./xxx/Demo.class")));

3.3

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()));// 将一个目录作为classpathpool.insertClassPath("/xxx/lib");

3.4

内存回收

为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.

// 从ClassPool中删除CtClass对象ctClass.detach();// 也可以每次创建一个新的ClassPool, 而不是ClassPool.getDefault(), 避免内存溢出ClassPool pool2 = new ClassPool(true);

3.5

类信息

3.5.1

基础信息

// 类名String simpleName = ctClass.getSimpleName();// 类全名String name = ctClass.getName();// 包名String packageName = ctClass.getPackageName();// 接口CtClass[] interfaces = ctClass.getInterfaces();// 继承类CtClass superclass = ctClass.getSuperclass();// 获取类方法CtMethod ctMethod = ctClass.getDeclaredMethod("getXxx()", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});// 获取类字段CtField ctField = ctClass.getField("xxx");// 判断数组类型ctClass.isArray();// 判断原生类型ctClass.isPrimitive();// 判断接口类型ctClass.isInterface();// 判断枚举类型ctClass.isEnum();// 判断注解类型ctClass.isAnnotation();

3.5.2

类操作

// 添加接口ctClass.addInterface(...);// 添加构造器ctClass.addConstructor(...);// 添加字段ctClass.addField(...);// 添加方法ctClass.addMethod(...);

3.5.3

类编译

// 获取字节码文件Class clazz = ctClass.toClass();// 类的字节码文件ClassFile classFile = ctClass.getClassFile();// 编译成字节码文件, 使用当前线程上下文类加载器加载类, 如果类已存在或者编译失败将抛出异常byte[] bytes = ctClass.toBytecode();


3.6

方法信息

3.6.1

获取方法属性

CtClass ctClass5 = pool.get(TestService.class.getName());CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");// 方法名String methodName = ctMethod.getName();// 返回类型CtClass returnType = ctMethod.getReturnType();// 方法参数, 通过此种方式得到方法参数列表// 格式: test.TestService.getXX(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;// 非静态的成员函数的第一个参数是thisint pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;for (int i = pos; i < len; i++) { argKeys.add(attr.variableName(i));}

3.6.2

方法体修改

// 在方法体前插入代码块ctMethod.insertBefore("");// 在方法体后插入代码块ctMethod.insertAfter("");// 在某行 字节码 后插入代码块ctMethod.insertAt(10, "");// 添加参数ctMethod.addParameter(CtClass);// 设置方法名ctMethod.setName("newName");// 设置方法体ctMethod.setBody("");

3.6.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;}

3.7

特殊标识

$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)

$_ 与 $r
$_是方法调用的结果;$r是返回结果的类型, 用于强制类型转换
Object result = ... ;
$_ = ($r)result;

$w
基本类型的包装类
Integer i = ($w)5;

$class
一个 java.lang.Class 对象, 表示当前正在修改的类
$sig
类型为 java.lang.Class 的参数类型数组
$type
一个 java.lang.Class 对象, 表示返回值类型
$class
一个 java.lang.Class 对象, 表示当前正在修改的类
$proceed
调用表达式中方法的名称


小结


Javassist虽然性能上比ASM,bcel较差, 但不需要了解字节码命令, 对开发者要求也不那么高, 开发效率也很高.
同时, 配合javaAgent和instrument技术, 动态插入代码, 无侵入实现埋点, 获取方法执行时间, 热部署以及流量回放等功能.

 推 / 荐 / 阅/ 读