Java 动态代理:Java Proxy 和 CGLIB

如果我们想要编写简洁、易维护的 Java 代码,那么掌握动态代理是其中必不可少的一个技术。

想必大家都用过 Spring 框架,因此动态代理对于我们来说也不会太陌生。因为 Spring 框架中大量使用了动态代理,比如 Spring AOP、@Transactional 注解等等。但是如果我们项目中没有使用 Spring 框架,那么事实上,我们要自己实现类似于 Spring AOP 这样的面向切面编程也并不是很难。

本文会对 Java 中两种非常常见的动态代理(Java Proxy 和 CGLIB)进行对比说明,并给出实际项目中可用(需要自己具体实现逻辑)的示例代码。

为了内容的完整性,我们先从 Java 静态代理开始入手。

静态代理

在 Java 中进行面向接口的编程是非常常见的。譬如我们有一个 UserService 接口,其中定义了一个获取用户名的方法 getUsername。然后已经有小伙伴提供给我们一个 UserServiceImpl 类,该类已实现了 UserService 接口和相应获取用户名的方法。现在要求我们在不改动小伙伴代码的前提下,添加获取用户名方法前后的某些额外操作,比如:修改参数、记录日志等等。

对于这样的情况,我们可以使用静态代理非常轻松地实现该需求:

package com.wuxianjiezh.test;

/**
 * 静态代理方式测试类。
 *
 * @author 吴仙杰
 */
public class StaticProxyTest {

    public static void main(String[] args) {

        StaticProxy proxy = new StaticProxy();

        System.out.println("获取用户名:" + proxy.getUsername());
    }
}

interface UserService {

    String getUsername();
}

class UserServiceImpl implements UserService {

    @Override
    public String getUsername() {

        String username = getName();

        print(username);

        return username;
    }

    private String getName() {

        // int i = 1 / 0; // 测试异常时,代理类的处理情况

        return "Jason Wu";
    }

    private void print(String name) {
        System.out.println("内部打印用户名:" + name);
    }
}

class StaticProxy implements UserService {

    private UserService userService = new UserServiceImpl();

    @Override
    public String getUsername() {

        beforeInvoke();

        String returnVal;

        try {

            returnVal = userService.getUsername();

        } catch (Throwable t) {

            afterThrowable();

            throw t;

        } finally {

            afterInvoke();
        }

        return returnVal;
    }

    private void beforeInvoke() {
        System.out.println("静态代理——拦截在方法调用之前");
    }

    private void afterThrowable() {
        System.out.println("表态代理——拦截在出现异常之后");
    }

    private void afterInvoke() {
        System.out.println("静态代理——拦截在方法调用之后");
    }
}

控制台输出结果:

静态代理——拦截在方法调用之前
内部打印用户名:Jason Wu
静态代理——拦截在方法调用之后
获取用户名:Jason Wu

虽然我们通过静态代理实现了上面的需求,但是静态代理意味着有多少个类需要代理,就要建立多少个代理类,这就难免会导致代理类变得越来越多,维护难度也会随之加大。

我们来想一想,因为静态代理是在编译期就确定了的,所以需要我们编写各个实现的代理类。那么有没有办法,让代理类在运行时自动生成呢?如果可以,这不就可以解决了我们的问题。

Java 动态代理可以在运行时确定代理类。

JDK 原生动态代理(Java Proxy)

JDK 本身就提供了动态代理功能:Java Proxy。它是通过 Java 反射机制实现。

要使用 JDK 原生动态代理只需要以下简单两步即可:

  1. 实现一个 InvocationHandler 接口,并实现其中的 invoke 方法。在代理类执行接口的方法调用时,会将方法调用转发到 invoke 方法中
  2. 通过 Java Proxy 获取目标对象的代理对象,然后通过代理对象执行目标方法

针对上面例子中的 UserService 接口,我们可以使用 JDK 原生动态代理进行实现:

package com.wuxianjiezh.test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * JDK 原生动态代理测试类。
 *
 * @author 吴仙杰
 */
public class JDKProxyTest {

    public static void main(String[] args) {

        UserService userService = JDKProxy.bind(new UserServiceImpl(), UserService.class);

        System.out.println("获取用户名:" + userService.getUsername());
    }
}

class JDKProxy implements InvocationHandler {

    private Object target;

    private JDKProxy(Object target) {
        this.target = target;
    }

    /**
     * 绑定代理类。
     *
     * @param target 需要代理的目标对象。必须是一个接口的实现类
     * @return 代理类
     */
    public static <T> T bind(Object target, Class<T> clazz) {

        // 获取调用句柄
        JDKProxy handler = new JDKProxy(target);

        // 生成代理类
        Object proxy = Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 类加载器,用于定义代理类
                target.getClass().getInterfaces(), // 接口列表,用于指定代理类需要实现的接口
                handler // 调用句柄,用于分派方法调用

        );

        // 根据泛型强制类型转换
        // 因为代理类只关心接口,而不关心具体是什么实现类,所以代理类必须按接口类型进行强制转换
        return clazz.cast(proxy);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        beforeInvoke();

        Object returnVal;

