java中的代理模式

一、代理是什么?

结合生活理解,比如,张三要租房,需要找房源(房东),然而张三现在并不知道哪里有房源,没有能力找到房东,这个时候,就出现了房屋中介,他们可以负责帮张三找到房源,其实房屋中介就是一种代理。
再比如,张三想吃外卖了,又不想去饭店取餐,那么就可以点外卖,让外卖小哥去取餐,给张三送过来,这里,外卖小哥就是代理。

在程序开发中,也会有这样的情况:A类需要调用C类中的方法来完成某些功能,但是C类不让A调用。这个时候,可以尝试创建一个B类,让B可以访问C类,A又可以访问B类,B就是一个代理。
A通过代理可以访问C.png

代理模式,Proxy Pattern,是程序设计的一种设计模式。为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象(即客户端)不适合或者不能直接引用另一个对象(即目标对象),而代理类(或代理对象)可以在客户端和目标类(或目标对象)之间起到中介的作用。

在程序开发中我们经常强调“解耦”和“高扩展性”,代理模式其实就考虑到了这些,在不修改原对象代码的基础上,对原对象的功能进行修改或者增强。使用代理模式主要有两个作用:

  1. 功能增强:在客户类原有的功能上,不修改客户类,由代理增加额外的功能,新增加的功能,就叫做功能增强。
  2. 控制访问:通过代理类控制,不让客户类访问目标对象。

二、实现代理的方式

java中的代理主要有两种:静态代理和动态代理。而动态创建代理对象的方法又可以分为:基于JDK(接口)的动态代理和基于CGLIB(继承)的动态代理。
1.静态代理:(1)代理类是我们自己手动创建实现的;(2)而且目标类是固定的
静态代理,可以通过继承的方式来实现,也可以通过接口的方式来实现。下面通过顾客Customer让外卖小哥DeliveryClerk跑腿取餐的例子来用代码说明。
继承的方式:

/*
 *顾客,想吃饭,得点餐,有点餐的方法order()
 */
package com.wjun.proxy.staticdemo;
public class Customer {
    public String order(String food) {
        return "下单成功,点了:"+ food;
    }
}

/*
 *外卖小哥,得知道点了什么才能取餐派送,通过继承的方式重写order方法
 */
package com.wjun.proxy.staticdemo;
public class DeliveryClerk extends Customer {
    @Override
    public String order(String food) {
        String result = super.order(food);//目标方法的调用
        System.out.println("外卖小哥已收到订单,正在取餐途中...");//功能增强
        System.out.println("已经取餐,正在派送...");//功能增强
        return result+",餐盒已消毒!";//功能增强
    }
}

/*
 *测试类
 */
package com.wjun.proxy.staticdemo;
public class StaticDemoTest {
    public static void main(String[] args) {
        //顾客想吃饭点了餐又不想去取,让外卖小哥取餐,派送,餐盒消毒
        Customer customer = new DeliveryClerk();
        String result = customer.order("麻辣鱼头");
        System.out.println(result);
    }
}

接口的方式:

/*
 *点餐这件事,通过接口来定义
 */
package com.wjun.proxy.staticdemo;
public interface OrderInterface {
    public String order(String food);
}

/*
 *顾客,实现点餐的方法
 */
package com.wjun.proxy.staticdemo;
public class Customer implements OrderInterface{
    @Override
    public String order(String food) {
        return "下单成功,点了:"+ food;
    }
}

/*
 *外卖小哥去取餐
 */
package com.wjun.proxy.staticdemo;
public class DeliveryClerk2 implements OrderInterface{
    //必须知道要做什么事。把原来的对象传入,并保存到成员位置,也就是目标类对象
    private OrderInterface source;

    public DeliveryClerk2(OrderInterface source) {
        this.source = source;
    }

    @Override
    public String order(String food) {
        String result = source.order(food);//目标方法调用
        System.out.println("外卖小哥已收到订单,正在取餐途中...");//增强功能
        System.out.println("已经取餐,正在派送...");//增强功能
        return result+",餐盒已消毒!";//增强功能
    }
}

/*
 *测试类
 */
package com.wjun.proxy.staticdemo;
public class StaticDemoTest {
    public static void main(String[] args) {
        Customer customer = new Customer();
        //创建代理对象,也就是外卖小哥对象
        OrderInterface deliveryClerk = new DeliveryClerk2(customer);
        //调用代理对象的方法,可以看到增强之后的效果
        String result = deliveryClerk.order("红烧肉");
        System.out.println(result);
    }
}

