【一起学系列】之代理模式:是为了控制访问啊!

1,676 阅读6分钟

意图

为其他对象提供一种代理以控制对这个对象的访问

代理模式的诞生

【产品】:Kerwin,我记得你是在通州租房住吧?

【开发】:是啊,怎么了?

【产品】:你是房东直租还是中介啊?我最近真是特别烦中介,收费都好黑!

【开发】:我啊,我租的房子名义上倒是房东直租,但估计还是中介,你知道吗,中介的扩张是一个必然。

【产品】:扩张?你指的是全北京的房子都是中介的意思吗?

【开发】:现在肯定不至于全部都是,但也是大部分了,为什么会这样呢,因为中介需要控制租户,控制租金市场,如果租户直租房东,房东钱多人好,就很有可能很便宜,这就会打乱市场价格,所以拿下所有房老板,不仅为了赚钱,也是为了控制这种市场关系。

【产品】:我看你们程序员平常“傻傻的”,怎么对这个这么了解?莫非有计算机相关的故事?

【开发】:被你说中了,这个就是代理模式!它的诞生就是为了控制对象的访问,不过我们一般是用来增强其功能,不像XXX🤪

HeadFirst 核心代码

定义正常业务类接口

public interface PhoneInterface {

    /***
     * 更新电话号码
     * @param phoneNum    电话号码
     * @throws Exception  可能抛出Exception 异常
     */
    void updatePhone(Long phoneNum);
}

实现正常业务类

public class PhoneServiceImpl implements PhoneInterface{

    @Override
    public void updatePhone(Long phoneNum) {
        System.out.println("update phoneNum is: -> " + phoneNum);
    }
}

静态代理业务类

public class PhoneServiceProxy implements PhoneInterface {

    /** 代理模式一般自行New对象, 反观装饰器模式则是传入对象 **/
    private PhoneInterface phoneInterface;

    public PhoneServiceProxy() {
        this.phoneInterface = new PhoneServiceImpl();
    }

    @Override
    public void updatePhone(Long phoneNum) {
        before(phoneNum);
        phoneInterface.updatePhone(phoneNum);
        after();
    }

    private void before(Long phoneNum) {
        System.out.println(MessageFormat.format("log start time:{0} , phoneNum is: {1}", new Date(), phoneNum));
        if (null == phoneNum || String.valueOf(phoneNum).length() != 11) {
            throw new RuntimeException("Update phoneNum fail, phoneNum is wrong.");
        }
    }

    private void after() {
        System.out.println(MessageFormat.format("log end time:{0}", new Date()));
    }
}

静态代理模式的设计思路:

  • Proxy 代理类
  • RealSubject 定义被代理的实体
  • Subject 定义RealSubject和Proxy共用接口

简单来说,

  1. 需要一个普通接口及其普通实现类
  2. 代理类同时实现该接口,自行new出对应实现类对象,对接口方法的前后增加额外操作

如果看着有点模棱两可,建议看完本文后,访问专题设计模式开源项目,里面有具体的代码示例,链接在最下面

装饰器模式和代理模式的区别

  • 持有对象方式:代理模式一般是New,装饰器模式则是传入同一接口对象
  • 意图:装饰器模式意在增强方法功能,代理模式意在控制对象的访问(例如代码中增加校验)

动态代理

刚才 HeadFirst核心代码 章节展示的是其静态代码的书写方式,如果所有的类都基于这样实现,那势必发生类膨胀的无解问题,因此真正常用的还是动态代理,分为两种 CGLIB | JDK动态代理

JDK 动态代理之MyBatis

注意事项:

  1. JDK动态代理的本质是创造一个实现了同一个接口的Proxy代理类,去进行真正的调用
  2. JDK动态代理在实现中的本质是反射技术
  3. 由于所有的代理类都实现了Proxy.class -> 包括帮我们创造的代理类也是,因此由于JAVA单继承的特点,只能想要实现代理必须实现某一个接口

JDK 动态代理必不可少的三要素:InvocationHandler,newProxyInstance,invoke

public class MybatisInvocation implements InvocationHandler {

    /**
     * 代理指定的接口
     * @param tClass 接口class
     * @param <T>    接口类型
     */
    @SuppressWarnings("unchecked")
    public static  <T> T newProxyInstance(Class<T> tClass) {
        return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class[]{tClass}, new MybatisInvocation());
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.isAnnotationPresent(Select.class)) {
            Select select = method.getAnnotation(Select.class);
            System.out.println(MessageFormat.format("Method Name: {0} , Annotation Value is: {1}", method.getName(), select.value()));
        }

        // 获取到SQL及参数, 即可通过JDBC进行数据库操作查询数据, MyBatis不再神秘
        return Arrays.asList("I", " am", " Kerwin~");
    }
}

