设计模式二:代理模式

1、什么是动态代理

可能很多小伙伴首次接触动态代理这个名词的时候,或者是在面试过程中被问到动态代理的时候,不能很好的描述出来,动态代理到底是个什么高大上的技术。不方,其实动态代理的使用非常广泛,例如我们平常使用的 Spring中的 @Transactional注解,其依赖于 AOP,而 AOP的底层实现便是动态代理,看到这里,是不是更有兴趣去了解动态代理了呢?

动态代理:可以分解为"动态"+"代理"。

  • 代理:"代理"一词,在我们的生活中也是随处可见的,例如房屋中介,其是对房主的一种代理,房主需要出租其房屋,但是可能没时间去接待租客,给租客介绍房屋信息,带领租客看房,但是房屋中介可以为租客提供这些服务,所以,代理其是对被代理对象的一个功能增强
  • 动态:"动态"通常与"静态"相比较,"静态"描述的是事物是固定存在的,"动态"则描述的是事物是随着需求而动态生成的。

所以,静态代理存在一定的局限性,不能很好的满足需求的千变万化,动态代理的出现,就是为了解决这些局限性。

我们先来看看静态代理。

2.、静态代理

在开发中,通常需要为方法添加日志打印,能够记录程序的执行过程,以便后续出现异常问题的时候,能更好的排查定位。

假设我们现在已经完成了系统用户的增加、删除、修改等功能,这些功能在类 UserServiceImpl中已经实现。

代码示例:

java 复制代码
public class UserServiceImpl {
    public void add() {
        System.out.println("添加用户");
    }
    
    public void update() {
        System.out.println("修改用户");
    }

    public void delete() {
        System.out.println("删除用户");
    }
}

现在,我们需要在UserServiceImpl类中的方法添加日志功能,那么怎么才能更好地去实现这个需求呢?

1)直接在目标方法前后添加日志代码

代码示例:

java 复制代码
public class UserServiceImpl {
    public void add() {
        System.out.println("====== add方法开始 ======");
        System.out.println("添加用户");
        System.out.println("====== add方法结束 ======");
    }
​
    public void update() {
        System.out.println("====== update方法开始 ======");
        System.out.println("修改用户");
        System.out.println("====== update方法结束 ======");
    }
​
    public void delete() {
        System.out.println("====== delete方法开始 ======");
        System.out.println("删除用户");
        System.out.println("====== delete方法结束 ======");
    }
}

观察上述代码,这种方式的缺点在于:

2)静态代理方式实现

静态代理需要我们将目标类的方法抽取到接口中,代理类和目标类实现同一个接口,既然要实现代理,代理类自然需要在其内部维护目标对象的引用,并通过构造函数为其赋值,然后在代理类的方法中调用目标对象的同名方法,并在调用前后完成功能的增强。

实现步骤:

  • 抽取UserService接口
  • 创建目标类UserServiceImpl实现UserService接口
  • 创建代理类UserServiceProxy实现UserService接口
  • 代理类中完成功能的增强

代码实现:

java 复制代码
// 目标接口
public interface UserService {
    void add();

    void update();

    void delete();
}
// 目标类
public class UserServiceImpl implements UserService {
    @Override
    public void add() {
        System.out.println("添加用户");
    }

    @Override
    public void update() {
        System.out.println("修改用户");
    }

    @Override
    public void delete() {
        System.out.println("删除用户");
    }
}
// 代理类
public class UserServiceProxy implements UserService {

    private UserService userService;

    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void add() {
        System.out.println("====== add方法开始 ======");
        userService.add();
        System.out.println("====== add方法结束 ======");
    }

    @Override
    public void update() {
        System.out.println("====== update方法开始 ======");
        userService.update();
        System.out.println("====== update方法结束 ======");
    }

    @Override
    public void delete() {
        System.out.println("====== delete方法开始 ======");
        userService.delete();
        System.out.println("====== delete方法结束 ======");
    }
}

观察上述代码,静态代理遵循开闭原则,在不修改目标类的前提下,完成了功能的增强,但是依然存在大量重复的代码,且一个代理类只能代理一个目标类,如果有n个目标类需要被代理,就需要同比增加n个代理类。

那么,有没有办法可以使得我们不需要去定义这么多的代理类,就可以实现对目标类功能的增强?答案是有的:动态代理

3、JDK动态代理

前面,我们提到静态代理的实现方式:代理类和目标类都实现同一个接口,在代理类中维护目标类对象,并完成对目标类对象方法的增强,这种方式虽然遵循开闭原则,但是代理类和目标类至少是"一对一"的绑定关系,如果需要被代理的目标类个数越多,代理类就会越多,会产生大量重复的代码,也不利于后期的维护。

从静态代理中,我们知道代理类也是接口的一个实现类,代理对象的类型也是属于接口类型,我们来验证一下。

java 复制代码
public class Test {
    public static void main(String[] args) {
        UserServiceProxy userServiceProxy = new UserServiceProxy(new UserServiceImpl());
        System.out.println(userServiceProxy instanceof UserService);
    }
}
// 打印结果:true

那么,能不能动态生成这些代理对象呢?我们知道类是构造对象的模板,代理类都还不存在,怎么去构造代理对象呢?

除了不存在代理类,还剩下 UserService接口和 UserServiceImpl目标类,JDK动态代理的目的就是通过接口来生成代理类以及代理类的对象,我们知道接口是不能直接通过new关键字创建对象的。

那么JDK动态代理是怎么创建出代理类以及代理类对象的呢?

我们先来看看通过 new关键字创建对象的过程。

java 复制代码
UserServiceImpl userService = new UserServiceImpl();
/*
创建对象的过程:
    1.执行new指令,如果类未加载,先执行类加载过程。
        1.加载:JVM通过ClassLoader将UserServiceImpl.class文件加载到方法区(Method Area),在堆内存中创建代表该类的Class对象。
        2.验证
        3.准备:为静态变量分配内存并设置类型初始值。
        4.解析
        5.初始化:为静态变量赋值、执行静态代码块
    2.为对象分配内存,将对象的实例字段初始化类型零值。
    3.执行构造方法,对对象进行初始化
*/

追踪上述过程,我们得知创建对象,需要先得到该类的Class对象,通过Class对象去创建实例对象。为了验证这一点,我们不妨来看看通过反射的方式创建对象的过程。

java 复制代码
public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        // 获取Class对象
        Class<UserServiceImpl> userServiceClass = UserServiceImpl.class;
        // 获取构造器
        Constructor<?>[] constructors = userServiceClass.getConstructors();
        for (Constructor<?> constructor : constructors) {
            // 通过构造器创建实例对象
            System.out.println(constructor.newInstance());
        }
    }
}

现在,问题回归到接口不能直接new,也没有构造方法,并且不存在代理类的class文件,怎么获得Class对象了。

动态代理关键类

我们先来看看JDK动态代理的实战代码:

  • 需要自定义个 CustomInvocationHandler实现 InvocationHandler接口。
  • 利用 Proxy.newProxyInstance构建实例对象。
java 复制代码
// UserService接口
public interface UserService {
    void add();

    void update();

    void delete();
}

// 目标类
public class UserServiceImpl implements UserService {
    @Override
    public void add() {
        System.out.println("添加用户");
    }

    @Override
    public void update() {
        System.out.println("修改用户");
    }

    @Override
    public void delete() {
        System.out.println("删除用户");
    }
}

// CustomInvocationHandler
public class CustomInvocationHandler implements InvocationHandler {

    // 目标对象
    private Object target;

    public CustomInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("====== 方法开始 ======");
        Object result = method.invoke(target, args);
        System.out.println("====== 方法结束 ======");
        return result;
    }
}

public class Test {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        // 关键代码
        UserService service = (UserService) Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces(), new CustomInvocationHandler(userService));
        service.add();
    }
}

从测试代码可以看出,Proxy类是关键。我们来看看Proxy为我们提供的方法:

Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)虽然被标注为过时方法,但是从名字上可以得知,其目的是为了获得代理类的Class对象。话不多说,我们来调用一下。

java 复制代码
public class Test {
    public static void main(String[] args) {
        Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());
        System.out.println(proxyClass.getName());
        for (Method method : proxyClass.getDeclaredMethods()) {
            System.out.println(method.getDeclaringClass() + "." + method.getName() + "()");
        }
        System.out.println(Arrays.toString(proxyClass.getConstructors()));
    }
}

可以看到:

  • 获得的Class对象的名称为 $Proxy0
  • 定义了我们需要的 add();update();delete()方法。
  • 定义了一个有参构造方法 $Proxy0(InvocationHandler handler)

虽然没有无参构造方法,我们还是得尝试一下调用一下这个有参的构造方法,需要我们传入一个 java.lang.reflect.InvocationHandler对象

java 复制代码
public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
        Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());
        // 获取$Proxy0(InvocationHandler handler)构造方法
        Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
        UserService userService = (UserService) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(proxy.getClass());
                System.out.println(method.getDeclaringClass() + "." + method.getName() + "()");
                return null;
            }
        });
        userService.add();
    }
}

