面向对象编程内功心法系列十(聊一聊代理模式)

105 阅读8分钟

1.引子

代理,我们都很熟悉,非要举一个例子的话,比如说生活中的各种中介,比如说明星艺人的经纪人,都是代理,生活中的代理。

延伸到程序员的世界中也有代理,我们都非常熟悉代理设计模式。比如说提到spring框架,你一定会想起IOC,和AOP。IOC叫做控制反转,是工厂设计模式和依赖注入的应用。AOP叫做面向切面编程,相应的应用了代理设计模式。

这么看来代理两个字还有许多讲究,那么你有想过代理设计模式,到底解决了什么问题吗?或者说平常我们租房、买房为什么非要找中介呢?

在代理这种设计模式中,我们可以尝试问这么一些问题

  • 谁是代理对象?
  • 谁是被代理(目标对象)?
  • 如果没有代理对象,目标对象会怎么样?

举个例子,某歌星需要开一场演唱会,需要做许多准备工作。比如说搭台、商务签约、行程安排、最后开唱。我们常说术业有专攻,对于一个歌手,他擅长于也仅擅长于唱歌,别的不会。

那怎么办?请经纪人呐!像商务签约、行程安排都交给经纪人来做,歌星只需要到点登台唱歌即可。你看这就是代理,我们可以用正式一点的话语来总结描述一下

  • 代理是一种机制,一种用于控制访问目标对象的机制
  • 代理,可以实现对目标对象的保护(要找歌星签名,先得过经纪人这一关)
  • 代理,可以实现对目标对象的增强(歌星只需要关心唱歌,别的事情交给经纪人)

到这里,你应该能够充分理解代理,及代理设计模式了。

那到底什么是代理设计模式呢?

  • 代理设计模式,它是一种结构型设计模式
  • 有代理对象,被代理对象(目标对象)
  • 通过代理对象,可以实现对目标对象的访问控制,增加目标对象安全性
  • 通过代理对象,可以实现在不改变目标对象的的情况下,增强目标对象能力

2.案例

在引子部分,我们搞清楚了什么是代理设计模式,接下来我将通过案例实现,给你演示一下代理设计模式的应用。

关于代理设计模式,你需要留意它有两种实现方式,分别是

  • 静态代理
  • 动态代理

另外在实现的过程中,通常需要代理对象,与被代理对象实现相同的接口,即它们是同类。我们具体看代码案例吧,相信看到代码你就明白了。

2.1.静态代理

通常在项目中,我们将功能分为

  • 业务功能(比如说用户、订单的增删改查)
  • 非业务功能(比如说事务控制、记录日志、性能耗时统计)

对于业务开发工程师来说,只需要关注业务功能的开发。而非业务功能,在每个项目中都差不多,具备一定的通用性,完全可以通过代理设计模式来支持实现。

接下里我将给你模拟一个这样的案例,我们以保存用户接口为例,在保存用户的同时进行接口耗时统计。

2.1.1.接口

/**
 * 用户接口
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/3/7 16:09
 */
public interface UserService {
​
    /**
     * 保存用户接口
     * @param name
     */
    void saveUser(String name);
}

2.1.2.被代理类

/**
 * 用户接口实现类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/3/7 16:10
 */
public class UserServiceImpl implements UserService {
​
    /**
     * 保存用户接口
     * @param name
     */
    @Override
    public void saveUser(String name) {
        System.out.println("保存用户:" + name);
    }
}

2.1.3.代理类

/**
 * 用户接口代理,与目标对象实现相同的接口
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/3/7 16:12
 */
public class UserProxy implements UserService{
​
    private UserService target;
​
    public UserProxy(UserService target){
        this.target = target;
    }
​
    /**
     * 保存用户接口(代理)
     * @param name
     */
    @Override
    public void saveUser(String name) {
        // 统计耗时开始(增强的功能)
        System.out.println("统计保存用户接口耗时.start:" + System.currentTimeMillis());
​
        // 调用目标对象
        target.saveUser(name);
​
        // 统计耗时结束(增强的功能)
        System.out.println("统计保存用户接口耗时.end:" + System.currentTimeMillis());
    }
}

2.1.4.测试类

public static void main(String[] args) {
  // 创建目标对象
  UserService target = new UserServiceImpl();
​
  // 创建代理对象
  UserService proxy = new UserProxy(target);
​
  // 通过代理对象,实现保存用户的时候,统计耗时
  proxy.saveUser("小明");
}
​
#执行结果
统计保存用户接口耗时.start:1615105035485
保存用户:小明
统计保存用户接口耗时.end:1615105035485Process finished with exit code 0

