《设计模式系列》一文带你学习代理模式,场景案例,及源码刨析。

151 阅读8分钟

三大代理模式

代理模式

代理(Proxy)是设计模式中的其中一种。即通过代理对象访问目标对象。

优势

可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

框架中涉及到代理模式的地方

我们常用的 mybatis ,都是定义接口但是不需要书写实现类,就可以对 XML 或者 自定义的 sql 进行 curd 操作等。

一定要看完文章末尾会有关于 这部分 源码 显示。 如果没看懂前文或者不熟悉代理模式。直接看后文 源码 头一定会很疼。

一定要 仔细看仔细看仔细看。 重要的事情要说 3 遍。

列子

假设我们现在想邀请一位明星,众所周知我们并不是直接联系到明星,而是联系到明星的经纪人,来达到同样的目的。 切换到代理中也就是:明星就是一个 目标 对象,他只需要负责活动中的演出。其余的繁琐的事情就交由 代理(经纪人)来解决。

用图表示

代理.png

java 三种实现方式

  1. 静态代理
  2. 动态代理
  3. CGLIB代理

静态代理

  1. 创建演出对象
@Data
@Builder
public class Sign {

    private String name;

    private String program;

    private LocalDateTime signTime;
}
  1. 定义开幕式演出接口
public interface OpeningCeremony {
    // 定义开幕式演出接口
    void commercialShow(Sign sign);
}
public class OpeningCeremonyImpl implements OpeningCeremony{

    @Override
    public void commercialShow(Sign sign) {
        System.out.println("由" + sign.getName() +"于北京时间"+ sign.getSignTime() +"演出"+ sign.getProgram());
    }
}
  1. 创建代理
@Slf4j
public class StaticProxy implements OpeningCeremony{

    // 目标类
    private OpeningCeremony openingCeremony;
	
    public StaticProxy(OpeningCeremony openingCeremony) {
        this.openingCeremony = openingCeremony;
    }

    @Override
    public void commercialShow(Sign sign) {
        log.info("经纪人告知" + sign.getName() + "本次活动");
        log.info("预付款给经纪人");
        
        // 执行目标方法
        openingCeremony.commercialShow(sign);
        log.info("付尾款给经纪人");
    }
}
  1. Test
public class StaticProxyTest {

    public static void main(String[] args) {
        Sign sign = Sign.builder()
                .name("薛之谦")
                .signTime(LocalDateTime.now())
                .program("你过得好吗")
                .build();
        // 创建代理对象
        StaticProxy staticProxy = new StaticProxy(new OpeningCeremonyImpl());
        staticProxy.commercialShow(sign);
    }
}
  1. 输出
经纪人告知薛之谦本次活动
预付款给经纪人
由薛之谦于北京时间2021-07-15T15:26:28.784演出你过得好吗
付尾款给经纪人

静态代理总结

  1. 如代码所示在不改动 目标对象 的前提下,对目标对象进行了扩展。

  2. 缺点

    假设现在需求上需要改变一下明星,需要新增一个接口。那么现有的 StaticProxy 代理对象就无法为 改变明星 的目标对象进行代理。

    1. 静态代理需要和目标对象实现同一个接口。接口的变动会导致维护 StaticProxy目标对象实现

      			  2. 会因为接口的增加导致代理对象持续增长。 
      

解决方案

由下文的 动态代理 来解决这个缺点。

动态代理

介绍 动态代理 前,先介绍两个重要的 接口 和 **类 ** InvocationHandler Proxy

Proxy

Proxy 类就是用来创建一个代理对象的类, 里面提供了很多方法(有兴趣的同学可以打开这个类去了解) 这里只介绍我们要用到的一个方法,也是经常用的一个。newProxyInstance 静态方法

/**
 * @param   loader : 定义代理类的类加载器
 * @param   interfaces : 代理类实现的接口列表 即目标对象的接口列表
 * @param   h : 将方法调用分派到代理实例的调用处理程序  即调用该接口中的 invoke 方法(如果未理解,没关系下文还会提到)
 **/
public static Object newProxyInstance(ClassLoader loader,
                                     Class<?>[] interfaces,
                                     InvocationHandler h)

InvocationHandler 接口

proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用会被分派到调用处理程序的invoke方法。

该接口中只定义了一个方法 public Object invoke(Object proxy, Method method, Object[] args)

下面就用代码介绍如何使用 InvocationHandlerProxy

  1. 创建动态代理对象
@Slf4j
public class DynamicProxy {

    // 目标对象
    private Object target;

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

    public Object getProxyInstance() {
        // 相当于实现 InvocationHandler 中的 invoke 方法
        InvocationHandler handler = (proxy, method, args) -> {
            Sign sign = (Sign)args[0];
            log.info("经纪人告知" + sign.getName() + "本次活动");
            log.info("预付款给经纪人");

            // 执行目标方法
            method.invoke(target, args);
            log.info("付尾款给经纪人");

            return null;
        };
        // 返回代理对象
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), target.getClass().getInterfaces(), handler);
    }
}
  1. Test
public class DynamicProxyTest {

    public static void main(String[] args) {
        Sign sign = Sign.builder()
                .name("薛之谦")
                .signTime(LocalDateTime.now())
                .program("你过得好吗")
                .build();
        
        // 创建代理对象
        OpeningCeremony openingCeremony = (OpeningCeremony)new DynamicProxy(new OpeningCeremonyImpl()).getProxyInstance();
        openingCeremony.commercialShow(sign);
    }

