Java中的高性能字节码工具:Javassist

571 篇文章 3 订阅
571 篇文章 6 订阅

前言

一般常见的动态方法调用使用Reflection或者字节码生成技术。虽然JDK已对反射进行了优化但在追求性能的场景中仍然显得性能不佳。本文即是介绍一个面向程序员友好的字节码操作类库javassist。根据benchmark其展现的性能已几乎无异于直接调用。

开源地址:javassist,简单地看一下官方介绍:

Javassist 使 Java 字节码操作变得简单。它是一个用于在 Java 中编辑字节码的类库。它使 Java 程序可以在运行时定义新类,并在 JVM 加载它时修改类文件。与其他类似的字节码编辑器不同,Javassist 提供了两个级别的 API:源级别和字节代码级别。如果用户使用源代码级 API,则他们可以在不了解 Java 字节码规范的情况下编辑类文件。整个 API 仅使用 Java 语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码。Javassist 可以即时对其进行编译。另一方面,字节码级别的 API 允许用户像其他编辑器一样直接编辑类文件。

说得直白一点就是,我们可以通过它的API来生成我们想要的字节码。下面演示如何进行使用。

正文

生成

首先需要引入Jar包,仓库地址为:

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

为了演示,我们直接创建一个main函数,用来构建一个不存在的对象。

public class JavassistInvoker {


    public static void main(String[] args) throws Exception {
        //创建类,这是一个单例对象
        ClassPool pool = ClassPool.getDefault();
        //我们需要构建的类
        CtClass ctClass = pool.makeClass("io.github.pleuvoir.prpc.invoker.Person");

        //新增字段
        CtField field$name = new CtField(pool.get("java.lang.String"), "name", ctClass);
        //设置访问级别
        field$name.setModifiers(Modifier.PRIVATE);
        //也可以给个初始值
        ctClass.addField(field$name, CtField.Initializer.constant("pleuvoir"));
        //生成get/set方法
        ctClass.addMethod(CtNewMethod.setter("setName", field$name));
        ctClass.addMethod(CtNewMethod.getter("getName", field$name));

        //新增构造函数
        //无参构造函数
        CtConstructor cons$noParams = new CtConstructor(new CtClass[]{}, ctClass);
        cons$noParams.setBody("{name = \"pleuvoir\";}");
        ctClass.addConstructor(cons$noParams);
        //有参构造函数
        CtConstructor cons$oneParams = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);
        // $0=this  $1,$2,$3... 代表方法参数
        cons$oneParams.setBody("{$0.name = $1;}");
        ctClass.addConstructor(cons$oneParams);

        // 创建一个名为 print 的方法,无参数,无返回值,输出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "print", new CtClass[]{}, ctClass);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        ctClass.addMethod(ctMethod);

        //当前工程的target目录
        final String targetClassPath = Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath();

        //生成.class文件
        ctClass.writeFile(targetClassPath);
    }
}

可以看到生成的文件如下:

需要注意的是,如果我们大量地使用该方法生成类,可能会造成内存压力。因为ClassPool中存在哈希表用来缓存生成的CtClass对象。我们可以通过调用CtClass#detach()方法清除缓存。 

到此为止,我们的文件是生成了。需要如何调用呢?难道是通过ClassLoader加载吗?接着往下看。

调用

反射调用

有两种常用的方式,调用toClass()方法或者读取.class文件。

修改如上的方法:

这里可以看到已经获得了我们期待的对象,但是由于我们编译器并无此对象所以不能完成强制转换。也就是说还得通过反射来调用。

也可以读取文件进行加载:

//如果生成的类没有放在classpath下需要自己指定类加载器加载的位置,否则加载不到。这里我们不需要设置
//  pool.appendClassPath(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());
final CtClass loadCtClass = pool.get("io.github.pleuvoir.prpc.invoker.Person");
loadCtClass.toClass().newInstance();

注意:toClass不要重复调用。

以上两种形式都是通过反射调用,我们又不存在这样的实体。那么怎么样才能完成转换呢?聪明的小伙伴肯定想到了,那就是定义接口。生成后转换为接口就可以直接调用了。

OK,话不多说。我们先定义一个接口正好来演示一下使用javassist实现动态代理。

接口调用

在实现动态代理前,我们先来回顾一组静态代理。

我们先来定义目标接口,用来被代理。等会这也是我们程序强制转换的接口。

public interface HelloService {

	String sayHello(String name);
}

定义代理实现,传入接口实现类。

public class HelloServiceProxy implements  HelloService {

	private HelloService helloService;

	public HelloServiceProxy() {
	}

	public HelloServiceProxy(HelloService helloService) {
		this.helloService = helloService;
	}


	// 这里会做修改
	@Override
	public String sayHello(String name) {
		System.out.println("静态代理前 ..");
		helloService.sayHello(name);
		System.out.println("静态代理后 ..");
		return name;
	}

}

很简单,我们来测试一下:

public class ProxyTest {


