【设计模式】-代理模式及动态代理详解

代理模式

代理模式是一种结构性设计模式,让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。

代理模式结构

  1. 服务接口(ServiceInterface) 声明了服务接口提供的功能。代理必须遵循该接口才能伪装成对象
  2. 服务(Service)类,提供具体的一些实用的业务逻辑
  3. 代理(Proxy)类包含一个指向服务对象的引用成员变量,代理完成其交代的任务(例如延迟加载,记录日志,访问控制或者缓存等)后会将请求传递给服务对象,通常情况下,代理会对其服务对象的整个声明周期进行管理。
  4. 客户端(Client) 能通过同一接口与服务或与代理进行交互,所以你可以在一些需要服务对象的代码中实用代理。

案例分析

我们有一个常用的数据库访问接口,大量的客户端都是对数据库进行直接的访问,对系统资源的消耗特别大,并且有很多的重复查询操作。

直接访问数据库,可能会非常的慢

这时候我们考虑加入缓存,当需要重复的查询时直接从缓存中获取数据返回到客户端,节省系统开销,并记录一下每一个客户端访问花费的时间。

代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。

代理将自己伪装成数据库对象,可以在客户端不知道的情况下做缓存查询操作并记录其访问时间或日志

代码实现

定义查询数据库的接口

public interface DataService {
    // 通过ID查询数据
    String getById(Integer id);
}

具体的数据库查询业务类

public class DataServiceImpl implements DataService{

    // 模拟数据
    final Map<Integer,String> dataMap = new HashMap<Integer,String>(){{
        for (int i = 0; i < 10; i++) {
            put(i,"data_"+ i);
        }
    }};

    @Override
    public String getById(Integer id) {
        // 模拟数据库查询的耗时
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return dataMap.get(id);
    }
}

创建代理类,伪装业务类

public class DataServiceProxy implements DataService{
    DataService dataService;
    // 缓存
    Map<Integer,String> cacheMap = new HashMap<>();
    
    public DataServiceProxy(DataService dataService) {
        this.dataService = dataService;
    }

    @Override
    public String getById(Integer id) {
        // 记录访问的开始时间
        final long start = System.currentTimeMillis();
        String result = null;
        // 优先从缓存获取
        String cache = getCache(id);
        if (cache == null){
            result = dataService.getById(id);
            // 放入缓存中
            putCache(id,result);
        }else {
            result = cache;
        }
        final long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start) + "ms");
        return result;
    }

    // 缓存信息
    private void putCache(Integer id,String value){
        cacheMap.put(id,value);
    }
    // 获取缓存信息
    private String getCache(Integer id){
        return cacheMap.get(id);
    }
    
}

客户端

@Test
public void ProxyTest() {
    DataService dataService = new DataServiceImpl();
    DataServiceProxy dataServiceProxy = new DataServiceProxy(dataService);
    dataServiceProxy.getById(1);
    // 第二次查询
    dataServiceProxy.getById(1);
    dataServiceProxy.getById(1);
}

这种代理模式的设计方式,我们一般称之为静态代理:由编码人员创建完成或由特定工具生成源代码,在编译时就已经将接口、被代理类、代理类等确定类下来,在程序运行之前,代理类的字节码文件已经生成了。如果由其他的代理内容,可能需要新建很多的代码来实现。

动态代理

与静态代理最大的区别在于,动态代理类是在程序运行时创建的代理。例如在上面的例子中DataServiceProxy代理类是我们自己定义的,在程序运行之前就已经编译完成。在动态代理中,代理类不是在代码中定义,而是在程序运行时根据我们的需要在Java代码中动态生成的。

Java中我们提到动态代理,一般绕不开JDK动态代理和CGLIB动态代理。

JDK动态代理

利用JDK自带的代理类来完成,相当于利用一个拦截器(需实现接口InvocationHanlder)配合反射机制生成一个实现代理类的匿名接口,在调用具体的方法前调用InvocationHanlder来处理。

我们依旧使用DataService接口和DataServiceImpl业务类来完成一个动态代理的案例。

  1. 创建被代理类的接口和业务类(已经有了)
  2. 创建InvocationHanlder接口的实现类,在invoke方法中实现代理的逻辑
  3. 通过Proxy的静态方法newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)创建一个代理对象。
public class JDKProxy implements InvocationHandler {
    // 被代理对象
    private Object object;

    // 缓存
    Map<Integer,String> cacheMap = new HashMap<>();

    public JDKProxy(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只代理其中的查询方法
        if (method.getName().equals("getById")){
            // 参数
            Integer id = (Integer) args[0];
            // 记录访问的开始时间
            final long start = System.currentTimeMillis();
            String result = null;
            // 优先从缓存获取
            String cache = getCache(id);
            if (cache == null){
                // 代理执行
                result =(String) method.invoke(object,args);
                // 放入缓存中
                putCache(id,result);
            }else {
                result = cache;
            }
            final long end = System.currentTimeMillis();
            System.out.println("耗时:" + (end - start) + "ms");
            return result;
        }else {
            return method.invoke(object,args);
        }
    }

