Java动态代理

1. 絮絮叨叨

  • 最近,小组同事做代码改造时,使用到了动态代理
  • 自己阅读时,发现对代理这种设计模式都不怎么清楚,导致理解代码也很困难
  • 自己唯一能看懂的,大概就是handler中的invoke()方法,是真正实现代理的地方
  • invoke()方法,传入了被代理的方法methodA,通过Java反射机制methodA.invoke()调用被代理对象的methodA方法

2. 代理设计模式

2.1 什么是代理

  • 考虑真实的编程场景,项目中存在一个访问其他数据源的接口,包含一个query()方法

  • 我们已经针对这个接口,实现了MySQL、Hive、HBase、MongoDB等作为数据源的实现类

  • 但是,在测试过程中,我们发现这些数据源的查询并不是很稳定

  • 最原始的想法: 在所有实现类query()方法中,代码头部获取startTime,代码尾部获取endTime,打印查询耗时

    long startTime = System.currentTimeMillis();
    logger.info("query mysql start:" + new Date(startTime).toLocaleString());
    // 具体的query代码
    ...
    
    long endTime = System.currentTimeMillis();
    logger.info(String.format("query mysql end: %s, consumed time: %dms", new Date(endTime).toLocaleString(), (endTime-startTime)));
    
  • 问题一:

    • 现在只是打印查询耗时,后续可能需要做SQL安全检测、查询重试等。
    • 越来越多的辅助性需求,使得query()方法变得越来越臃肿,实际上query()方法的核心逻辑就是执行查询
  • 问题二:

    • 同时,除了query()方法有耗时打印需求外,其他的方法也可能有类似需求。
    • 如果能将这个功能抽象出来,实现一个自动打印耗时的功能,这样可以极大地减少代码开发的工作量
  • 进阶想法: 为每个实现类创建一个包装类

    • 这个包装类与具体的实现类一样,实现了相同的接口。
    • 在query()方法中,直接调用实现类的query()方法,并在调用前后进行耗时打印
    • 对实现类query()方法的调用,都改成对包装类query()方法的调用
    // 包装类内含一个target对象,存储真实的数据源,如MySQL
    private Coonector target;
    
    // 包装类的query()方法
    long startTime = System.currentTimeMillis();
    logger.info("query mysql start:" + new Date(startTime).toLocaleString());
    // 使用try-finally
    JSONObject[] data = null;
    try {
        data = target.query();
        return data;
    } catch (Exception exception) {
        throw exception;
    } finally {
        long endTime = System.currentTimeMillis();
        logger.info(String.format("query mysql end: %s, consumed time: %dms", new Date(endTime).toLocaleString(), (endTime - startTime)));
    }
    
  • 这时,代理模式的概念就变得非常清晰了:不直接调用实现类的某个方法,而是通过实现类的代理去调用。

  • 这样不仅可以实现调用者与被调用者之间的解耦合,还可以在不修改调用者的情况下,丰富功能逻辑

  • 这是一个渣渣的理解,只能理解这么多。 😂 如果有更好的理解,欢迎私信或者评论交流~

2.2 代理模式入门

  • 代理模式的UML图如下
    1. subject: 抽象主题角色,是一个接口,定义了一系列的公共对外方法
    2. real subject: 真实主题角色,也就是我刚刚提到的实现类,又称委托类。委托类实现抽象主题,负责实现具体的业务逻辑
    3. proxy: 代理主题角色,简称代理类。它也实现了抽象主题,用于代理、封装,甚至增强委托类。
      一般通过内含委托类,实现对委托类的封装
    4. client: 当访问具体的业务逻辑时,clinet表面是访问代理类,而实际是访问被代理类封装的委托类
      在这里插入图片描述
  • 代理模式的应用场景:目前,就我本人所接触的使用场景,就是通过代理去打印日志、增强业务逻辑 😂

3. Java代理的三种实现

3.1 静态代理

  • 所谓的静态动态,是相对于字节码的生成时机来说的:
    • 静态是指字节码,即class文件,在编译时就已经生成。
    • 动态是指字节码在运行时动态生成,而不是编译时提前生成
  • 使用包装类实现耗时打印的进阶方法,实际就是静态代理。
  • 通过为每个委托类创建对应的代理类,然后编译时就可以得到代理类的字节码

3.1.1 静态代理实例

  • 抽象主题:

    public interface Animal {
       void eat();
    }
    
  • 委托类:Dog和Cat,实现了抽象接口

    public class Dog implements Animal {
        @Override
        public void eat() {
            System.out.println("I like eating bone");
        }
    }
    
    public class Cat implements Animal {
        @Override
        public void eat() {
            System.out.println("I like eating fish");
        }
    }
    
  • 代理类:代理类中含有对应的委托类,通过调用委托类的具体实现,来封装委托类

    public class DogProxy implements Animal {
        private Dog dog;
    
        public DogProxy(Dog dog) {
            this.dog = dog;
        }
    
        @Override
        public void eat() {
            System.out.print("I'm a "+dog.getClass().getSimpleName() +". ");
            dog.eat();
        }
    }
    
    public class CatProxy implements Animal {
        private Cat cat;
    
        public CatProxy(Cat cat) {
            this.cat = cat;
        }
    
        @Override
        public void eat() {
            System.out.print("I'm a " + cat.getClass().getSimpleName()+". ");
            cat.eat();
        }
    }
    

