JAVA中的动态代理

代理模式

       代理模式的英文叫做Proxy或Surrogate,中文都可译为“代理”,所谓代理,就是一个人或者一个机构代表另一个人或者另一个机构采取行动。在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用 。代理就是为其他对象提供一个代理以控制对某个对象的访问。比如火车票代售点就是一个代理,它控制要买火车票的人(其他对象)对火车站售票点(某个对象)的访问,由它向火车票售票点买票。

       代理模式简要图示如下:

       代理模式的优点

  • 优点一:可以隐藏真实目标类的实现;
  • 优点二:可以实现客户与真实目标类间的解耦,在不修改真实目标类代码的情况下能够做一些额外的处理

      代理模式:代理类和被代理类实现共同的接口(或继承),代理类中存有指向被代理类的索引,实际执行时通过调用代理类的方法、实际执行的是被代理类的方法

       如上图所示,Client类就是客户端,Proxy类就是代理类,RealSubject类是真实目标类,Proxy和RealSubject类为客户提供的服务能力都体现在DoAction()方法中,Proxy的DoAction()实际上是在调用RealSubject的DoAction方法,当然Proxy类的DoAction()在调用RealSubject的DoAction方法前后也可以有一些自定义的操作,比如在打印日志等,这样一个流程就是体现了简单的代理思想

       代理又可以分为静态代理和动态代理。

  • 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了
  • 动态代理:在程序运行时,运用反射机制动态创建而成,动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码

       举一个实际的例子,考虑一个字体提供功能,字体库可能源自本地磁盘、网络或者系统。 先考虑从本地磁盘中获取字体,和上面的例子一样,采用代理的方式实现,定义一个提供字体的接口FontProvider:

public interface FontProvider {
    Font getFont(String name);
}

真正提供获取磁盘字体库的类:

class FontProviderFromDisk implements FontProvider {
    @Override
    Font getFont(String name){
        System.out.println("磁盘上的字体库");
        return null;
    }
}

代理类ProxyForFont:

class ProxyForFont implements FontProvider {
    private FontProvider fontProvider;
    ProxyForFont(FontProvider fontProvider) {
        this.fontProvider=fontProvider;
    }
    @Override
    Font getFont(String name) {
        System.out.println("调用代理方法前可以做点事情");
        Font font = fontProvider.getFont(name);
        System.out.println("调用代理方法后可以再做点事情");
        return font;
    }
}

当我们需要从磁盘获取字体库时,直接调用ProxyForFont就可以了:

public class MyFontProvider {
    public static void main(String[] args) {
        FontProvider fp = new ProxyForFont(new FontProviderFromDisk());
        fp.getFont("字体库名");
    }
}

这样实现的好处在哪儿呢?比如,每次从磁盘获取字体库的时候,磁盘的I/O比较耗时,想通过缓存将读到的字体库暂存一份。此时,我们直接修改ProxyForFont类而不用去修改真正的目标类FontProviderFromDisk:

class ProxyForFont implements FontProvider {
    private FontProvider fontProvider;
    ProxyForFont(FontProvider fontProvider) {
        this.fontProvider = fontProvider;
    }
    @Override
    Font getFont(String name){
        System.out.println("检查磁盘缓存中是否存在字体库");
        if (exist) {
            Font font = fontProvider.getFont(name);
            System.out.println("将磁盘读到的字体库保存到缓存");
            return font;
        } else {
            System.out.println("如果存在直接从缓存中获取");
            return null;
        }
    }
}

       直接修改FontProviderFromDisk类的getFont方法,也能达到相同的效果,但是这样会有一个问题,上文中我们提到,字体库的获取源除了磁盘还有系统和网络等,所以还存在FontProviderFromSystem和FontProviderFromNet两个类,如果这两个类也需要缓存功能的时候,还得再继续动这两个类的getFont实现,而如果使用了代理模式,不仅能实现客户与目标的解耦,还可以在不修改真实目标类代码的情况下能够做一些额外的处理,即只需要在代理类ProxyForFont中修改即可