  1. 输出
经纪人告知薛之谦本次活动
预付款给经纪人
由薛之谦于北京时间2021-07-15T15:26:28.784演出你过得好吗
付尾款给经纪人

添加扩展

静态代理 中我们提到过如果因为需要的变动需要 改变明星 静态代理会再次建立一个 代理类 实现 目标接口。 下面就用代码演示下 动态代理 如何解决掉这个缺点。

  1. 添加改变明星接口
public interface ChangeService {

    void changeSign(Sign sign);
}
public class ChangeServiceImpl implements ChangeService{
    
    @Override
    public void changeSign(Sign sign) {
        System.out.println("临时变更由" + sign.getName() +"于北京时间"+ sign.getSignTime() +"演出"+ sign.getProgram());
    }
}
  1. Test
public class StaticProxyTest {

    public static void main(String[] args) {
        Sign sign = Sign.builder()
                .name("毛不易")
                .signTime(LocalDateTime.now())
                .program("像我这样的人")
                .build();
		// 创建代理对象
        ChangeService openingCeremony = (ChangeService)new DynamicProxy(new ChangeServiceImpl()).getProxyInstance();
        openingCeremony.changeSign(sign);
    }
}
  1. 输出
经纪人告知毛不易本次活动
预付款给经纪人
临时变更由毛不易于北京时间2021-07-15T16:03:07.803演出像我这样的人
付尾款给经纪人

如上所示: 我们无需再为 改变明星 这个接口再次创建一个代理类。 只需要通过传参更换 DynamicProxytarget 目标对象即可实现扩展效果。

动态代理总结

代理对象 不需要实现接口,但是目标对象一定要实现接口。

CGLIB 代理

上面的 静态代理动态代理 模式都要求 目标对象 是实现一个接口或者多个接口的 目标对象 。但有时候 目标对象 仅仅是一个类。这个时候可以用目标对象的子类 实现代理

Enhancer : 对象把代理对象设置为被代理类的子类来实现动态代理的。因为是采用继承方式,所以代理类不能加final修饰,否则会报错。

final类 :类不能被继承,内部的方法和变量都变成final类型

  1. 创建Cglib代理对象
@Slf4j
public class CglibProxyChange implements MethodInterceptor {

    private Change change;

    public Change getInstance(Change change) {
        this.change = change;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.change.getClass());
        enhancer.setCallback(this);
        return (Change) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Sign sign = (Sign) objects[0];
        log.info("经纪人告知" + sign.getName() + "本次活动");
        log.info("预付款给经纪人");

        // 执行目标方法
        Object object = methodProxy.invokeSuper(o, objects);
        log.info("付尾款给经纪人");

        return object;
    }
}

  1. Test
 public static void main(String[] args) {
        Sign sign = Sign.builder()
                .name("毛不易")
                .signTime(LocalDateTime.now())
                .program("像我这样的人")
                .build();
        CglibProxyChange prayingMantis = new CglibProxyChange();
        Change instance = prayingMantis.getInstance(new Change());
        instance.changeSign(sign);
    }
  1. 输出
经纪人告知毛不易本次活动
预付款给经纪人
临时变更由毛不易于北京时间2021-07-15T16:41:28.275演出像我这样的人
付尾款给经纪人

总结

​ 看到了这里。那么我相信你一定是一个很上进。对代码有 敬畏感 的人。小编为给你感到开心,也为你 加油!!!

​ 先上两个关于 mybatis-spring 集成中的部分源码图,只需要 关注我框住的部分 即可。其余的先 不必关心

1626339736(1).png

1626339762(1).png

看到这个是否感觉到一股 熟悉 的味道了, 是的,没错正是我们前文所提到的 动态代理 模式。

这里我们就只是简单的聊一下,考虑到写的太多,消化不掉(毕竟一口吃不成大胖子)

关于 Spring-mybatis的实现我们得从 MapperScannerConfigurer说起, 首先 MapperScannerConfigurer实现了 BeanDefinitionRegistryPostProcessor接口。

BeanDefinitionRegistryPostProcessor依赖于 Spring框架,通俗的讲 BeanDefinitionRegistryPostProcessor使得我们可以将 BeanDefinition添加到 BeanDefinitionRegistry中,而 BeanDefinition描述了一个Bean实例所拥有的实例、结构参数和参数值,简单点说拥有它就可以实例化 Bean了。 BeanDefinitionRegistryPostProcessorpostProcessBeanDefinitionRegistry方法在 Bean被定义但还没被创建的时候执行,所以 Spring-mybatis也是借助了这一点。将扫描包路径的Bean 注册到了 registry

1626341799(1).png

mapperFactoryBeanClass 引用 便是 MapperFactoryBean 的子类 在checkDaoConfig 方法中会调用 addMapperaddMapper 中便会创建一个 MapperProxyFactory 也是最开始的第一张图。是不是又回到了熟悉的味道。

1626342042(1).png

1626342099(1).png

​ 这里就先大致介绍这些吧。 这里没对 spring-mybatis 做详情介绍。只是从宏观上面介绍了一下大致流程。 有兴趣得小伙伴可以点开这几个类去追溯一下。当然大家觉得小编写的不错,希望小编继续输出这部分集成的内容,可以下方留言。

代理模式在业务代码上是比较少见的,但是代理模式也是我们必须要理解的一种模式,因为学习好代理模式有助于我们去读一些源码。 本期文章就到这里结束了。欢迎各位伙伴将自己的看法评论在下面。