    // 缓存信息
    private void putCache(Integer id,String value){
        cacheMap.put(id,value);
    }
    // 获取缓存信息
    private String getCache(Integer id){
        return cacheMap.get(id);
    }
}

InvocationHandler接口详解

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

每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用,看如下invoke方法:

/**
* proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0(按次序进行,每生成一个 +1)
* method:我们所要调用某个对象真实的方法的Method对象
* args:指代代理对象方法传递的参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

Client

客户端在调用时的方式也和静态代理不一样,最终是使用代理类$Proxy来进行方法的调用

@Test
public void JDKProxyTest() {
    DataService dataService = new DataServiceImpl();
    JDKProxy jdkProxy = new JDKProxy(dataService);
    // 获取代理对象
    DataService dataServiceProxy = (DataService) Proxy.newProxyInstance(DataService.class.getClassLoader(), new Class[]{DataService.class}, jdkProxy);
    dataServiceProxy.getById(1);
    dataServiceProxy.getById(1);
}

其运行的结果是一样的,都完成了代理内容。

Proxy类详解

Proxy类就是用来创建一个代理对象的类,它提供了很多方法,我们最常用的是newProxyInstance方法。

public static Object newProxyInstance(ClassLoader loader, 
                                            Class<?>[] interfaces, 
                                            InvocationHandler h)

newProxyInstance就是创建一个代理类对象,它接收三个参数:

  • loader:指定代理类的类加载器(我们传入当前测试类的类加载器)
  • interfaces:一个interface对象数组,代理类需要实现的接口(我们传入被代理类实现的接口,这样生成的代理类和被代理类就实现了相同的接口)
  • h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,用来处理方法的调用。这里传入我们自己实现的handler

CGLIB动态代理

利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

  1. 导入cglib-xxx.jar包,这里包含了asmcglib
  2. 创建MethodInterceptor接口的实现类,在intercept方法中实现代理的逻辑
  3. 编写getCglibProxy方法(自定义)返回代理类对象

Pom导入cglb

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

重写MethodInterceptor

public class CglibProxy implements MethodInterceptor {
    // 被代理对象,便于通用,可以写成Object
    private Object object;

    // 缓存
    Map<Integer,String> cacheMap = new HashMap<>();

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 只代理其中的查询方法
        if (method.getName().equals("getById")) {
            // 参数
            Integer id = (Integer) args[0];
            // 记录访问的开始时间
            final long start = System.currentTimeMillis();
            String result = null;
            // 优先从缓存获取
            String cache = getCache(id);
            if (cache == null) {
                result = (String)method.invoke(object,args);
                // 放入缓存中
                putCache(id, result);
            } else {
                result = cache;
            }
            final long end = System.currentTimeMillis();
            System.out.println("耗时:" + (end - start) + "ms");
            return result;
        } else {
            return method.invoke(object, args);
        }
    }

    // 获取代理对象 这里采用了范型的写法,更直观的传入被代理类,然后返回代理对象
    public <T> T getCglibProxy(T t){
        this.object = t;//为目标对象target赋值
        Enhancer enhancer = new Enhancer();
        //设置父类,因为Cglib是针对指定的类生成一个子类,所以需要指定父类
        enhancer.setSuperclass(object.getClass());
        //设置回调
        enhancer.setCallback(this);
        //创建并返回代理对象
        Object result = enhancer.create();
        return (T) result;
    }

    // 缓存信息
    private void putCache(Integer id,String value){
        cacheMap.put(id,value);
    }
    // 获取缓存信息
    private String getCache(Integer id){
        return cacheMap.get(id);
    }
}

Client

@Test
public void CGLBProxyTest(){
    // 被代理类 这里可以不用接口声明哦
    DataService dataService = new DataServiceImpl();
    CglibProxy cglibProxy = new CglibProxy();
    // 获取代理对象
    DataService proxy = cglibProxy.getCglibProxy(dataService);
    proxy.getById(1);
    proxy.getById(1);
}

可以发现两种动态代理的写法基本差不多,基本的思路都是生成代理类,拦截,反射,获取真正的代理类方法,执行。那么两种方式有什么区别和用法呢?

JDK代理和CGLIB代理的区别

  1. JDK动态代理只能对实现了接口的类生成代理,而不能针对类 ,使用的是 Java反射技术实现,生成类的过程比较高效。
  2. CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 ,使用asm字节码框架实现,相关执行的过程比较高效,生成类的过程可以利用缓存弥补,因为是继承,所以该类或方法最好不要声明成final
  3. JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理
  4. CGLIB必须依赖于CGLIB的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用JDK的代理;
Tags: