美文网首页Java
干货:使用 Javassist 对字节码操作为 JBoss 实现

干货:使用 Javassist 对字节码操作为 JBoss 实现

作者: 4d11ff5df74e | 来源:发表于2020-05-27 17:24 被阅读0次

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

Javassist的官方网站:http://jboss-javassist.github.io/javassist/

通过javasssit,我们可以:

  • 动态创建新类或新接口的二进制字节码
  • 动态扩展现有类或接口的二进制字节码(AOP)

1、动态创建新类或新接口的二进制字节码

假设我们需要生成一个User类:

package com.tianshouzhi; public class User {
  private String name; 
   public User(String name) {   
 this.name = name; 
   }     
public User() {    }   
  public String getName() {   
     return name;  
  }     
public void setName(String name) {    
    this.name = name;  
  }   
  @Override    public String toString() {    
    return "name="+name;   
 }
}

javassist创建代码如下:

package com.tianshouzhi;
 
import javassist.*;
 
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
 
public class UserGenerator {
    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        //定义User类
        CtClass ctClassUser = classPool.makeClass("com.tianshouzhi.User");
 
        //定义name字段
        CtClass fieldType = classPool.get("java.lang.String");//字段类型
        String name = "name";//字段名称
        CtField ctFieldName=new CtField(fieldType, name,ctClassUser);
        ctFieldName.setModifiers(Modifier.PRIVATE);//设置访问修饰符
        ctClassUser.addField(ctFieldName, CtField.Initializer.constant("javasssit"));//添加name字段,赋值为javassist
 
        //定义构造方法
        CtClass[] parameters = new CtClass[]{classPool.get("java.lang.String")};//构造方法参数
        CtConstructor constructor=new CtConstructor(parameters,ctClassUser);
        String body = "{this.name=$1;}";//方法体 $1表示的第一个参数
        constructor.setBody(body);
        ctClassUser.addConstructor(constructor);
 
        //setName getName方法
        ctClassUser.addMethod(CtNewMethod.setter("setName",ctFieldName));
        ctClassUser.addMethod(CtNewMethod.getter("getName",ctFieldName));
 
        //toString方法
        CtClass returnType = classPool.get("java.lang.String");
        String methodName = "toString";
        CtMethod toStringMethod=new CtMethod(returnType, methodName, null,ctClassUser);
        toStringMethod.setModifiers(Modifier.PUBLIC);
        String methodBody = "{return \"name=\"+$0.name;}";//$0表示的是this
        toStringMethod.setBody(methodBody);
        ctClassUser.addMethod(toStringMethod);
 
        //代表class文件的CtClass创建完成,现在将其转换成class对象
        Class clazz = ctClassUser.toClass();
        Constructor cons = clazz.getConstructor(String.class);
        Object user = cons.newInstance("wangxiaoxiao");
        Method toString = clazz.getMethod("toString");
        System.out.println(toString.invoke(user));
 
        ctClassUser.writeFile(".");//在当前目录下,生成com/tianshouzhi/User.class文件
    }
}

运行程序后输出:

name=wangxiaoxiao

通过反编译工具(例如:JD-gui )打开User.class 可以看到类似以下代码:

image

2. 动态扩展现有类或接口的二进制字节码(AOP)

假设我们现在有如下一个类

package com.tianshouzhi;
 
