设计模式之代理模式

683 阅读7分钟

模式介绍

代理模式(Proxy Design Pattern)是一种结构型设计模式,它用于在不更改原有类代码的情况下,扩展(增强)其功能。

代理模式给某一个对象提供一个代理对象,由代理对象持有原对象的引用,并控制对原有对象的访问。通过对象组合代理对象可以完全覆盖原对象的能力,并且可以按需增加相关代码,对功能进行增强,也使得代码更加符合OCP。

对于代理模式,Java开发者肯定不陌生,Spring、Mybatis、Dubbo等框架中都有大量使用,熟练掌握代理模式的设计思想与使用方式,对于我们阅读这些优秀的源码大有裨益。

下图为代理模式的UML类图,类图中的几个主体分别是客户端-Client、主题接口-Subject、实际主题实现类-RealSubject、代理类-Proxy。
image.png
从类图中可以看到:RealSubject、Proxy都实现了Subject接口,并且Proxy持有Subject的引用(实际为RealSubject实例);Client持有Subject的实例,但是该实例由运行时决定(Client持有的Subject实现为Proxy实例)。

Proxy类是一个中间层,原来由RealSubject直接完成的工作转交给Proxy去完成,充当了一个“中介”角色。这个中间层并不是徒增代码的复杂度,借助它我们可以做很多工作,也可以带来很多好处,通过几个业务场景举例说明一下:

  • 如果需要统计RealSubject#request方法的耗时、错误率等指标,就可以在Proxy中做数据埋点而不需要修改RealSubjce的代码。统计性能指标本不是RealSubject的功能,强行为他增加逻辑会破坏它的完整性,不符合OCP、SRP等设计原则;而且如果RealSubject是第三方提供的服务,我们也无法更改其代码。使用Proxy代理类,更好的实现职责分离,原有代码无入侵,他好我也好。
  • 如果需要为RealSubject#request方法增加Redis缓存以提高接口性能,我们可以使用Proxy类对用户的请求进行拦截,增加Redis缓存逻辑。当然在Java中,我们一般使用Spring AOP做动态实现,但是原理是一样的。


在实际应用中,代理模式有静态代理和动态代理两种实现。静态代理即代理类在代码编译之前已经实现,或为程序员手动实现,或为工具自动生成;动态代理是通过反射技术在运行时生成代理类。

上面文字描述比较枯燥,下面以示例代码对两种代理模式的使用进行说明。继续上文提到的例子,我们需要对现有业务代码增加性能统计逻辑,现有代码如下:

public interface Subject {
    /**
     * 接口方法
     */
    void request();
}

public class RealSubject implements Subject {
    @Override
    public void request() {
        try {
            //通过sleep模拟业务执行
            Thread.sleep(50);
        } catch (Exception ex) {

        }
        System.out.println("RealSubject#request method invoked.");
    }
}

public class Client {
    public static void main(String[] args) {
        //实例化对象
        Subject realSubject=new RealSubject();
        //方法调用
        realSubject.request();
    }
}

静态代理

目标:为RealSubject#request增加性能统计功能,为了简单我们仅做耗时演示。
思路:按照代理模式的设计思想,在静态代理中我们需要手动创建代理类,该代理类需要同样实现Subject接口,并且包含对RealSubject的引用,需要在代理类中增加调用耗时计算逻辑用于完成耗时统计。

基于上述思路,创建MetricProxy代理类,代码如下:

public class MetricProxy implements Subject {

    private Subject delegate;

    public MetricProxy(Subject delegate) {
        this.delegate = delegate;
    }

    @Override
    public void request() {
        //方法执行开始时间
        long start = (new Date()).getTime();
        //调用委托对象的request方法
        this.delegate.request();
        //方法执行结束时间
        long stop = (new Date()).getTime();
        //计算耗时
        long span = stop - start;
        System.out.printf("invoke request() cost %d ms", span);
    }
}

通过MetricProxy构造方法传入委托对象,然后在request方法内调用delegate#request方法,确保原有功能不变;为了增加耗时统计,在调用delegate#request前后增加了时间记录和计算逻辑。接下来,我们就可以在Client中这么调用了:

