思维盛宴之设计模式-代理模式Proxy Pattern

来源:互联网 发布:python字符串截取函数 编辑:程序博客网 时间:2024/05/02 02:55

1. 简介和适用场景

代理模式:为某个对象提供一个代理,以控制对那个对象的访问。代理类和委托类(委托别人代理它自己的类)通常具有相同的父类或接口,这样委托类对象出现的地方一定可以用代理类对象代替。根据面向对象设计中的“里氏替换原则”,通常不论是代理对象还是委托对象,对调用者而言都呈现为一个共同接口的实现对象,而代理对象正好可以在对调用方保持透明的前提下在方法调用前执行一些预处理,然后将实际操作继续交给委托类对象自己完成,最后再执行一些清理扫尾工作。以上描述的过程正是AOP的场景,因此,一些知名的AOP框架的底层机制正是代理模式,如Spring AOP(动态代理)。

2. 实现思路

看过上面的简介,代理的总体思路其实就是这样:编写或生成一个类,即代理类,它与委托类实现相同的接口来替换委托类或对象(调用委托类或对象的时候其实真实调用的是代理类或对象),这样外部在调用时就可以一方面屏蔽内部细节,另一方面在内部对委托类或其对象的方法调用作“包装”。

那么如何来编写或者生成这个代理类呢?有两种实现方式:静态代理和动态代理。

静态代理的代理类由开发者创建,在编译期就已存在和确定,它与委托类都实现了一个共同的接口,client直接使用代理类及其对象即可。

而动态代理的代理类和对象是在运行期由jvm根据反射等机制动态生成的,他们与委托类和对象的关系(指代理关系)也是运行期决定的。
动态代理有基于jdk和基于cglib的两种实现。基于jdk的实现要求委托类要实现接口,这样才能帮助jvm在底层生成对应接口的实现类(即代理类)。

3. 代码实现

设想我们现在需要编写一个jdbc模板封装常用的dao操作,并且利用aop的思想给操作加入日志记录功能。为了更清晰的说明问题,代码进行了相当程度的简化并加以注释。

3.1 静态代理

(1)代理的接口

