Java字节码增强应用(一):动态代理

原创
2019/12/19 10:47
阅读数 420

在上一篇文章 Java字节码结构 里,我们了解了字节码文件的结构,其中提到方法代码指令区是我们进行字节码增强的关键地方。从这篇文章开始,我将继续介绍一些字节码增强的技术,有些是我们比较熟悉的比如动态代理,有些是在我们日常业务开发中较少接触到的,比如 Javaagent 技术,APM等。本文主要介绍动态代理的原理以及各类框架 AOP 的实现原理。

动态代理属于代理模式的一种实现方式,代理模式就是,我们需要在一个现有类的方法增加一些功能,我们可以通过代理模式来实现。代理模式可以分为两类:

  • 静态代理
  • 动态代理

静态代理

静态代理是程序员通过编程的方式扩展原有类的功能。大致方式为,编写一个代理类,和目标类实现同样的接口,并持有目标类的实例,在代理方法里先执行需要扩展的逻辑,然后再通过目标类的实例调用目标方法。 如我们有一个 Person 接口,其中有方法 run(),有个实现类 Tom,实现了具体的 run() 方法,现在我们需要在 Tom 类的 run() 方法前后增加一些逻辑,我们可以通过新建一个类 PersonProxy 类,实现 Person 接口,并在构造方法里传入具体要代理的目标类,然后在 run 方法里执行增加的逻辑,并调用目标类的 run 方法。具体类图如下:

测试代码如下:

public static void main(String[] args) {
        Person tom = new PersonProxy(new Tom());
        tom.run();
    }

静态代理的优点是,逻辑简单,不破坏原有类的结构,符合开闭原则,缺点是,要对每个目标类新建代理类,工作量大,而且造成类泛滥。

动态代理

动态代理是我们编写好处理逻辑,然后在运行时让 JDK 或其他三方工具为我们生成具体代理类。怎么生成呢? JDK 提供了动态代理的方法,但是要求目标类必须实现一个接口,如果不满足这个要求,就无法使用 JDK 动态代理。正是由于上面的限制,产生了第三方库通过一些其他手段来为什么生成动态代理类,这其中最著名的要数 CGLib 了。

JDK 动态代理

首先,我们有一个 UserService 接口,和其中一个实现类 UserServiceImpl,代码分别如下:

public interface UserService {
    String get(Integer id);
}

public class UserServiceImpl implements UserService {
    @Override
    public String get(Integer id) {
        System.out.println("我是Tom");
        return "Tom";
    }
}

通过 JDK 动态代理扩展 UserServiceImplget 方法的代码如下:

public static void main(String[] args) {
        UserService userService = new UserServiceImpl();

        UserService proxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),
                userService.getClass().getInterfaces(), new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("方法执行前 @ "+ LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
                        Object o = method.invoke(userService, args);
                        System.out.println("方法执行后 @ "+ LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
                        return o;
                    }
                });
        proxy.get(1);
    }

我们通过 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 这个方法来说生成代理类,并将结果强制转为 UserService 类型。 该方法第三个参数类型是 InvocationHandler 我们需要实现该接口的 invoke 方法,增加我们的逻辑,并通过反射调用目标类的方法。 Proxy 生成代理类的具体流程如下:

我们看到最终是通过 ProxyGenerator.generateClassFile 方法生成了 class 文件,ProxyGenerator 这个类在 Oracle JDK 是不开源的,我们可以通过 openJDK 的代码 查看具体生成 class 文件的逻辑。下面是摘取的 JDK 里生成 class 文件的代码:

        try {
            /*
             * Write all the items of the "ClassFile" structure.
             * See JVMS section 4.1.
             */
                                        // u4 magic;
            dout.writeInt(0xCAFEBABE);
                                        // u2 minor_version;
            dout.writeShort(CLASSFILE_MINOR_VERSION);
                                        // u2 major_version;
            dout.writeShort(CLASSFILE_MAJOR_VERSION);
            cp.write(dout);             // (write constant pool)
                                        // u2 access_flags;
            dout.writeShort(accessFlags);
                                        // u2 this_class;
            dout.writeShort(cp.getClass(dotToSlash(className)));
                                        // u2 super_class;
            dout.writeShort(cp.getClass(superclassName));
                                        // u2 interfaces_count;
            dout.writeShort(interfaces.length);
                                        // u2 interfaces[interfaces_count];
            for (Class<?> intf : interfaces) {
                dout.writeShort(cp.getClass(
                    dotToSlash(intf.getName())));
            }
                                        // u2 fields_count;
            dout.writeShort(fields.size());
                                        // field_info fields[fields_count];
            for (FieldInfo f : fields) {
                f.write(dout);
            }

上面的代码就是根据字节码文件规范生成字节码文件的逻辑,比如首先写入魔数,然后写入次版本号,再写入主版本号等等。

那么为什么 JDK 动态代理要求目标类必须实现至少一个接口呢?我们可以通过查看生成的代理类来了解其背后的本质。 可以通过在上面的 main 测试方法里加上下面一行代码来将生成的字节码文件保存到硬盘里:

System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

生成的文件包名为 com.sun.proxy 包下,类名为 $Proxy0 其中 0 为已生成代理类的个数

通过查看生成的代理类,可以看出代理类继承 Proxy 类来得到要增加的逻辑的 InvocationHandler 实例,实现 UserService 接口的 get 方法,并在实现方法里调用 InvocationHandler.invoke 方法来进入到代理处理类的方法里。

因为 Java 类是单继承的,而代理类必须要继承 Proxy ,要想和目标类保持同样功能,就必须要求目标类实现至少一个接口,代理类也实现同样的接口。

CGLib 动态代理

首先,我们写一个普通类如下:

public class CarService {
    public String getCar(Integer id) {
        System.out.println("调用 getCar() 方法");
        return "Car - " + id;
    }
}

并编写如下代理增强逻辑,该类需要实现 MethodInterceptor 接口:

public class CGLibProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("方法执行前 @ "+ LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
        Object object = methodProxy.invokeSuper(o, args);
        System.out.println("方法执行后 @ "+ LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
        return object;
    }
}

然后写下面的测试方法(注意需要引入 cglib.jar 和 asm.jar):

public static void main(String[] args) {
        //System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\cglib");
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(CarService.class);
        enhancer.setCallback(new CGLibProxy());
        CarService proxy = (CarService) enhancer.create();

        proxy.getCar(1);
    }

上面的代码,使用 cglib.jar 的 Enhancer 类来生成代理类,其中需要声明代理类的父类为 CarService.class ,声明 callback 为我们增强逻辑的代码 CGLibProxy,最后使用 enhancer.create() 方法生成代理类。cglib 生成代理类的逻辑比较复杂,经过分析,大致执行过程如下:

其中 Enhancer.generateClass() 方法生成字节码文件,之后会调用父类 AbstractClassGenerator.generate() 方法加载生成的字节码文件,继续往后看,其实最终是调用的 Class.forName() 方法来加载生成的类。

我们可以通过在生成代理类前设置环境变量,让 cglib 生成的类保存到硬盘里:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\cglib");

加上上面一行代码后,生成的字节码文件会保存到 D:/cglib 目录下。

最主要的是第二个文件:CarService$$EnhancerByCGLIB$$88f5487e.class,这个就是生成的代理类,我们在 IDEA 里打开反编译该文件,可以清晰的看出来生成的代理类继承自目标类,并重写了目标类的方法,并调用我们自定义 MethodInterceptor 接口实现类的 intercept 方法,从而能执行相关的增强逻辑。因为代理类继承目标类,所以要求目标类不能被 final 修饰,且被 final 修饰的方法不会被代理。

我们熟悉的 Spring AOP 的原理就是动态代理,它会根据目标类是否有实现接口来决定使用 JDK 动态代理或 CGlib 来生成代理类的字节码文件,本质就是动态生成代理类字节码文件,在目标方法执行前后插入要增强的逻辑代码,而在 Spring 中,会将代理类也托管到 IOC 容器管理。

总结

  1. 动态代理的本质就是动态生成字节码文件,并手动加载到 JVM 。
  2. 动态代理是在运行期使用字节码生成技术动态生成代理类的字节码文件的,然后被类加载器加载到 JVM 里。
  3. JDK 动态代理要求目标类必须实现接口,因为生成的代理类要继承 Proxy 类来获得增强方法的类的处理,所以代理类必须要实现和目标类同样的接口。
  4. CGLib 动态代理不要求目标类是否实现接口,是因为 CGLib 生成的代理类继承了目标类。
  5. Spring AOP 的本质就是动态代理,使用 JDK 或 CGLib 动态生成代理类的字节码文件。

如果你喜欢这篇文章,请关注我的公众号,后续我将继续分享字节码增强的相关应用。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部