3.1.2 静态代理的缺点

  • 静态代理虽然实现简单、不更改原始的业务逻辑,但是仍然存在以下缺点:
    • 如果存在多个委托类,则需要创建多个代理类,这样则会产生过多的代理类
    • 如果抽象主题增加、删除、修改方法时,委托类和代理类都需要同时修改,不易维护

3.2 Java自带的动态代理

3.2.1 代理类、中介类、委托类

  • 如果能在程序运行时,动态生成对应的代理类,则无需创建并维护过多的代理类
  • 使用Java自带的java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口,可以实现动态代理
  • 从类的全路径可以看出,Java的动态代理实际利用的是反射机制实现的
    • Proxy用于创建对应接口的代理类,创建代理类时会设置中介类
    • 中介类就是实现InvocationHandler接口的类,是一个分发方法调用的调用程序(invocation handler)
    • 中介类中会包含一个具体的委托类被代理类)。代理类具体代理哪个委托类,是由中介类决定的
  • 代理类、委托类、中介类之间的关系如下
    • 通过 Proxy.newProxyInstance() 创建某个接口的代理类,并为代理类设置中介类
    • 调用代理类的 methodA() 方法时,实际会先调用中介类的 invoke() 方法
    • 在中介类的 invoke() 方法中,可以通过反射调用委托类的 methodA() 方法
    • 可以说,中介类实现了代理类和委托类之间的解耦,是将二者联系起来的纽带

3.2.2 代码示例

  1. 创建抽象主题 —— 与静态代理类一致,不再展示

  2. 创建实现类,即委托类 —— 与静态代理类一致,不再展示

  3. 实现InvocationHandler接口,创建中介类

    public class AnimalInvokeHandler implements InvocationHandler {
        private Animal animal;
    
        public AnimalInvokeHandler(Animal animal) {
            this.animal = animal;
        }
    
    	@Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.printf("%s(proxy class): \n\tinterfaces implemented: %s\n\tclass extended: %s\n", proxy.getClass().getName(),
                    Arrays.toString(proxy.getClass().getInterfaces()), proxy.getClass().getSuperclass());
            System.out.printf("proxy is instanceof Animal: %b \n", proxy instanceof Animal);
            System.out.printf("---- Call method %s() in %s ----\n", method.getName(), animal.getClass().getSimpleName());
            // 通过反射,调用委托类的方法
            Object result = method.invoke(animal, args);
            return result;
        }
    }
    
  4. 通过Proxy创建动态代理类,实现对抽象主题的代理

    public static void main(String[] args) {
    	// 获取代理类的类加载器
    	ClassLoader loader = Main.class.getClassLoader();
    	// 获取委托类的接口,这些接口是代理类需要实现的;这里的委托类为Dog
    	Animal dog = new Dog();
        Class<?>[] interfaces = dog.getClass().getInterfaces();
    	// 创建中介类
    	InvocationHandler handler = new AnimalInvokeHandler(dog);
        // 通过Proxy.newProxyInstance创建代理类
        Animal proxy = (Animal) Proxy.newProxyInstance(loader, interfaces, handler);
        proxy.eat();
    }
    
  • 执行结果如下:
  • Java原生的动态代理,利用反射动态生成代理类字节码ProxyX.classX表示序号,如0),然后将其强制转化为抽象主题类型,就能实现对该接口的代理

3.2.3 JDK动态代理的优缺点

  • Java原生动态代理,又叫JDK动态代理,具有以下优缺点
    • 优点: JDK动态代理,避免了静态代理需要创建并维护过多的代理类的问题
    • 缺点: 因为Java的单继承原则,JDK动态代理只能代理接口:代理类本身已经继承了Proxy类,就不能再继承其他类,只能实现委托类的抽象主题接口。

3.3 cglib实现动态代理

  • JDK动态代理存在只能代理接口的问题,是十分不方便的。
  • 考虑以下场景:
    • 一个类中有两个方法,methodA:转账到其他账户,methodB:查询账户余额。
    • 如果用户访问methodA,希望先提示用户检查账户信息是否正确;
    • 如果用户访问methodB,希望在用户查询完余额后,提示用户关注银行的微信公众号。
  • 上述类已经成功用于业务场景了,我们想要实现这些增强功能,最好不要更改其原始代码,而是通过代理实现功能增强
  • 这时,便可以使用cglib实现对类的动态代理 —— 小白看了实现后,可能会倾向于说这就是类似Java web的拦截器 😂