public class Looper {
    public void loop(){
        try {
            System.out.println("Looper.loop() invoked");
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

现在我们想统计Looper的loop方法的耗时时间。最简单的思路是使用javassist修改loop方法的源码:

在最前加入:long start=System.currentTimeMillis();

在最后插入:System.out.println("耗时:"+(System.currentTimeMillis()-start)+"ms");

如下:

public class Looper {
 
    public void loop(){
 
        //记录方法调用的开始时间
 
        long start=System.currentTimeMillis();
 
        try {
 
            System.out.println("Looper.loop() invoked");
 
            Thread.sleep(1000L);
 
        } catch (InterruptedException e) {
 
            e.printStackTrace();
 
        }
 
        //方法结束时打印耗时
 
        System.out.println("耗时:"+(System.currentTimeMillis()-start)+"ms");
 
    }
 
}

javassist的CtClass方法提供的insertBefore和insertAfter方法,允许我们在一个方法开始和结束添加自己的代码。

public class JavassisTimingWrong {
 
    public static void main(String[] args) throws Exception {
 
        //需要修改的已有的类名和方法名
 
        String className="com.tianshouzhi.javassist.Looper";
 
        String methodName="loop";
 
        ClassPool classPool = ClassPool.getDefault();
 
        CtClass clazz = classPool.get(className);
 
        CtMethod method = clazz.getDeclaredMethod(methodName);
 
        method.insertBefore("long start=System.currentTimeMillis();");
 
        method.insertAfter("System.out.println(\"耗时:\"+(System.currentTimeMillis()-start)+\"ms\");");
 
        //调用修改的Looper类的loop方法
 
        Looper looper = (Looper) clazz.toClass().newInstance();
 
        looper.loop();
 
    }
 
}

此时:

因此运行时,会爆出类似以下的错

image

这是因为,javassist插入的代码片段中,每次插入操作的代码,称之为一个插入代码块,后面的插入块不能使用前面的插入块定义的局部变量,而言且也不能使用方法中的原有局部变量。而上述代码中,我们分表调用了insertBefore和insertAfter插入了两个代码块,而后面的插入块不能使用前面的插入块定义的局部变量start,因此爆出了上面的错。

而如果代码片段都位于一个插入块中,则局部变量是可以引用的。因此考虑使用如下的方法实现:

具体的思路是:将原有的loop方法名改为loopimpl,然后再定义一个loop方法,新的loop方法内部会调用loopimpl,在调用之前和调用之后分别加入上述的代码片段。

实现如下:

package com.tianshouzhi;
 
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
 
public class JavassisTiming {
    public static void main(String[] args) throws Exception{
        //需要修改的已有的类名和方法名
        String className="com.tianshouzhi.Looper";
        String methodName="loop";
 
        //修改为原有类的方法名为loop$impl
        CtClass clazz = ClassPool.getDefault().get(className);
        CtMethod method = clazz.getDeclaredMethod(methodName);
        String newname = methodName + "$impl";
        method.setName(newname);
 
        //使用原始方法名loop,定义一个新方法,在这个方法内部调用loop$impl
        CtMethod newMethod = CtNewMethod.make("public void "+methodName+"(){" +
                        "long start=System.currentTimeMillis();" +
                        ""+newname+"();" +//调用loop$impl
                        "System.out.println(\"耗时:\"+(System.currentTimeMillis()-start)+\"ms\");" +
                        "}"
                , clazz);
        clazz.addMethod(newMethod);
 
        //调用修改的Looper类的loop方法
        Looper looper = (Looper) clazz.toClass().newInstance();
        looper.loop();
    }
}

输出:

Looper.loop() invoked耗时:1000ms

此外还有一种更加简单的方式

package com.tianshouzhi;
 
import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
 
import java.lang.reflect.Method;
 
public class JavassistAop {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        ProxyFactory factory=new ProxyFactory();
        //设置父类,ProxyFactory将会动态生成一个类,继承该父类
        factory.setSuperclass(Looper.class);
        factory.setFilter(new MethodFilter() {
            @Override
            public boolean isHandled(Method m) {
                if(m.getName().equals("loop")){
                    return true;
                }
                return false;
            }
        });
        //设置拦截处理
        factory.setHandler(new MethodHandler() {
            @Override
            public Object invoke(Object self, Method thisMethod, Method proceed,
                                 Object[] args) throws Throwable {
                long start=System.currentTimeMillis();
                Object result = proceed.invoke(self, args);
                System.out.println("耗时:"+(System.currentTimeMillis()-start)+"ms");
                return result;
            }
        });
        Class<?> c=factory.createClass();
        Looper object=(Looper) c.newInstance();
        object.loop();
    }
}

原文链接:https://mp.weixin.qq.com/s/xjoCG8nHlqf871uBfQVO-A

更多java资料,大厂面试题以及视频讲解,点击这里,免费获取!

相关文章

  • 干货:使用 Javassist 对字节码操作为 JBoss 实现

    Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shi...

  • Java字节码修改 - javassist

    AOP的实现一般使用了动态代理和字节码修改,本文介绍使用javassist实现类的创建和修改 添加依赖 使用字节码...

  • 四、Java探针技术

    对于agent,是在vm启动,执行方法前,将字节码修改的服务代理。 对于javassist,是修改字节码具体实现。...

  • javassist文档翻译(一)

    1、读和写字节码 Javassist是一个处理Java字节码的库,java字节码是使用二进制格式存储在文件中的话,...

  • JavaAgent 与 动态代理

    ASM, CGlib, Java Proxy, Javassist都是可以操作字节码,但是这些操作字节码都需要等到...

  • Javassist指引(一)

    原文链接 [TOC] 1. 读写字节码 1.1概述 Javassist是一个Java字节码类库。Java的字节码是...

  • android 热修复之类加载机制

    文章目标 Android类加载机制介绍 javassist动态修改字节码 实现热补丁动态修复 Android类加载...

  • Javassist 指南1

    1、读写字节码 Javassist 是一个能处理 Java字节码 的类库,Java字节码存储在class文件中,每...

  • Javassist 使用指南(三)

    本文翻译自 Javassist Tutorial-3 5. 字节码操作 Javassist 还提供了用于直接编辑类...

  • Java Javassist/ASM 框架比较

    Javassist: Javassist (Java编程助手)使操作Java字节码变得简单。它是一个用于编辑Jav...

网友评论

    本文标题:干货:使用 Javassist 对字节码操作为 JBoss 实现

    本文链接:https://www.haomeiwen.com/subject/aafgahtx.html