        try {

            returnVal = method.invoke(target, args);

        } catch (Throwable t) {

            afterThrowable();

            // 因为通过 `Method#invoke` 调用目标对象的方法时,
            // 若目标对象的方法内部抛出异常,则只会抛出 `InvocationTargetException` 异常,
            // 这不对我们的异常分析没有帮助,所以只要抛出原始异常即可
            //
            if (t instanceof InvocationTargetException) {
                InvocationTargetException e = (InvocationTargetException) t;
                t = e.getTargetException();
            }

            throw t;

        } finally {

            afterInvoke();
        }

        return returnVal;
    }

    private void beforeInvoke() {
        System.out.println("JDK 原生动态代理——拦截在方法调用之前");
    }

    private void afterThrowable() {
        System.out.println("JDK 原生动态代理——拦截在出现异常之后");
    }

    private void afterInvoke() {
        System.out.println("JDK 原生动态代理——拦截在方法调用之后");
    }
}

控制台输出结果:

JDK 原生动态代理——拦截在方法调用之前
内部打印用户名:Jason Wu
JDK 原生动态代理——拦截在方法调用之后
获取用户名:Jason Wu

通过动态代理,我们除了省去编写代理类的工作外,还有一个非常重要的好处:通过动态代理,我们可以提取通用的处理逻辑,对代码进行高度可重用的重构,从而大大减少一些通用且冗余的代码,比如数据库的事务处理(提交和回滚)、日志记录等。

CGLIB 动态代理

虽然 JDK 原生动态代理可以实现动态代理,但是它却有一个编程方面的局限性:JDK 原生动态代理要求目标对象必须得实现接口才可以代理。

如果我们想要代理没有实现接口的类,那么就无法使用 JDK 原生动态代理。这是不是很难受?☹

哎,没得办法,你不让我们用,那我们就用其它的嘛!CGLIB 动态代理是通过动态生成被代理类的子类,即通过继承实现。它不论目标对象有没有实现接口都可以代理。

现在,我们回到上面的例子,在这里我们只考虑 UserServiceImpl 类,就像得了选择性健忘症一样,我们完成忘记了它是实现了一个 UserService 接口的类。

首先加入 CGLIB 依赖库:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.12</version>
</dependency>

然后我们通过 CGLIB 动态代理来实现一样的效果:

package com.wuxianjiezh.test;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

/**
 * CGLIB 动态代理动态代理测试类。
 *
 * @author 吴仙杰
 */
public class CGLIBProxyTest {

    public static void main(String[] args) {

        UserServiceImpl userService = CGLIBProxy.bind(new UserServiceImpl());

        System.out.println("获取用户名:" + userService.getUsername());
    }
}

class CGLIBProxy {

    /**
     * 绑定代理类。
     *
     * @param target 需要代理的目标对象
     * @return 代理类
     */
    @SuppressWarnings("unchecked")
    public static <T> T bind(Object target) {

        // 通过 CGLIB 动态代理获取代理对象
        //
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> {

            beforeInvoke();

            Object returnVal;

            try {

                returnVal = methodProxy.invokeSuper(obj, args);

            } catch (Throwable t) {

                afterThrowable();

                throw t;

            } finally {

                afterInvoke();
            }

            return returnVal;
        });

        return (T) enhancer.create();
    }

    private static void beforeInvoke() {
        System.out.println("CGLIB 动态代理——拦截在方法调用之前");
    }

    private static void afterThrowable() {
        System.out.println("CGLIB 动态代理——拦截在出现异常之后");
    }

    private static void afterInvoke() {
        System.out.println("CGLIB 动态代理——拦截在方法调用之后");
    }
}

控制台输出结果:

CGLIB 动态代理——拦截在方法调用之前
内部打印用户名:Jason Wu
CGLIB 动态代理——拦截在方法调用之后
获取用户名:Jason Wu

Good Job!

Java Proxy vs. CGLIB

Java Proxy 和 CGLIB 是 Java 中两种非常常见的动态代理方法,下面我们就来好好对比一下这两者之间的区别,具体开发中选择哪一种,完全取决我们自己。

原理

JDK 原生动态代理是通过 Java 反射机制实现。

CGLIB 动态代理是动态生成被代理类的子类,即通过继承实现。

优点

JDK 原生动态代理:Java 原生支持的,不需要任何外部依赖。

CGLIB 动态代理:

  • 不论目标对象有没有实现接口都可以代理
  • 底层采用 ASM 字节码生成框架,通过字节码技术生成代理类,因为比使用 Java 反射的性能要高

缺点

JDK 原生动态代理:

  • 目标对象必须得实现接口才可以代理,这导致了编程的局限性
  • 通过 Java 反射机制实现的动态代理,因反射的效率本身就不是很高,所以性能相对来说也有一些影响。但这不是关键,这不应该成为我们不选用 JDK 原生动态代理的主要条件

CGLIB 动态代理:

  • 无法处理 final 问题
    • 因为 CGLIB 是通过继承实现的,而 final 类不能有子类,所以 CGLIB 不能代理 final 类。否则可能抛出运行时异常:java.lang.IllegalArgumentException: Cannot subclass final class com.wuxianjiezh.test.UserServiceImpl
    • 同理,final 方法是不能被重写的,所以也不能通过 CGLIB 代理。遇到这种情况不会抛出异常,而是跳过 final 方法只代理其他方法;如果强行通过代理类调用 final 方法,则调用的还是原方法,并不会插入代理类的任何动作
  • 目标类必须提供可访问的默认构造函数(编译器默认都会生成一个 public 默认构造函数)。否则可能抛出运行时异常:java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值