    public static void main(String[] args) {

        final HelloServiceProxy serviceProxy = new HelloServiceProxy(new HelloService() {
            @Override
            public String sayHello(String name) {
                System.out.println("目标接口实现:name=" + name);
                return "null";
            }
        });

        serviceProxy.sayHello("pleuvoir");
    }
}

输出的结果是:

静态代理前 ..
目标接口实现:name=pleuvoir
静态代理后 ..

不错,符合我们的预期。接下来我们就要开始生成真正的动态代理了。

先来思考一下,我们的静态代理是使用有参构造函数传入真正的实现类。如果我们动态生成的类去调用构造函数还是必须使用反射,所以我们增加一个新接口用来设置实现类。如下:

public interface IProxy {

    void setProxy(Object t);
}

我们期望生成的类是这样的:

public class HelloSericeProxyV2 implements IProxy, HelloService {

    private HelloService helloService;

    public HelloSericeProxyV2() {
    }

    @Override
    public void setProxy(Object t) {
        this.helloService = (HelloService) t;
    }

    // 这里会做修改
    @Override
    public String sayHello(String name) {
        System.out.println("静态代理前 ..");
        helloService.sayHello(name);
        System.out.println("静态代理后 ..");
        return name;
    }

}

通过调用setProxy设置实现类,然后调用目标方法sayHello。获取到对象后可以分别转换为这两个接口进行方法调用。可能有的朋友会问,为什么void setProxy(Object t)不使用泛型。原因是对其支持不是很好,设置起来有点麻烦,所以就没设置。看一下具体的实现:

public class ProxyTest {


    public static void main(String[] args) throws Exception {

//        final HelloServiceProxy serviceProxy = new HelloServiceProxy(new HelloService() {
//            @Override
//            public String sayHello(String name) {
//                System.out.println("目标接口实现:name=" + name);
//                return "null";
//            }
//        });
//
//        serviceProxy.sayHello("pleuvoir");

        //创建类,这是一个单例对象
        ClassPool pool = ClassPool.getDefault();
        pool.appendClassPath(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());
        //我们需要构建的类
        CtClass ctClass = pool.makeClass("io.github.pleuvoir.prpc.invoker.HelloServiceJavassistProxy");
        //这个类实现了哪些接口

        ctClass.setInterfaces(new CtClass[]{
                pool.getCtClass("io.github.pleuvoir.prpc.invoker.HelloService"),
                pool.getCtClass("io.github.pleuvoir.prpc.invoker.IProxy")});

        //新增字段
        CtField field$name = new CtField(pool.get("io.github.pleuvoir.prpc.invoker.HelloService"), "helloService", ctClass);
        //设置访问级别
        field$name.setModifiers(Modifier.PRIVATE);
        ctClass.addField(field$name);

        //新增构造函数
        //无参构造函数
        CtConstructor cons$noParams = new CtConstructor(new CtClass[]{}, ctClass);
        cons$noParams.setBody("{}");
        ctClass.addConstructor(cons$noParams);

        //重写sayHello方方法,可以通过构造字符串的形式
        CtMethod m = CtNewMethod.make(buildSayHello(), ctClass);
        ctClass.addMethod(m);


        // 创建一个名为 setProxy 的方法
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "setProxy",
                new CtClass[]{pool.getCtClass("java.lang.Object")}, ctClass);
        ctMethod.setModifiers(Modifier.PUBLIC);
        // // $0=this  $1,$2,$3... 代表方法参数
        ctMethod.setBody("{$0.helloService =   $1;}");
        ctClass.addMethod(ctMethod);

        ctClass.writeFile(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());

        //获取实例对象
        final Object instance = ctClass.toClass().newInstance();

        System.out.println(Arrays.toString(instance.getClass().getDeclaredMethods()));
        //设置目标方法
        if (instance instanceof IProxy) {
            IProxy proxy = (IProxy) instance;
            proxy.setProxy(new HelloService() {
                @Override
                public String sayHello(String name) {
                    System.out.println("目标接口实现:name=" + name);
                    return "null";
                }
            });
        }

        if (instance instanceof HelloService) {
            HelloService service = (HelloService) instance;
            service.sayHello("pleuvoir");
        }

    }

    private static String buildSayHello() {
        String methodString = "   public String sayHello(String name) {\n"
                + "        System.out.println(\"静态代理前 ..\");\n"
                + "        helloService.sayHello(name);\n"
                + "        System.out.println(\"静态代理后 ..\");\n"
                + "        return name;\n"
                + "    }";
        return methodString;
    }
}

 

生成的.class和我们预期的一样。

参考

  • javassist tutorial
  • javassist 使用全解析

后语

OK,我们看到了javassist在生成代码上还是很方便高效的。注意事项是尽量不要使用泛型比较麻烦,另外如果生成的字节码会被其它对象缓存可以选择new ClassPool(true)的方式进行创建,而不是使用ClassPool.getDefault()单例对象。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值