看的出来,**当我们获得代理对象之后,通过代理对象来调用接口方法,都会回调构造时传进来的 InvocationHandler对象的 invoke(Object proxy, Method method, Object[] args)方法,**该方法有3个参数:

  • Object proxy:代表的是代理对象本身。
  • Method method:代表的是被调用的方法的Method对象。
  • Object[] args:代表的是被调用方法的参数。

可以猜测,JDK动态代理生成的代理类中,维护了InvocationHandler类的对象变量,并且在实现接口方法时,通过InvocationHandler对象调用了 invoke(Object proxy, Method method, Object[] args)方法。

java 复制代码
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

不知道大家没有看到这行代码哈,当添加了这行代码之后,可以将在项目目录下保存动态创建的class文件, com/sun/proxy/$Proxy0.class

可以看到生成的代理类 $Proxy0继承自 Proxy类,并实现了 UserService接口,并且在 add()方法中通过其父类 Proxy中维护的 InvocationHandler对象调用 invoke()方法,这也就成功的解释了前面调用 userService.add()方法,会回调到invoke()方法。

这时候我们再把代码改造一下,如下:

java 复制代码
public class CustomInvocationHandler implements InvocationHandler {

    // 目标对象
    private Object target;

    public CustomInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("====== 方法开始 ======");
        Object result = method.invoke(target, args);
        System.out.println("====== 方法结束 ======");
        return result;
    }
}

public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        UserServiceImpl target = new UserServiceImpl();
        Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());
        Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
        UserService userService = (UserService) constructor.newInstance(new CustomInvocationHandler(target));
        userService.add();
    }
}

这样就完成了对目标对象功能的增强,前面我们提到过 Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)

已经被标注为过时,推荐我们使用 Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)方法。

动态代理设计思想

好的,到这里,我们来总结一下JDK动态的设计思想:

使用 JDK动态代理,使得我们免去编写代理类,只需要将增强功能编写在 InvocationHandlerinvoke方法中。

4、CGLib动态代理

CGLib代理的目标对象不需要事先任何接口,它是通过动态集成目标对象实现动态代理的。CGLib代理执行代理方法的效率之所以比JDK高,是因为CGLib采用了FastClass机制:为代理类和被代理类各生成一个类,这个类会为代理类或被代理类的方法分配一个index(int类型);这个index当作一个入参,FastClass 就可以直接定位要调用的方法并直接进行调用,省去了反射调用,所以调用效率比JDK代理通过反射调用高。FastClass并不是跟代理类一起生成的,而是在第一次执行MethodProxy的invoke()或invokeSuper()方法时产生并放在缓存中的。

5、CGLib和JDK动态代理对比

  • JDK动态代理实现了被代理对象的接口,CGLib代理继承了被代理对象。
  • JDK动态代理和CGLib代理在运行期生成字节码,JDK动态代理直接写Class字节码,CGLib代理使用ASM框架(字节码操控框架)写Class字节码,CGLib代理实现更复杂,生成代理类比JDK动态代理效率低。
  • JDK动态代理调用代理方法是通过反射机制调用的,CGLib代理是通过FastClass机制直接调用方法的,CGLib代理的执行效率更高

6、Spring中的代理选择原则

  • 当Bean有实现接口时,Spring就会用JDK动态代理
  • 当Bean没有实现接口时,Spring会选择CGLib代理
  • Spring可以通过配置强制使用CGLib代理,只需要在配置中加入<aop:aspectj-autoproxy roxy-target-clas="true">
相关推荐
越甲八千32 分钟前
重温设计模式--享元模式
设计模式·享元模式
码农爱java2 小时前
设计模式--抽象工厂模式【创建型模式】
java·设计模式·面试·抽象工厂模式·原理·23种设计模式·java 设计模式
越甲八千2 小时前
重温设计模式--中介者模式
windows·设计模式·中介者模式
犬余3 小时前
设计模式之桥接模式:抽象与实现之间的分离艺术
笔记·学习·设计模式·桥接模式
Theodore_10224 小时前
1 软件工程——概述
java·开发语言·算法·设计模式·java-ee·软件工程·个人开发
越甲八千5 小时前
重拾设计模式--组合模式
设计模式·组合模式
思忖小下8 小时前
梳理你的思路(从OOP到架构设计)_设计模式Composite模式
设计模式·组合模式·eit
机器视觉知识推荐、就业指导8 小时前
C++设计模式:组合模式(公司架构案例)
c++·后端·设计模式·组合模式
越甲八千9 小时前
重拾设计模式--工厂模式(简单、工厂、抽象)
c++·设计模式
重生之绝世牛码10 小时前
Java设计模式 —— 【结构型模式】外观模式详解
java·大数据·开发语言·设计模式·设计原则·外观模式