代码中,代理类中做了两件事,目标方法的调用和功能增强。在目标类的方法调用之后,做的其他功能,都是增强的意思。
静态代理的缺点:
静态代理虽然能够实现我们所说的代理模式,完成了解耦,但是静态代理的代码维护依旧非常复杂。当目标类增加了,代理类可能也需要成倍增加,代理类数量过多;而且,一旦接口或父类发生了变动,则代理类的代码就得随之做出修改,代理类多的时候维护比较麻烦。所以在实际开发的时候,一般使用动态代理。

2.动态代理
在静态代理中目标类很多的时候,可以使用动态代理,避免静态代理的缺点。动态代理中目标类即使很多,代理类数量可以很少,当修改了接口中的方法时,不会影响代理类。
动态代理,是在内存中生成代理对象的一种技术。也就是整个代理过程在内存中进行,我们不需要手写代理类的代码,也不会存在代理类编译的过程,而是直接在运行期,在JVM中“凭空”创造出一个代理类对象供我们使用。
动态代理是一种创建java对象的能力,让你不用创建上例中的DeliveryClerk对象,就能创建代理类对象。
(1)基于JDK(接口)的动态代理
在程序的执行过程中,使用jdk的反射机制,创建代理类对象,并动态的指定要代理目标类。
jdk自带的动态代理技术,需要使用一个静态方法即Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h),来创建代理对象。它要求被代理对象,也就是目标类,必须实现接口。生成的代理对象和原对象都实现相同的接口,是兄弟关系。
依然借助外卖小哥取餐的例子:

/**
 * 定义接口
 */
package com.wjun.proxy.jdk;
public interface OrderInterface {
    public String order(String foodName);
    //接口中可能不止一个方法
    public void test();
    public void test2();

}

/**
 * 顾客类
 */
package com.wjun.proxy.jdk;
public class Customer implements OrderInterface {
    @Override
    public String order(String food) {
        return "下单成功,点了:"+ food;
    }

    @Override
    public void test() {
        System.out.println("我是test");
    }

    @Override
    public void test2() {
    }
}

/**
 * 测试类
 */
package com.wjun.proxy.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicTest {
    public static void main(String[] args) {
        //准备一个目标类对象,也就是顾客对象
        Customer customer = new Customer();
        //使用jdk的API,动态生成一个代理对象
        OrderInterface deliveryClerk = (OrderInterface) Proxy.newProxyInstance(
                customer.getClass().getClassLoader(),
                customer.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //System.out.println("你调用了"+method.getName()+",执行的实际是我invoke"+",args是"+args[0]);
                        if ("order".equals(method.getName())) {
                            Object result = method.invoke(customer,args);
                            System.out.println("外卖小哥已收到订单,正在取餐途中...");
                            System.out.println("已经取餐,正在派送...");
                            return result+",餐盒已消毒!";
                        } else {
                            return method.invoke(customer, args);//使用method反射调用,在目标对象中执行该方法,并不修改其逻辑
                            //也就是说原封不动的调用原来的逻辑
                        }
                    }
                }
        );
        //调用代理对象,执行对应方法
        String result = deliveryClerk.order("红烧肘子");
        System.out.println(result);
        //deliveryClerk.test();
    }
}
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
主要关注参数列表:
1. ClassLoader loader:
固定写法,指定目标类对象的类加载器即可。
用于加载目标类及其接口的字节码文件。
通常,使用目标类的字节码对象调用getClassLoader()方法即可得到。
2. Class<?>[] interfaces:
固定写法,指定目标类的实现的所有接口的字节码对象的数组。
通常,使用目标类的字节码对象调用getInterfaces()方法即可得到。
3. InvocationHandler h:
这个参数是一个接口,主要关注它里面的唯一一个方法,invoke:
    Object invoke(Object proxy, Method method, Object[] args)
    这个invoke方法会在代理类对象调用方法时执行。
    也就是说,我们在代理类对象中调用任何接口中的方法时,实际上都是会执行invoke;
    所以,我们在此方法中完成对增强或扩展代码逻辑的编写。
proxy:就是代理类对象的一个引用,也就是Proxy.newProxyInstance的返回值,
       此引用几乎不会用到,忽略即可。
method:对应的是触发invoke执行的方法的Method对象。  
        假如我们调用了xxx方法,该方法出发了invoke的执行,
        那么,method就是xxx方法对应的反射对象(Method对象)
