在JAVA 语言中 我们知道最终JVM执行的是字节码文件,那么 改变字节码指令 其实就是修改了代码执行逻辑.
今天我们就来介绍下 java中操作字节码的工具 javasst
一种简单易用操作字节码的工具类 —— [ 官方网站 ]
public class Student {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString(){
return "student";
}
}
通过javap 查看下 生成的字节码
javap -v Student
public java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #31 // String student
2: areturn
LineNumberTable:
line 26: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lsun/cn/demo/Student;
通过javassist api修改toString 方法
public class StudentByteCode {
public void modifyByteCodeMethod() throws Exception {
ClassPool pool = ClassPool.getDefault();
/** 获取类信息 */
CtClass cls = pool.get(Student.class.getCanonicalName());
/** 获取方法描述信息 */
CtMethod toStringMethod = cls.getDeclaredMethod("toString");
toStringMethod.insertBefore("System.out.println(\"toString before\");");
toStringMethod.insertAfter("System.out.println(\"toString after\");");
cls.writeFile(Student.class.getResource("../../../").getPath());
}
public static void main(String[] args) throws Exception {
new StudentByteCode().modifyByteCodeMethod();
Student s = new Student();
s.toString();
}
}
在来看下 生成的字节码
public java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=4, locals=3, args_size=1
0: getstatic #40 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #42 // String toString before
5: invokevirtual #47 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: ldc #31 // String student
10: goto 13
13: astore_2
14: getstatic #40 // Field java/lang/System.out:Ljava/io/PrintStream;
17: ldc #49 // String toString after
19: invokevirtual #47 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
22: aload_2
23: areturn
LineNumberTable:
line 26: 8
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lsun/cn/demo/Student;
StackMapTable: number_of_entries = 1
frame_type = 77 /* same_locals_1_stack_item */
stack = [ class java/lang/String ]
可以明显看到 字节码已经变更了我们在执行对象的toString方法
Student s = (Student)Student.class.getClassLoader().loadClass(Student.class.getCanonicalName()).newInstance();
System.out.println(s.toString());
toString before
toString after
student
但是 使用如下 方式运行 就得不到如上效果:
public class StudentByteCode {
public void modifyByteCodeMethod() throws Exception {
ClassPool pool = ClassPool.getDefault();
/** 获取类信息 */
CtClass cls = pool.get(Student.class.getCanonicalName());
/** 获取方法描述信息 */
CtMethod toStringMethod = cls.getDeclaredMethod("toString");
toStringMethod.insertBefore("System.out.println(\"toString before\");");
toStringMethod.insertAfter("System.out.println(\"toString after\");");
cls.writeFile(Student.class.getResource("../../../").getPath());
}
public static void main(String[] args) throws Exception {
Student s1 = (Student)Student.class.getClassLoader().loadClass(Student.class.getCanonicalName()).newInstance();
System.out.println(s1.toString());
new StudentByteCode().modifyByteCodeMethod();
Student s = (Student)Student.class.getClassLoader().loadClass(Student.class.getCanonicalName()).newInstance();
System.out.println(s.toString());
}
}
student
student
这是为什么呢?
因为 我们的Student 类描述信息是通过 classloader 加载的.而class改变了并不会使classloader 重新加载.
当然 JVM 有类卸载参数 比如 CMS收集器
-XX:+CMSClassUnloadingEnabled
卸载是JVM自身机制 不受外部因素影响(比如字节码文件的改动)
所以这里 通过classloader拿到的改动之前的字节码信息.
那如何实现字节码修改 在classloader里面变更呢?
jvm 提供了一套api 叫 JVMTI 是 jvm提供的一套native api
开发的时候 我们不用关心JVMTI 做了什么 只需要在java 项目启动的时候 添加
-javaagent 即可.
我们利用 这个特性来实现无侵入的对方法添加耗时统计
public class TimeCount {
/**
* 实现javaagent 必须添加一个叫premain 的方法
* (在1.6之后 也可以添加 一个叫 agentmain 具体参见官网)
*/
public static void premain(String agentOps, Instrumentation inst) {
// 添加Transformer
inst.addTransformer(new TimeCountAgent());
}
}
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
public class TimeCountAgent implements ClassFileTransformer {
/**
* 方法返回一个字节数组,其实就是修改后的字节码.
* 然后就会替换掉classloader中的类描述信息
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String newClassName = className.replace("/", ".");
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = null;
try{
/** 类信息是否处于冻结状态 */
if(ctClass.isFrozen()){
return null;/** 不需要转换 */
}
CtMethod[] methods = ctClass.getDeclaredMethods();
byte[] result = null;
if(methods != null && methods.length > 0){
for(CtMethod method : methods){
String name = method.getName();
/** 2种实现 */
//addTimeCountMethod2(method);
addTimeCountMethod(ctClass,name,method);
}
return ctClass.toBytecode();
}
}catch(Exception e){
e.printStackTrace();
}
return null;
}
/**
* 修改方法体
*/
private void addTimeCountMethod2(CtMethod method) throws Exception {
method.addLocalVariable("start", CtClass.longType);
method.insertBefore("start = System.currentTimeMillis();");
method.insertAfter("System.out.println(\"exec time is :\" + (System.currentTimeMillis() - start) + \"ms\");");
}
/**
* 新增方法 的实现
*/
private byte[] addTimeCountMethod(CtClass cc,String oldName,CtMethod method) throws Exception{
if(cc.isFrozen()){
return null;/** 不需要转换 */
}
String newName = oldName + "tmp";
/** 重命名老方法 */
method.setName(newName);
/** 新建一个 方法 名字 跟老方法一样 */
CtMethod newMethod = CtNewMethod.copy(method, oldName, cc, null);
String type = method.getReturnType().getName();
StringBuilder body = new StringBuilder();
/** 新方法增加计时器 */
body.append("{\n long start = System.currentTimeMillis();\n");
/** 判断老方法返回类型 */
if(!"void".equals(type)){
/** 类似 Object result = newName(args0,args1); */
body.append(type + " result = ");
}
/** 执行老方法 */
body.append( newName + "($$);\n");
/** 统计执行完成时间 */
body.append("System.out.println(\"Call to method " + oldName +
" took \" +\n (System.currentTimeMillis()-start) + " +
"\" ms.\");\n");
if(!"void".equals(type)){
/** 类似 Object result = newName(args0,args1); */
body.append("return result;\n");
}
body.append("}");
newMethod.setBody(body.toString());
cc.addMethod(newMethod);
return cc.toBytecode();
}
}
pom.xml 添加
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<version>3.0.0version>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>sun.cn.agent.TimeCountPremain-Class>
manifestEntries>
transformer>
transformers>
configuration>
execution>
executions>
plugin>
最后生成的jar 里面有个
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: sun.cn.agent.TimeCount
Archiver-Version: Plexus Archiver
在java 项目启动的时候 添加 javaagent 参数
java -server -javaagent:timeCount.jar XXX
执行结果如下:
=====java.io.FileOutputStream$1=====
Call to method close took 5 ms.
Call to method run took 399 ms.
Call to method run took 466 ms.