/** * 代理的接口 */public interface DbOperations {    //根据id删除记录    public void delete(int id);    //插入或更新    public void insertOrUpdate(Object obj);    //根据id查询    public void find(int id);}

(2)委托类

/** * 委托类,处理实际业务(dao操作),实现需要他方代理的接口 */public class JdbcTemplate implements DbOperations{    @Override    public void delete(int id) {        System.out.println("delete "+id);//模拟删除操作    }    @Override    public void insertOrUpdate(Object obj) {        System.out.println("insertOrUpdate");//模拟操作    }    @Override    public void find(int id) {        System.out.println("find");//模拟操作    }}

(3) 静态代理类

/** * 静态代理类,与委托类一样实现代理的接口 */public class JdbcStaticProxy implements DbOperations {    /**     * 代理类对象持有委托类的对象引用     */    private DbOperations jdbcTemplate;    public JdbcStaticProxy(DbOperations jdbcTemplate){        this.jdbcTemplate = jdbcTemplate;    }    @Override    public void delete(int id) {        System.out.println("before delete "+id);        jdbcTemplate.delete(id);//将请求指派给委托类对象执行,这里aop的思想,在前后插入其他代码        System.out.println("after delete "+id);//通过aop的思想可以计算任务执行时间,进行权限判断,事务处理等等    }    @Override    public void insertOrUpdate(Object obj) {        //e.g. log        jdbcTemplate.insertOrUpdate(obj);        //e.g. log    }    @Override    public void find(int id) {        //e.g. log        jdbcTemplate.find(id);        //e.g. log    }}

(4) 客户类

public static void main(String[] args){    DbOperations jdbcTemplate = new JdbcTemplate();    DbOperations staticProxy = new JdbcStaticProxy(jdbcTemplate);//这里可以使用工厂方法,对客户来说透明,其并不知道给他返回的到底是代理类对象还是委托类对象    staticProxy.delete(1);//方法调用,基于抽象而不基于具体}

(5) 查看控制台输出

before delete 1delete 1after delete 1  

可以看到静态代理对象staticProxy已经成功的给delete操作加入了日志记录功能。

3.2 动态代理

首先我们再次明确一下,动态代理类的代码是在程序运行期(runtime)由jvm根据反射等机制动态生成的,因此代理类和委托类的“代理”关系或者说某种程度上的耦合关系是在运行期才确定的。

动态代理按照实现方式可进步分为jdk动态代理和cglib代理,我们暂时先忽略掉他们的区别,先来看jdk动态代理。

实现jdk动态代理必须要先了解一下java中与其紧密相关的api。

  • Proxy

    自java1.3起,根据官方文档,Proxy提供了创建动态代理类和对象的静态方法,同时它也是所有由这些静态方法创建出的动态代理类的父类。

    // 方法 1: 该方法用于获取指定代理对象所关联的调用处理器  public static InvocationHandler getInvocationHandler(Object proxy)   // 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类类型对象  public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)   // 方法 3:该方法用于判断指定类对象是否是一个动态代理类  public static boolean isProxyClass(Class<?> cl)   // 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例  public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

    通常,对于某个接口Foo,创建它的代理可以直接通过以下方式,简单明了:

    InvocationHandler handler = new MyInvocationHandler(...);Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),                                      new Class[] { Foo.class },                                       handler);

    这种方式也是我们在下面要展现的。

    • InvocationHandler

    每一个代理对象都要关联一个调用处理器,而InvocationHandler正是那个调用处理器需要实现的接口,每当代理对象的某个方法被调用时,这个方法调用都会被封装并分发到它(代理对象)关联的InvocationHandler的invoke方法。这样一来,我们可以在invoke方法中实现对委托类对象的代理访问。

    // 该方法负责集中处理动态代理类上的所有方法调用。// 第一个参数是代理类实例,第二个参数是被调用的方法对象  // 第三个参数是调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上反射执行public Object invoke(Object proxy, Method method, Object[] args)
    • ClassLoader

    这是类装载器类,负责将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。每次生成动态代理类对象时都需要指定一个类装载器对象(通常使用委托类所在的类装载器即可)

    具体的实现过程如下:

    首先编写调用处理器,InvocationHandler

    public class JdbcDynamicProxy implements InvocationHandler {    //持有代理目标对象(委托类对象)的引用    private Object target;    //对外暴露设置代理目标的方法    public void setTarget(Object target){        this.target = target;    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        Object ret;        System.out.println("before invoke,log。。");//前置逻辑        ret = method.invoke(target,args);//调用委托类对象的方法并获取返回值        System.out.println("after invoke,log。。");//后置逻辑        return ret;//返回委托类对象方法调用的返回值        }    /**     * 获取代理对象,需要类装载器,委托类实现的接口及关联的调用处理器三个参数     *     * @return     */    public Object getProxy(){        return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);    }}

    客户端代码:

     public static void main(String[] args){    DbOperations jdbcTemplate1 = new JdbcTemplate();    JdbcDynamicProxy dynamicProxy = new JdbcDynamicProxy();    dynamicProxy.setTarget(jdbcTemplate1);    DbOperations dbOperations = (DbOperations)dynamicProxy.getProxy();    dbOperations.find(1);}

    可以看到,通过new出动态代理对象,设置其代理目标并调用getProxy方法等三个步骤便可以获得一个委托类对象的代理。这里获取代理对象的过程仍显复杂(还是需要好几个步骤呢。。),可以通过创建型设计模式如工厂方法,抽象工厂等解决。

    到这里,大家也许注意到了,基于jdk的动态代理需要委托类实现接口,虽然基于抽象,面向接口编程是软件工程中的一个best practice,但很多时候事情并不能随人愿,如果委托类并没有实现任何接口呢?那么就要使用我们之前提到的cglib了。cglib与jdk动态代理不同,它通过继承创建目标类也就是委托类的子类及子类对象,复写委托类的非final方法的方式来实现代理。这种方式是不是就没有任何限制了呢?其实它的实现方式已经告诉我们了,只要委托类不是不可继承的,方法不是不可变的,也就是class定义及方法签名不含有final关键字即可。

    cglib的使用过程如下:

    public class CglibProxy implements MethodInterceptor {    /**     * 代理目标     */    Object target;    /**     * create the proxy for target object     * @param target     * @return     */    public Object createProxy(Object target){        this.target = target;        Enhancer enhancer = new Enhancer();//动态生成子类以实现方法拦截的增强器        Class clz = target.getClass();        enhancer.setSuperclass(clz);//设置父类为委托类        enhancer.setCallback(this);//设置方法回调        //not mandatory to set classloader        //enhancer.setClassLoader(target.getClass().getClassLoader());        return enhancer.create();//创建代理对象(创建了委托类的子类及子类对象)    }    /**     *     * @param o 增强的对象     * @param method 父类方法对象     * @param args 方法参数     * @param methodProxy 方法代理     * @return     * @throws Throwable     */    @Override    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {        Object result = null;        //do something with target        System.out.println("before");        result = methodProxy.invokeSuper(o,args);//invokeSuper调用的是父类(即目标类或委托类)方法        System.out.println("after");        return result;    }    public static void main(String[] args){        TestJava test = new TestJava();        TestJava tProxy = (TestJava)new CglibProxy().createProxy(test);        tProxy.sayHi();    }}    

    main方法输出:

    beforehi~after

    可以看到,通过实现方法拦截器接口MethodInterceptor,并在类的内部使用一个增强器对象Enhancer即可动态生成一个委托类的代理对象。

4. 总结与思考

4.1 静态代理

对于静态代理,因为代理类与委托类都需要实现同样的接口,因此针对每一个委托类都需要编写相应的代理类(就像委托类的影子一样),当程序规模较大,需要代理的类很多时,静态代理的弊端就凸显出来了:类数量太多(2倍),接口的一个变化会引出两倍的代码更改工作,维护复杂。

4.2 动态代理

根据实际情况,动态代理可以有不同的实现方式,基于jdk的动态代理通过生成实现了委托类实现的接口的类来创建代理,而cglib则通过生成委托类的子类,子类对象,及复写父类非final方法来创建代理。并没有哪一种方式是绝对更优,因此一些java框架如spring混合使用了这两种方式。

0 0