一文看懂Java的动态代理

作者: 好重 | 来源:发表于2019-07-27 16:41 被阅读22次

    前言

    什么是代理?

    首先,代理是一种基本的设计模式。

    它的形式是,用代理对象代替实际对象,从而提供额外的或完全不同的操作

    举个例子,我们购买火车票,12306官网有火车票下单的操作,某抢票APP也有火车票下单的操作,我们可以在官网下单,也可以在第三方抢票APP上下单。

    那么,类比到代理设计模式,12306官网就是实际对象,而第三方抢票APP就是代理对象,这个第三方APP实际上还是调用12306官网来完成下单,但它提供了一些额外的操作,比如在用户购买完火车票后推荐一些酒店等。

    一般情况下,代理对象和实际对象都实现同一个接口,调用方仅仅操作接口,而不知道处理的是代理对象还是真正的对象。

    同样是购买火车票的例子,用户购买的时候可以使用实际对象,即官网OfficialWeb12306,也可以使用代理对象比如高铁管家GaoTieGuanJia,这些对于用户来说都是透明的,用户关心的只有TicketSystem,只要能用来购买火车票就行。

    GaoTieGuanJia的内部实际上是调用OfficialWeb12306来完成实际购票的,可以看以下示例代码:

    /** 购票系统 **/
    interface TicketSystem {
        void buyTicket();
    }
    
    /** 12306官网 **/
    class OfficialWeb12306 implements TicketSystem {
        @Override
        public void buyTicket() {
            System.out.println("BuyTicket from 12306.");
        }
    }
    
    /* 第三方抢票app,高铁管家 */
    class GaoTieGuanJia implements TicketSystem {
        private TicketSystem ticketSystem;
        GaoTieGuanJia() {
            this.ticketSystem = new OfficialWeb12306();
        }
        @Override
        public void buyTicket() {
            ticketSystem.buyTicket();
            System.out.println("Recommend some hotels to users.");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            TicketSystem officialWeb12306 = new OfficialWeb12306();
            buyTrainTicket(officialWeb12306);
            
            TicketSystem gaoTieGuanJia = new GaoTieGuanJia();
            buyTrainTicket(gaoTieGuanJia);
        }
        /* 用户进行购票 */
        private static void buyTrainTicket(TicketSystem ticketSystem) {
            System.out.println("------ begin buy ticket ------");
            ticketSystem.buyTicket();
            System.out.println("------ end buy ticket ------\n");
        }
    }
    

    输出是:
    ------ begin buy ticket ------
    BuyTicket from 12306.
    ------ end buy ticket ------

    ------ begin buy ticket ------
    BuyTicket from 12306.
    Recommend some hotels to users.
    ------ end buy ticket ------

    动态代理

    Java的动态代理比代理的思想更进了一步,它可以动态地创建代理,并动态地处理对所代理方法的调用。

    在动态代理中,我们无需显示地声明一个实现特定接口的类来实例化代理对象,它的代理对象是直接通过工厂方法创建出来的,我们不需要关注这个代理对象的具体类型,只需知道在这个代理对象上的方法调用将会被重定向到单一的调用处理器上即可,这个调用处理器就是InvocationHandler

    好了,同样是购买火车票的例子,同样会有一个购票系统的接口:

    interface TicketSystem {
        void buyTicket();
    }
    

    以及现有的官方的购票方式,

    class OfficialWeb12306 implements TicketSystem {
        @Override
        public void buyTicket() {
            System.out.println("BuyTicket from 12306.");
        }
    }
    

    我们现在可以动态地创建第三方购票方式,而不需要静态地声明一个类去实现TicketSystem了,但在此之前,我们需要明确用户调用第三方TicketSystem中的buyTicket()时需要做什么事情,在这里无非就是
    1)调用官网的购票方法
    2)推荐一些酒店给用户
    那么动态代理要求我们定义一个InvocationHandler去做这些事情:

    class ThirdPartyTicketHandler implements InvocationHandler {
        private TicketSystem ticketSystem;
        ThirdPartyTicketHandler() {
            ticketSystem = new OfficialWeb12306();
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if("buyTicket".equals(method.getName())) {
                System.out.println("We are using dynamic proxy to buy ticket!");
                ticketSystem.buyTicket();
                System.out.println("Recommend some hotels to users.");
            }
            return null; // 因为是void方法,所以直接返回null即可
        }
    }
    

    其中,invoke(...)方法的各个参数和返回值是这样的:
    1)proxy是代理对象,也就是到时传入buyTicket()方法的对象,因为不同代理对象可以共用一个InvocationHandler
    2)method是在代理对象上调用的方法
    3)args是调用方法传入的参数
    4)返回值是调用方法的返回值
    然后,我们可以动态创建代理对象来完成购票操作了:

    public class Main {
        public static void main(String[] args) {
            TicketSystem officialWeb12306 = new OfficialWeb12306();
            buyTrainTicket(officialWeb12306);
    
            /* 使用刚刚创建的InvocationHandler,从而动态地创建代理对象 */
            TicketSystem thirdPartyTicketApp = (TicketSystem) Proxy.newProxyInstance(
                    TicketSystem.class.getClassLoader(),
                    new Class<?>[]{TicketSystem.class},
                    new ThirdPartyTicketHandler());
            buyTrainTicket(thirdPartyTicketApp);
        }
        private static void buyTrainTicket(TicketSystem ticketSystem) {
            System.out.println("------ begin buy ticket ------");
            ticketSystem.buyTicket();
            System.out.println("------ end buy ticket ------\n");
        }
    }
    

    这里使用刚刚创建的InvocationHandler,动态地创建了代理对象,再将这个对象传入buyTrainTicket方法,这个方法只关心TicketSystem,并不知道传入的是什么类型。

    在创建代理对象时,使用的工厂方法是Proxy.newProxyInstance(...),需要传入要实现的接口的类加载器,以及接口的列表,最后是一个调用处理器。接口的列表可以有多个,因此在调用处理器中可以根据方法的名字来进行不同的处理。

    上述代码的输出是:
    ------ begin buy ticket ------
    BuyTicket from 12306.
    ------ end buy ticket ------

    ------ begin buy ticket ------
    We are using dynamic proxy to buy ticket!
    BuyTicket from 12306.
    Recommend some hotels to users.
    ------ end buy ticket ------

    动态代理的使用场景:

    那么,为什么要使用动态代理呢?什么场景下会用到动态代理呢?我认为以下这些场景可以使用动态代理:
    1. 避免实现过多接口的方法
    有些接口有大量的方法,比如Android操作数据库时用的Cursor接口,如果用常规的方法实现这个接口,不可避免要声明每个方法,比较繁琐,这时就可以用动态代理了,在InvocationHandler里边对感兴趣的方法进行处理,其它的方法则调用实际对象对应的方法即可。
    2. 代理不可见的类
    有些接口是不可见的,比如一些第三方库中的内部接口,我们无法显式地声明实现这些接口的类,因此可以结合反射和动态代理去实现代理的操作。
    3. Java的AOP
    AOP的源码中用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。
    4. 访问控制(access control)
    比如权限管理、认证、懒加载(Hibernate, Mybatis)等。

    动态代理也有一些局限
    JDK动态代理的代理类字节码在创建时,需要实现业务实现类所实现的接口作为参数。如果业务实现类是没有实现接口而是直接定义业务方法的话,就无法使用JDK动态代理了。或者,如果业务实现类中新增了接口中没有的方法,这些方法也是无法被代理的。

    这种情况下就需要使用基于CGlib的动态代理技术,这个后续再来研究。

    参考文献

    [1] 太好了!总算有人把动态代理、CGlib、AOP都说清楚了!
    [2] what are dynamic proxy classes and why would i use one

    本文完。


    csdn:https://blog.csdn.net/chz429/article/details/97528781
    版权声明:本文为博主原创文章,转载请附上博文链接!

    相关文章

      网友评论

        本文标题:一文看懂Java的动态代理

        本文链接:https://www.haomeiwen.com/subject/chperctx.html