从静态代理到动态代理

       以上都是静态代理的实现方式,是不是感觉静态代理已经无所不能了呢?我们再来看一个需求。

       以上都是获取字体库,如果想获取图片、音乐等其他资源呢?这个时候一个FontProvider接口就不够用了,还得提供ImageProvider和MusicProvider接口,实现对应的两个功能类以及两个代理类ProxyForImage和ProxyForMusic。当要给获取图片和获取音乐等都加上缓存功能的时候,两个代理类ProxyForImage和ProxyForMusic都需要改动,而缓存的逻辑三个类又是相同的,如此写代码就会出现重复和代理类爆炸。当你要代理的方法越多时,你需要重复的逻辑就越多,假设你的目标类有100个方法,那么你的代理类就需要对这100个方法进行委托,但是又可能他们前后需要执行的逻辑时一样的,这样就会产生很多冗余。 这样,就有个更好的动态代理的方法出现了。

       动态代理也分为两类:基于接口的代理和基于继承的代理;两类实现的代表分别是:JDK代理与CGlib代理。

JDK代理

       JDK动态代理主要涉及java.lang.reflect包下的Proxy类和InvocationHandler接口。 JDK代理实现的三个要点:

  • 通过java.lang.reflect.Proxy类来动态生成代理类;
  • 代理类要实现InvocationHandler接口;
  • JDK代理只能基于接口进行动态代理;

       每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。我们来看看InvocationHandler这个接口的唯一一个方法 invoke 方法:

/**
* proxy:   指代我们所代理的那个真实对象
* method:  指代的是我们所要调用真实对象的某个方法的Method对象
* args:    指代的是调用真实对象某个方法时使用的参数
*/
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

       Proxy这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法,这个方法的作用就是得到一个动态的代理对象,其接收三个参数,我们来看看这三个参数所代表的含义。

/**
 * loader:
 * 一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象
 * 进行加载
 * interfaces:
 * 一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么
 * 接口,如果我提供了一组接口给它,这样我就能调用这组接口中的方法了
 * h:
 * 一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的
 * 时候,会关联到哪一个InvocationHandler对象上
 *
 * /
 public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException;


       JDK动态代理实例。

//subject接口,这个是jdk动态代理必须的前提。
public interface Subject {
    void request();
    void hello();
}

定义业务类,实现该接口

//目标对象RealSubject
public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("real subject execute request");
    }
    @Override
    public void hello() {
        System.out.println("hello");
    }
}

定义代理类

//代理类JdkProxySubject
public class JdkProxySubject implements InvocationHandler {

    //这个就是我们要代理的真实对象
    private Object subject;
    //构造方法,给我们要代理的真实对象赋初值
    public JdkProxySubject(Object subject) {
        this.subject = subject;
    }

    /*
    *invoke方法方法参数解析
    *Object proxy:指被代理的对象。 
    *Method method:要调用的方法 
    *Object[] args:方法调用时所需要的参数 
    *InvocationHandler接口的子类可以看成代理的最终操作类。
    */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before");
        Object result = null;
        try {
            //利用反射动态的来反射方法,这就是动态代理和静态代理的区别
            result = method.invoke(subject,args);
        } catch (Exception e) {
            System.out.println("ex:"+e.getMessage());
            throw e;
        } finally {
            System.out.println("after");
        }
        return result;
    }
}

定义客户端类,执行业务逻辑时使用代理类

//客户端Client
public class Client {
    /*
    *newProxyInstance方法参数解析
    *ClassLoader loader:类加载器 
    *Class<?>[] interfaces:得到全部的接口 
    *InvocationHandler h:得到InvocationHandler接口的子类实例 
    */
    public static void main(String[] args) {
	    //我们要代理的真实对象
        Subject realSubject = new RealSubject();
        //我们要代理哪个真实对象,就将该对象传进去
        InvocationHandler handler = new JdkProxySubject(realSubject);
        Subject subject = (Subject) Proxy.newProxyInstance(Client.class.getClassLoader(),new Class[]{Subject.class}, handler);
        subject.hello();
        subject.request();
    }
}

输出:

before
hello
after
before
real subject execute request
after

       因为利用JdkProxySubject生成的代理类实现了接口,所以目标类中所有的方法在代理类中都有。现在,如果我们在subject接口中新增加了一个goodBye()方法,然后再RealSubject中对goodBye()方法进行实现,但是在代理类中,我们不需要再去为goodBye()方法再去写一个代理方法,而是通过反射调用目标对象的方法,来动态的生成代理类。

代理的本质其实就是一种对行为的监听,对代理对象($proxy InvocationHandler)的一种监听行为。

CGlib代理模式

       CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,为一个类创建子类,并在子类中采用方法拦截的技术拦截所有对父类方法的调用,并顺势加入横切逻辑。CGlib是针对类来实现代理的,原理是对指定的业务类生成一个子类,并覆盖其中业务方法实现代理,因为采用的是继承,所以不能对final修饰的类进行代理。CGlib和JDK的原理类似,也是通过方法去反射调用目标对象的方法。

//目标对象RealSubject,cglib不需要为目标类定义接口,当然目标类实现了接口也不影响
public class RealSubject {
    public void request() {
        System.out.println("real subject execute request");
    }

    public void hello() {
        System.out.println("hello");
    }
}
//实现MethodInterceptor方法代理接口,创建代理类
public class DemoCglibProxy implements MethodInterceptor {

	private Object target;//业务类对象,供代理方法中进行真正的业务方法调用
	
    //相当于JDK动态代理中的绑定
    public Object getInstance(Object target) { 
    	//给业务对象赋值 
        this.target = target;  
        //创建加强器,用来创建动态代理类
        Enhancer enhancer = new Enhancer(); 
        //为加强器指定要代理的业务类(即:为下面生成的代理类指定父类)
        enhancer.setSuperclass(this.target.getClass());  
        //设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦
        enhancer.setCallback(this); 
       // 创建动态代理类对象并返回  
       return enhancer.create(); 
    }
    
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("before in cglib");
        Object result = null;
        try{
            result = proxy.invokeSuper(obj, args);
        }catch (Exception e){
            System.out.println("get ex:"+e.getMessage());
            throw e;
        }finally {
            System.out.println("after in cglib");
        }
        return result;
    }
}

创建业务类和代理类对象,然后通过代理类对象的getInstance(业务类对象) 返回一个动态代理类对象(它是业务类的子类,可以用业务类引用指向它),最后通过动态代理类对象进行方法调用。

//客户端
public class Client {
    public static void main(String[] args) {
	    // 此刻,realSubject不是单纯的目标类,而是增强过的目标类  
        RealSubject realSubject = ((RealSubject))new DemoCglibProxy().getInstance(new RealSubject());
        realSubject.hello();
        realSubject.request()
    }
}

输出:

before in cglib
hello
after in cglib
before in cglib
real subject execute request
after in cglib

Cglib是无需通过接口来实现,它是通过实现子类的方式来完成调用的。Enhancer对象把代理对象设置为被代理类的子类来实现动态代理的。因为是采用继承方式,所以代理类不能加final修饰,否则会报错。被final修饰的类不能被继承,内部的方法和变量都变成final类型。

Spring如何选择用JDK还是CGLib

  1. 当Bean实现接口时,Spring就会用JDK的动态代理;
  2. 当Bean没有实现接口时,Spring使用CGlib的代理实现;
  3. 可以通过修改配置文件强制使用CGlib;

      CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类和方法进行代理,因为CGLib原理是动态生成被代理类的子类。代理模式也是我们必须要理解的一种模式,因为学习好代理模式有助于我们去读一些源码,排查一些更深层次的问题,或者面对一些业务场景问题,也能有一个很大的提升,设计模式本身也就是为了解决问题而创建出来的。


参考资料:
  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值