args:代理对象调用方法时传递的实际参数

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)究竟如何完成动态代理的呢?底层是如何实现的?我们来自己模拟实现一下:

package com.wjun.proxy.jdk.explain;
import com.wjun.proxy.jdk.OrderInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
 * 此处我们模拟一下Proxy.newProxyInstance()中做的事情,从底层看一看到底jdk如何完成的动态代理
 * Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
 */
public class DeliveryClerk implements OrderInterface {
    //接收外部传递过来的InvocationHandler对象
    private final InvocationHandler handler;
    public DeliveryClerk(InvocationHandler handler) {
        this.handler = handler;
    }
    @Override
    public String order(String food) {
        //每个方法的实现,实际上并没有做其他的事情,而是直接调用了InvocationHandler中的invoke方法
        try {
            //调用的是order方法,则反射获取order对应的method对象,传入invoke中
            Method method = OrderInterface.class.getMethod("order", String.class);
            //调用InvocationHandler中的invoke方法
            Object result = handler.invoke(this, method, new Object[]{food});
            //将返回值返回
            return (String) result;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }

    @Override
    public void test() {
        //每个方法的实现,实际上并没有做其他的事情,而是直接调用了InvocationHandler中的invoke方法
        try {
            //调用的是test方法,则反射获取test对应的method对象,传入invoke中
            Method method = OrderInterface.class.getMethod("test");
            //调用InvocationHandler中的invoke方法
            Object result = handler.invoke(this, method, null);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    @Override
    public void test2() {
        //每个方法的实现,实际上并没有做其他的事情,而是直接调用了InvocationHandler中的invoke方法
        try {
            //调用的是test2方法,则反射获取test2对应的method对象,传入invoke中
            Method method = OrderInterface.class.getMethod("test2");
            //调用InvocationHandler中的invoke方法
            Object result = handler.invoke(this, method, null);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

上面这部分代码就是动态代理在内存中做的事情,当然忽略了一些安全检查的细节。用下面的代码测试一下,发现结果跟之前是一样的:

package com.wjun.proxy.jdk.explain;
import com.wjun.proxy.jdk.Customer;
import com.wjun.proxy.jdk.OrderInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicTest2 {
    public static void main(String[] args) {
        //准备一个目标类对象,也就是顾客对象
        Customer customer = new Customer();
        //把InvocationHandler的定义抽取出来
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //System.out.println("你调用了"+method.getName()+",执行的实际是我invoke"+",args是"+args[0]);
                if ("order".equals(method.getName())) {
                    Object result = method.invoke(customer, args);
                    System.out.println("外卖小哥已收到订单,正在取餐途中...");
                    System.out.println("已经取餐,正在派送...");
                    return result + ",餐盒已消毒!";
                } else {
                    return method.invoke(customer, args);//使用method反射调用,在目标对象中执行该方法,并不修改其逻辑
                    //也就是说原封不动的调用原来的逻辑
                }

            }
        };
        OrderInterface deliveryClerk = new DeliveryClerk(handler);
        //调用代理对象,执行对应方法
        String result = deliveryClerk.order("红烧肘子");
        System.out.println(result);
        //deliveryClerk.test();
    }
}

基于接口的动态代理,实际上是在内存中生成了一个对象,该对象实现了指定的目标类对象拥有的接口。所以代理类对象和目标类对象是兄弟关系。
这里说明一下,兄弟关系,是并列的关系,不能互相转换,包容性比较差。在spring框架中,如果配置jdk的动态代理方式,一定要用接口类型接受代理类。
(2)cglib的动态代理
cglib时第三方的工具库,创建代理对象。原理是继承,cglib通过继承目标类,创建它的子类,在子类中重写父类中同名的方法,实现功能的修改。它不要求目标类实现接口,但是要求目标类不能是最终类,也就是不能被final修饰。因为CGLIB是基于目标类生成该类的一个子类作为代理类,所以目标类必须可被继承。

Enchancer.create(Class type, Callback callback)
主要参数列表:
Class type:
    指定我们要代理的目标类的字节码对象,也就是指定目标类的类型
Callback callback:
    Callback是一个接口,由于该接口只是一个名称定义的作用,并不包含方法的声明。
    所以,我们使用时通常使用它的一个子接口MethodInterceptor,(方法拦截器)
    而MethodInterceptor接口中也只有一个方法——intercept。

Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)的解释:

proxy:就是代理类对象的一个引用,也就是Enchancer.create的返回值,
       此引用几乎不会用到,忽略即可。
method:对应的是触发intercept执行的方法的Method对象。  
        假如我们调用了xxx方法,该方法出发了intercept的执行,
        那么,method就是xxx方法对应的反射对象(Method对象)
args:代理对象调用方法时传递的实际参数
methodProxy:方法的代理对象,一般也不做处理,可以暂时忽略

代码示例:

/**
 * 顾客类
 */
package com.wjun.proxy.cglib;
public class Customer {
    public String order(String food) {
        return "已经下单"+food;
    }

    public void test() {
        System.out.println("我是test");
    }

    public void test2() {}
}

/**
 * 测试类
 */
package com.wjun.proxy.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class DynamicTest {
    public static void main(String[] args) {
        //创建一个目标类对象,也就是顾客对象
        Customer customer = new Customer();
        //使用CGLIB创建代理对象
        Customer deliveryClerk = (Customer) Enhancer.create(customer.getClass(), new MethodInterceptor() {
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //判断,如果是order方法就增强
                if ("order".equals(method.getName())) {
                    Object result = method.invoke(customer, args);
                    System.out.println("外卖小哥已收到订单,正在取餐途中...");
                    System.out.println("已经取餐,正在派送...");
                    return result+",餐盒已消毒!";
                }else {
                    return method.invoke(customer, args);//原封不动调用原来的逻辑,反射调用
                }
            }
        }
    );
        //调用代理类的方法
        String result = deliveryClerk.order("鱼香肉丝");
        System.out.println(result);
    }
}

内存中是如何创建代理对象的?我们来模拟一下,看下面代码

package com.wjun.proxy.cglib.explain;
import com.wjun.proxy.cglib.Customer;
import net.sf.cglib.proxy.MethodInterceptor;
import java.lang.reflect.Method;
/**
 * 模拟在内存中调用CGLIB的方法Enchancer.create后,该方法内部写了一个什么样的代理类
 * Enchancer.create(Class type, Callback callback)
 */
public class DeliveryClerk extends Customer {
    //保存方法拦截器
    private final MethodInterceptor methodInterceptor;

    public DeliveryClerk(MethodInterceptor methodInterceptor) {
        this.methodInterceptor = methodInterceptor;
    }

    @Override
    public String order(String food) {
        try {
            Method method = Customer.class.getMethod("order", String.class);
            Object result = methodInterceptor.intercept(this, method, new Object[]{food}, null);
            return (String) result;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        return super.order(food);
    }

    @Override
    public void test() {
        try {
            Method method = Customer.class.getMethod("test", String.class);
            Object result = methodInterceptor.intercept(this, method, null, null);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    @Override
    public void test2() {
        try {
            Method method = Customer.class.getMethod("test2", String.class);
            Object result = methodInterceptor.intercept(this, method, null, null);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

基于父类的动态代理,是在内存中生成了一个对象,该对象继承了原对象(目标类对象)。所以代理类对象实际上是目标类对象的儿子。
由于是父子关系,所以,代理类对象是可以用父类的引用接收的。

三、小结

代理模式在java开发中是广泛应用的,特别是在框架中低层原理经常涉及到代理模式(尤其是动态代理)。
静态代理和动态代理,实际使用时还是动态代理使用的比较多。原因就是静态代理需要自己手写代码,维护、修改非常繁杂,会额外引入很多工作量。也不能很好的使用配置完成逻辑的指定,所以使用比较少。
基于jdk和基于cdlib,实际使用时两个都会用到。

  1. 在spring中,默认情况下它就支持了两种动态代理模式,如果你指定的目标类实现了接口,spring就会自动选择jdk的动态代理,而如果目标类没有实现接口,则spring就会使用cglib。
  2. 我们在开发时,由于基于jdk的动态代理要求比较多,更不容易实现,所以很多人习惯于同一配置为使用CGLIB进行代理。也就是CGLIB更为通用。
  3. 如果使用dubbo+zookeeper,底层进行代理时,最好配置定死使用CGLIB的方式进行处理。因为dubbo会使用基于包名的扫描方式进行类的处理,而jdk的代理类生成的包名类似于com.sun.proxy....格式。我们实际需要让代理类和目标类保持一样的包名。所以只有CGLIB能保持原包名不变生成代理类。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容