javassist字节码增强
javassist是一种能够在不影响正常编译的情况下,修改字节码。java作为一种强类型的语言,不通过编译就不能够进行jar包的生成。而有了javaagent技术,就可以在字节码这个层面对类和方法进行修改。同时,也可以把javaagent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。
public class Agent implements ClassFileTransformer { public final String injectedClassName = "com.haiziwang.App"; public final String methodName = "hello"; public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/", "."); if (className.equals(injectedClassName)) { CtClass ctclass = null; ClassPool classPool = ClassPool.getDefault(); try { //加入当前线程的上下文类加载器作为额外的类搜索路径 classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader())); ctclass = classPool.get(className);// 使用全称,用于取得字节码类<使用javassist> CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 得到这方法实例 ctmethod.insertBefore("System.out.println(\"hello方法之前拦截...\");"); return ctclass.toBytecode(); } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); } } return null; } }
创建拦截器的主类,必须有premain方法:
public class AgentMain { public static void premain(String agentOps, Instrumentation inst) { System.out.println("=========premain方法执行========"); // 添加Transformer inst.addTransformer(new Agent()); } }
修改pom.xml文件,设定premain-class, 这一步非常关键。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.platform</groupId> <artifactId>kidstrack</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>kidstrack</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.22.0-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>utf-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Premain-Class>com.haiziwang.AgentMain</Premain-Class> </manifestEntries> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
最后,我们创建DEMO测试类
public class App { public static void main(String[] args) { hello(); } public static void hello() { System.out.println("hello方法执行"); } }
最后一步,我们设定JVM启动参数即可 -javaagent:d:xxx.jar
OK,至次我们就实现对hello方法的增强
=========premain方法执行========
hello方法之前拦截...
hello方法执行
可以发现利用javassist框架进行动态编程是比较轻松简单的,来看几个比较重要的API
一. API相关
最近使用Javassist框架开发了一些功能,使用的过程中碰见了不少问题,将使用方法总结下,以防日后重复踩坑。
先来看一段代码:
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
CtClass ctClass = classPool.getCtClass("cn.com.Test");
CtMethod ctMethod = CtNewMethod.make("public void helloWorld(){ System.out.println(\"hello world!\"); }", ctClass);
ctClass.addMethod(ctMethod);
ctClass.toClass();
这段代码的功能很简单,在创建了一个默认的classpool后,加入当前线程的上下文类加载器作为额外的类搜索路径,获取Test类后向其中加入了helloWorld这个方法,并把修改后的类加载至当前线程所在的上下文类加载器中。
可以发现利用javassist框架进行动态编程是比较轻松简单的,来看几个比较重要的API:
ClassPool
ClassPool是CtClass对象的容器,每一个CtClass对象都必须从ClassPool中获取。
ClassPool自身可以形成层级结构,其工作机制与java的类加载器类似,只有当父节点找不到类文件时,才会调用子节点的get()方法。通过设置 ClassPath.childFirstLookup 属性可以调整其工作流程。
需要注意的是ClassPool会在内存中维护所有被它创建过的CtClass,当CtClass数量过多时,会占用大量的内存,API中给出的解决方案是
周期性的调用compress方法或 重新创建ClassPool 或 有意识的调用CtClass的detach()方法以释放内存。需要关注的方法:
1. getDefault : 返回默认的ClassPool,单例模式!一般通过该方法创建我们的ClassPool。
2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬。
3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class。
4. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
ClassPath
ClassPath是一个接口,代表类的搜索路径,含有具体的搜索实现。当通过其它途径无法获取要编辑的类时,可以尝试定制一个自己的ClassPath。API提供的实现中值得关注的有:
1. ByteArrayClassPath : 将类以字节码的形式加入到该path中,ClassPool 可以从该path中生成所需的CtClass。
2. ClassClassPath : 通过某个class生成的path,通过该class的classloader来尝试加载指定的类文件。
3. LoaderClassPath : 通过某个classloader生成path,并通过该classloader搜索加载指定的类文件。需要注意的是该类加载器以弱引用的方式存在于path中,当不存在强引用时,随时可能会被清理。
CtClass
javassist为每个需要编辑的class都创建了一个CtClass对象,通过对CtClass对象的操作来实现对class的编辑工作。
该类方法较多,此处列出需要重点关注的方法:
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对象可以实现对方法的修改。
需要注意的是写入方法体的代码无法访问在其它地方定义的成员变量,一些比较重要的方法:
1. insertBefore : 在方法的起始位置插入代码。
2. insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception。
3. insertAt : 在指定的位置插入代码。
4. setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除。
5. make : 创建一个新的方法。
CtNewMethod
提供各种静态方法来操作CtMethod,不进行详细描述,有兴趣可以看下API。
特殊符号
$0, $1, $2, ... this and actual parameters $args An array of parameters. The type of $args is Object[]. $$ All actual parameters.For example, m($$) is equivalent to m($1,$2,...) $cflow(...) cflow variable $r The result type. It is used in a cast expression. $w The wrapper type. It is used in a cast expression. $_ The resulting value $sig An array of java.lang.Class objects representing the formal parameter types $type A java.lang.Class object representing the formal result type. $class A java.lang.Class object representing the class currently edited.
二. 使用场景总结
1. 实现代码插入功能:
CtClass ctClass = classPool.getCtClass("com.netease.HelloWorld");
CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
ctMethod.insertAfter("System.out.println(\"Hello world!\");");
ctClass.toClass();
2. 创建一个完整的类:
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("com.netease.Class");
CtField ctField = new CtField(classPool.get("java.lang.String"), "teacher", ctClass);
ctField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField);
ctClass.addMethod(CtNewMethod.setter("setTeacher", ctField));
ctClass.addMethod(CtNewMethod.getter("getTeacher", ctField));
ctClass.writeFile();
3. 实现拦截器功能:
CtMethod ctMethod = clazz.getDeclaredMethod(method);
String newName = method + "New";
ctMethod.setName(newName);
CtMethod newCtMethod = CtNewMethod.copy(ctMethod, method, clazz, null);
String type = ctMethod.getReturnType().getName();
StringBuilder body = new StringBuilder();
body.append("{\n System.out.println(\"Before Method Execute...\");\n");
if(!"void".equals(type)) {
body.append(type).append(" result = ");
}
body.append(newName).append("($$);\n");
body.append("System.out.println(\"After Method Execute...\");;\n");
if(!"void".equals(type)) {
body.append("return result;\n");
}
body.append("}");
newCtMethod.setBody(body.toString());
clazz.addMethod(newCtMethod);
上一篇: pinpoint字节码增强技术原理
下一篇: 字节码增强技术-Byte Buddy