public class Client {
    public static void main(String[] args) {
        //创建委托对象
        Subject delegate = new RealSubject();
        //创建代理对象
        Subject proxy = new MetricProxy(delegate);
        //调用代理对象request方法
        proxy.request();
    }
}

整体看来,静态代理方式是“模式介绍”一节中的UML类图的“高保真”实现,两者结合起来,对于代理模式应该会有一个直观的理解了。

这里有个问题需要一起考虑下:**现在是一个Subject接口需要增加性能统计功能,如果有100个Subject这样的类似的接口需要扩展统计功能呢?写100个代理类?简直不要太酸爽!**到这里,就需要动态代理上场了。

动态代理

前面有提到,动态代理是在运行时使用反射技术动态实现代理类,开发人员对这一过程并无感知。Java SDK reflect包提供了Proxy、InvocationHandler等工具类用于实现动态代理,使用Java实现动态代理并不是难题。

按照JDK要求创建代理助手类DynamicProxyHandler,实现InvocationHandler接口,在其invoke方法内实现性能指标统计功能。

public class DynamicProxyHandler implements InvocationHandler {
    //委托对象
    private Object delegate;

    public DynamicProxyHandler(Object delegate) {
        this.delegate = delegate;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //开始执行时间
        long start = (new Date()).getTime();

        //调用委托对象的方法
        Object result = method.invoke(delegate, args);

        //调用完成时间
        long stop = (new Date()).getTime();
        //计算时长
        long span = stop - start;
        System.out.printf("invoke request() cost %d ms", span);
        return result;
    }
}

使用Proxy#newProxyInstance创建动态代理对象:

public class Client {
    public static void main(String[] args) {
        //加上这句将会产生一个$Proxy0.class文件,这个文件即为动态生成的代理类文件
        //System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        //创建代理助手类对象
        DynamicProxyHandler proxy = new DynamicProxyHandler(new RealSubject());
        //动态创建代理类对象,它会实现Subject接口
        Subject subject = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(), new Class[]{Subject.class}, proxy);
        //调用方法
        subject.request();
    }
}

运行一下,会发现与静态代理的例子输出的结果完全一致。到这里,我们会感觉与静态代理没有什么差别,也实现了一个代理类,工作一点也没少。但是,仔细观察会发现DynamicProxyHandler其实是与类型无关的,这里我们传入了Subject,也同样可以传递其他的类型,这就是它的强大之处。

有没有好奇,JDK动态生成的代理类到底长什么样子呢?另外,有的同学会问subject.request()这句代码是如何走到DynamicProxyHandler#invoke的呢?

看下上面Client#main方法的第4行,把前面注释打开,再次运行代码,会在包com.sun.proxy下生成一个$Proxy0.class文件,我把它贴在下面:

public final class $Proxy0 extends Proxy implements Subject {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void request() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    //反射创建方法对象
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.raysonxin.proxy.staticproxy.Subject").getMethod("request");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

$Proxy0继承Proxy类,并实现Subject接口,所以它同时具备两者的能力。以request方法为例:当request方法被调用时,它会执行super.h.invoke(this, m3, (Object[])null);,这里的super是其基类Proxy,h就是我们的DynamicProxyHandler,所以最终会调用到DynamicProxyHandler#invoke方法。

Client中动态代理实现方式是JDK基础实现,而实际开发中往往借助Spring AOP强大的面向切面编程,基于Spring Bean、注解等技术进行动态实现。在我的上一篇文章《Mybatis源码之SQL执行过程》中,Mapper实例的创建就是一个鲜活的例子。

总结

代理模式是我们开发中常见、常用、常说的经典设计模式,旨在提高程序的可扩展性,让代码可维护。日常工作中使用的Spring、Mybatis等优秀的Java开发框架正是以反射+动态代理为基石,提供强大的功能,为我们的开发提供诸多便利。

本文从简单介绍了代理模式的设计思想,并通过实例对其经典的静态代理、动态代理两种方式举例说明。通过查看JDK生成的代理类,理解其执行过程。

百闻不如一见,百看不如一练。希望此文可以帮你初步了解代理模式。

本人能力有限,如有任何问题,请大家不吝赐教!