被代理的接口

public interface MyBatis {

    @Select("select * from demo")
    List<String> select();
}

测试调用

public class App {
    public static void main(String[] args) {
        // JDK动态代理:模拟 MyBatis 核心代理阶段
        MyBatis batis = MybatisInvocation.newProxyInstance(MyBatis.class);
        System.out.println("Result:" + batis.select());
    }
}

输出结果

# Method Name: select , Annotation Value is: select * from demo
# Result:[I,  am,  Kerwin~]

MyBatis中的JDK 动态代理:我们在使用MyaBtis的时候,肯定想过,它凭什么一个接口就可以输出结果,利用JDK 动态代理,可以非常方便的构建接口的代理,我们便可以在 Invoke 方法中大做文章,解析方法注解的值,解析其方法返回值,然后利用JDBC即可实现数据库查询实现一个简单ORM框架,推荐大家自行尝试一下

CGLIB 动态代理之AOP

基础使用

public class PhoneCglibProxy implements MethodInterceptor {

    Object target;

    public PhoneCglibProxy(Object o) {
        this.target = o;
    }

    public Object newProxyInstance(){

        Enhancer en = new Enhancer();

        // 设置要代理的目标类
        en.setSuperclass(target.getClass());

        // 设置要代理的拦截器
        en.setCallback(this);

        // 生成代理类的实例
        return en.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println(MessageFormat.format("Method Name is: {0} Params is: {1}", method.getName(), Arrays.toString(objects)));
        return methodProxy.invokeSuper(o, objects);
    }
}

测试调用

public class App {

    /***
     * CGLIB动态代理
     *      如果类是final的,则无法生成代理对象,报错
     *      如果方法是final的,代理无效
     *
     * 关键代码:
     *     1.PhoneCglibProxy 实现 MethodInterceptor 方法拦截器接口  同时实现其 newProxyInstance方法 -> 该方法内容比较固定
     *     2.通过代理工厂构建, 创建对象, 使用即可
     *
     * Spring 3.2之后默认包含了cglib依赖
     * 普通项目 CGLIB依赖如下:
     *
     *     <dependency>
     *        <groupId>cglib</groupId>
     *        <artifactId>cglib-nodep</artifactId>
     *        <version>2.2.2</version>
     *     </dependency>
     *
     * 推荐代码阅读顺序:
     *
     * @see PhoneServiceImpl
     * @see PhoneCglibProxy
     */
    public static void main(String[] args){
        PhoneServiceImpl phone = new PhoneServiceImpl();

        PhoneCglibProxy proxyFactory = new PhoneCglibProxy(phone);
        PhoneServiceImpl service = (PhoneServiceImpl) proxyFactory.newProxyInstance();
        service.updatePhone(15186564812L);
    }
}

CGLIB 动态代理:Spring 3.2之后默认包含了cglib依赖,在使用中也要注意 final 关键字会使CGLIB代理失效,另外Spring AOP 默认采用JDK 动态代理,同时配合CGLIB代理一起实现的。

两种动态代理总结

  • JDK 动态代理只能针对实现了接口的类的接口方法进行代理
  • CgLib 动态代理基于继承来实现代理,所以无法对final类、private方法和static方法实现代理

Spring AOP

  • 如果目标对象实现了接口,则默认采用JDK 动态代理
  • 如果目标对象没有实现接口,则采用CgLib 动态代理
  • 如果目标对象实现了接口,且强制CgLib 代理,则采用CgLib进行动态代理

关于两种动态原理的实现原理可以查查其他的文章~

遵循的设计原则

  • 封装变化:在父级接口中提供 default 方法,子类实现其对应的状态方法即可
  • 多用组合,少用继承:代理模式经常和策略模式做对比,它们都是利用组合而非继承增强其变化和能力

什么场景适合使用代理模式

当我们需要为额外控制对象方法的执行时,比如历史项目的接口都没有记录日志,在Spring环境下,我们可以对所有的Bean方法增加日志功能,又或是多数据源时,通过注解标明对应的数据源,解耦代码等等

最后

附上GOF一书中对于代理模式的UML图:

相关代码链接

GitHub地址

  • 兼顾了《HeadFirst》以及《GOF》两本经典书籍中的案例
  • 提供了友好的阅读指导