3.3.1 cglib动态代理的实现

  1. 添加maven依赖

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.2.11</version>
    </dependency>
    
  2. 简化版的银行系统类

    public class BankSystem {
        // 转账
        public boolean transferAccount(double amount, String address) {
            System.out.printf("Send %.2f dollars to account %s\n", amount, address);
            // 转账成功
            return true;
        }
    
        // 查询账户余额
        public void queryBalance(){
            System.out.printf("Query account balance success, 2400.00 dollars reserved\n");
        }
    }
    
  3. 为转账方法创建拦截器

    public class TransferInterceptor implements MethodInterceptor {
        /**
        *
        * @param o,表示要增强的对象,其实就是代理中的委托类
        * @param method,被拦截的方法,其实就是委托类中被代理的方法
        * @param objects,被拦截方法的入参,如果是基本数据类型需要传入包装类型
        * @param methodProxy,对method的代理,通过invokeSuper(),实现对method的调用
        * @return java.lang.Object
        * @author sunrise
        * @date 2021/5/23 10:43 上午
        **/
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            before(objects);
            Object result = methodProxy.invokeSuper(o, objects);
            return result;
        }
    
        private void before(Object[] args){
            System.out.printf("Please check: you will send %.2f dollar to account %s.\n", args[0], args[1]);
        }
    }
    
  4. 为余额查询方法创建拦截器

    public class QueryBalanceInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            // 调用预发插叙方法
            Object result = proxy.invokeSuper(obj, args);
            after();
            return result;
        }
    
        private void after() {
            System.out.printf("Please pay attention to WeChat official account.\n");
        }
    }
    
  5. 创建filter,实现方法与拦截器的映射

    public class BankFilter implements CallbackFilter {
        @Override
        public int accept(Method method) {
            if ("transferAccount".equals(method.getName())) {
                // 使用拦截器列表中的第1个拦截器
                return 0;
            }
            // 使用拦截器列表中的第2个拦截器
            return 1;
        }
    }
    
  6. 使用cglib实现动态代理 —— 我认为就是方法拦截 😂

    public static void main(String[] args) {
        // 新建拦截器
        TransferInterceptor transferInterceptor = new TransferInterceptor();
        QueryBalanceInterceptor queryBalanceInterceptor = new QueryBalanceInterceptor();
    
        // 创建工具类
        Enhancer enhancer = new Enhancer();
        // 设置委托类,在cglib中,这是cglib需要继承的超类
        enhancer.setSuperclass(BankSystem.class);
        // 设置多个拦截器
        enhancer.setCallbacks(new Callback[]{transferInterceptor, queryBalanceInterceptor});
        // 实现拦截器和方法的映射,即为不同的方法配置不同的拦截器
        enhancer.setCallbackFilter(new BankFilter());
    
        // 创建代理类
        BankSystem proxy = (BankSystem) enhancer.create();
    
        // 执行转账,调用TransferInterceptor
        proxy.transferAccount(1024.28, "lucy");
        
        // 查询余额,调用QueryBalanceInterceptor
        System.out.printf("\n");
        proxy.queryBalance();
    }
    
  • 执行结果如下:

3.3.2 cglib总结

优点:

  • cglib基于实现动态代理,通过ASM字节码框架动态生成委托类的子类,并使用方法拦截器实现对委托类方法的拦截。
  • 基于ASM字节码框架动态生成代理类,比jdk动态生成代理类更加高效

缺点:

  • 通过继承委托类创建动态代理类,因此不能代理final委托类或委托类中的final方法

4. 面试常见问题

java中代理的实现

  • 共有三种方法:静态代理、JDK动态代理、cglib动态代理
  • 静态代理: 为每个实现类都创建一个对应的代理类,需要创建并维护大量的代理类
  • jdk动态代理: 通过Proxy.newProxyInstance()为抽象主题创建代理类,被代理的委托类包含在InvocationHandler类中,由InvocationHandler类的invoke方法通过反射实现对委托类方法的调用
  • cglib动态代理:通过ASM字节码框架,继承委托类以创建代理类。代理类通过方法拦截器,实现对委托类方法的拦截

三种代理方式的比较

  1. 静态代理,需要创建并维护大量的委托类
  2. jdk动态代理,避免了静态类的上述缺点,但只能代理接口(Java单继承原则,代理类已经继承了Proxy类)
  3. cglib动态代理,可以实现对类的代理,并通过方法拦截器实现对委托类(父类)方法的拦截;使用强大的ASM字节码框架,更加高效;通过继承实现对类的代理,使其无法代理final类或类中的final方法

为何调用代理类的方法,会自动进入InvocationHandlerinvoke()方法?

  • 通过newProxyInstance()创建代理类时,会为代理类设置InvocationHandlerh

  • 动态生成的代理类字节码,通过反编译可以发现,它实现了抽象主题中的每个方法

  • 方法的实现,是调用内部成员h.invoke()方法

    this.h.invoke(this, method, args);
    
  • 因此,调用代理类的方法时,实际上会调用InvocationHandlerinvoke()方法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值