2.1.5.总结分析

UserProxy类有两个特点

  • 实现UserService接口,与目标对象实现相同的接口
  • 持有UserSerivceImpl实例,通过组合实现目标对象的能力增强
  • 你需要关注UserProxy中的saveUser方法,在保存用户target.saveUser(name)前后,进行耗时统计

最后通过执行结果,我们看到在不修改UserSerivceImpl类代码的情况下,实现了保存用户的同时,进行接口耗时统计能力的增强。你看这就是代理设计模式,代码实现比较简单。

不过你需要注意,在实际项目中,静态代理使用非常少,原因是静态代理方式,每一个目标类都需要相应定义一个代理类,导致类的数量会膨胀,另外代码维护性比较差

那么针对静态代理存在的问题,我们该如何解决呢?答案是动态代理

2.2.动态代理

通过静态代理示例代码,我们比较直观的看到了代理设计模式的实现。不过我们说静态代理存在两个问题

  • 每一个目标类,都需要一个相应的代理类,类的数量膨胀问题
  • 静态代理,代码是在编译时实现,代码维护性比较差

针对静态代理的问题,jdk提供了动态代理的语法支持,接下来我们一起来看动态代理示例代码,你需要关注两个类

  • Proxy,创建代理对象入口类
  • InvocationHandler,具体实现代理增强的接口类

2.2.1.通过动态代理,创建代理类

/**
 * 动态代理示例
 * 1.Proxy,创建代理对象入口类
 * 2.InvocationHandler,具体实现代理增强的接口类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/3/7 16:33
 */
public class DynamicUserProxy {
​
    public static void main(String[] args) {
        // 创建目标对象
        UserService target = new UserServiceImpl();
​
        // 创建代理对象
        UserService proxy = (UserService)Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 统计耗时开始
                        System.out.println("统计保存用户接口耗时.start:" + System.currentTimeMillis());
​
                        // 调用目标对象
                        Object result = method.invoke(target, "小明");
​
                        // 统计耗时结束
                        System.out.println("统计保存用户接口耗时.end:" + System.currentTimeMillis());
​
                        return result;
                    }
                }
        );
​
        // 通过代理对象,实现保存用户的时候,统计耗时
        proxy.saveUser("小明");
    }
}
​
​
#执行结果
统计保存用户接口耗时.start1615106267280
保存用户:小明
统计保存用户接口耗时.end1615106267280Process finished with exit code 0

2.2.2.总结分析

对比动态代理,静态代理

  • 执行结果是一样的,都在不修改目标类UserServiceImpl的情况下,实现了接口耗时统计能力的增强

  • 代码结构都遵循了三个步骤

    • 创建目标对象

      // 创建目标对象
      UserService target = new UserServiceImpl();
      
    • 创建代理对象

      #静态代理
      // 创建代理对象
      UserService proxy = new UserProxy(target);
          
      #动态代理
      // 创建代理对象
      UserService proxy = (UserService)Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new InvocationHandler() {
              @Override
              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 统计耗时开始
                System.out.println("统计保存用户接口耗时.start:" +
                                   System.currentTimeMillis());
      ​
                 // 调用目标对象
                 Object result = method.invoke(target, "小明");
      ​
                 // 统计耗时结束
                 System.out.println("统计保存用户接口耗时.end:" + 
                                    System.currentTimeMillis());
      ​
                 return result;
               }
            }
      );
      
    • 通过代理对象,实现功能增强

    // 通过代理对象,实现保存用户的时候,统计耗时
    proxy.saveUser("小明");
    
  • 差异最大的地方,是在创建代理对象。动态代理中,我们通过Proxy的newProxyInstance方法,创建代理对象,它有三个参数

    • 参数1:类加载器,动态代理的本质是运行时实现增强,即在运行的时候jvm动态生成代理类字节码Class,从而实例化代理对象。因此,需要类加载器
    • 参数2:目标对象实现的接口列表,符合代理设计模式定义,代理类,与目标类实现相同的接口
    • 参数3:InvocationHandler,用于实现增强的接口,关心接口中的invoke方法,该方法中实现代理逻辑

通过上面动态代理,静态代理的对比,并且我详细给你解释了动态代理相关的代码细节,相信你可以很好的理解代理设计模式、静态代理、动态代理了。

关于代理模式,是一个很重要,且用的比较多的设计模式。最后我想抛出一个知识点,基于jdk提供的Proxy动态代理实现,我们叫做基于接口的动态代理,它的意思是说目标类必须要实现接口。那么如果目标类在没有实现任何接口的情况下,如何实现动态代理呢?答案是cglib,抛砖引玉期望你可以去了解一下