2022-08-31  阅读(3)
原文作者:唯一浩哥 原文地址:https://www.jianshu.com/u/c52a29db48b6

1、动态代理(Dynamic Proxy)

代理分为静态代理和动态代理,静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能。

其实方法直接调用就可以完成功能,为什么还要加个代理呢?

原因是采用代理模式可以有效的将具体的实现与调用方进行解耦,通过面向接口进行编码完全将具体的实现隐藏在内部。

2、代理实现的一般模式

其实代理的一般模式就是静态代理的实现模式:首先创建一个接口(JDK代理都是面向接口的),然后创建具体实现类来实现这个接口,在创建一个代理类同样实现这个接口,不同之处在于,具体实现类的方法中需要将接口中定义的方法的业务逻辑功能实现,而代理类中的方法只要调用具体类中的对应方法即可,这样我们在需要使用接口中的某个方法的功能时直接调用代理类的方法即可,将具体的实现类隐藏在底层。

  • 第一步:定义总接口Iuser.java
            package ceshi1;
            public interface Iuser {
                void eat(String s);
            }
  • 第二步:创建具体实现类UserImpl.java
            package ceshi1;
            public class UserImpl implements Iuser {
              @Override
              public void eat(String s) {
                System.out.println("我要吃"+s);
              }
            }
  • 第三步:创建代理类UserProxy.java
            package ceshi1;
            public class UserProxy implements Iuser {
              private Iuser user = new UserImpl();
              @Override
              public void eat(String s) {
                System.out.println("静态代理前置内容");
                user.eat(s);
                System.out.println("静态代理后置内容");
              }
            }
  • 第四步:创建测试类ProxyTest.java
            package ceshi1;
            public class ProxyTest {
              public static void main(String[] args) {    
                UserProxy proxy = new UserProxy();
                proxy.eat("苹果");
              }
            }

运行结果:

            静态代理前置内容
            我要吃苹果
            静态代理后置内容

3、JDK动态代理的实现

JDK动态代理的思维模式与之前的一般模式是一样的,也是面向接口进行编码,创建代理类将具体类隐藏解耦,不同之处在于代理类的创建时机不同,动态代理需要在运行时因需实时创建。

  • 第一步:定义总接口Iuser.java
            package ceshi1;
            public interface Iuser {
              void eat(String s);
            }
  • 第二步:创建具体实现类UserImpl.java
            package ceshi1;
            public class UserImpl implements Iuser {
              @Override
              public void eat(String s) {
                System.out.println("我要吃"+s);
              }
            }
  • 第三步:创建实现InvocationHandler接口的代理类
            package ceshi1;
            import java.lang.reflect.InvocationHandler;
            import java.lang.reflect.Method;
            public class DynamicProxy implements InvocationHandler {
              private Object object;//用于接收具体实现类的实例对象
              //使用带参数的构造器来传递具体实现类的对象
              public DynamicProxy(Object obj){
                this.object = obj;
              }
              @Override
              public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
                System.out.println("前置内容");
                method.invoke(object, args);
                System.out.println("后置内容");
                return null;
              }
            }
  • 第四步:创建测试类ProxyTest.java
            package ceshi1;
            import java.lang.reflect.InvocationHandler;
            import java.lang.reflect.Proxy;
            public class ProxyTest {
              public static void main(String[] args) {
                Iuser user = new UserImpl();
                InvocationHandler h = new DynamicProxy(user);
                Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);
                proxy.eat("苹果");
              }
            }

运行结果为:

            动态代理前置内容
            我要吃苹果
            动态代理后置内容

4、实现分析

4.1 首先我要说的就是接口,为什么JDK的动态代理是基本接口实现的呢?

因为通过使用接口指向实现类的实例的多态实现方式,可以有效的将具体的实现与调用之间解耦,便于后期修改与维护。

再具体的说就是我们在代理类中创建一个私有成员变量(private修饰),使用接口来指向实现类的对象(纯种的多态体现,向上转型的体现),然后在该代理类中的方法中使用这个创建的实例来调用实现类中的相应方法来完成业务逻辑功能。

这么说起来,我之前说的“将具体实现类完全隐藏”就不怎么正确了,可以改成,将具体实现类的细节向调用方完全隐藏(调用方调用的是代理类中的方法,而不是实现类中的方法)。

这就是面向接口编程,利用java的多态特性,实现程序代码的解耦。

4.2 创建代理类的过程

如果你了解静态代理,那么你会发现动态代理的实现其实与静态代理类似,都需要创建代理类,但是不同之处也很明显,创建方式不同!

不同之处体现在静态代理我们知根知底,我们知道要对哪个接口、哪个实现类来创建代理类,所以我们在编译前就直接实现与实现类相同的接口,直接在实现的方法中调用实现类中的相应(同名)方法即可;而动态代理不同,我们不知道它什么时候创建,也不知道要创建针对哪个接口、实现类的代理类(因为它是在运行时因需实时创建的)。

虽然二者创建时机不同,创建方式也不相同,但是原理是相同的,不同之处仅仅是:静态代理可以直接编码创建,而动态代理是利用反射机制来抽象出代理类的创建过程。

让我们来分析一下之前的代码来验证一下上面的说辞:

  • 第一点:静态代理需要实现与实现类相同的接口,而动态代理需要实现的是固定的Java提供的内置接口(一种专门提供来创建动态代理的接口)InvocationHandler接口,因为java在接口中提供了一个可以被自动调用的方法invoke,这个之后再说。
  • 第二点:
            private Object object;
            public UserProxy(Object obj){this.object = obj;}

这几行代码与静态代理之中在代理类中定义的接口指向具体实现类的实例的代码异曲同工,通过这个构造器可以创建代理类的实例,创建的同时还能将具体实现类的实例与之绑定(object指的就是实现类的实例,这个实例需要在测试类中创建并作为参数来创建代理类的实例),实现了静态代理类中private Iuser user = new UserImpl();一行代码的作用相近,这里为什么不是相同,而是相近呢,主要就是因为静态代理的那句代码中包含的实现类的实例的创建,而动态代理中实现类的创建需要在测试类中完成,所以此处是相近。

  • 第三点:invoke(Object proxy, Method method, Object[] args)方法,该方法是InvocationHandler接口中定义的唯一方法,该方法在调用指定的具体方法时会自动调用。其参数为:代理实例、调用的方法、方法的参数列表。

在这个方法中我们定义了几乎和静态代理相同的内容,仅仅是在方法的调用上不同,不同的原因与之前分析的一样(创建时机的不同,创建的方式的不同,即反射),Method类是反射机制中一个重要的类,用于封装方法,该类中有一个方法那就是invoke(Object object,Object...args)方法,其参数分别表示:所调用方法所属的类的对象和方法的参数列表,这里的参数列表正是从测试类中传递到代理类中的invoke方法三个参数中最后一个参数(调用方法的参数列表)中,在传递到method的invoke方法中的第二个参数中的(此处有点啰嗦)。

  • 第四点:测试类中的异同

静态代理中我们测试类中直接创建代理类的对象,使用代理类的对象来调用其方法即可,若是别的接口(这里指的是别的调用方)要调用Iuser的方法,也可以使用此法。

动态代理中要复杂的多,首先我们要将之前提到的实现类的实例创建(补充完整),然后利用这个实例作为参数,调用代理来的带参构造器来创建“代理类实例对象”,这里加引号的原因是因为它并不是真正的代理类的实例对象,而是创建真正代理类实例的一个参数,这个实现了InvocationHandler接口的类严格意义上来说并不是代理类,我们可以将其看作是创建代理类的必备中间环节,这是一个调用处理器,也就是处理方法调用的一个类,不是真正意义上的代理类,可以这么说:创建一个方法调用处理器实例。

下面才是真正的代理类实例的创建,之前创建的”代理类实例对象“仅仅是一个参数:

            Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);

这里使用了动态代理所依赖的第二个重要类Proxy,此处使用了其静态方法来创建一个代理实例,其参数分别是:类加载器(可为父类的类加载器)、接口数组、方法调用处理器实例。

这里同样使用了多态,使用接口指向代理类的实例,最后会用该实例来进行具体方法的调用即可。

4.3 InvocationHandler

InvocationHandler是JDK中提供的专门用于实现基于接口的动态代理的接口,主要用于进行方法调用模块,而代理类和实例的生成需要借助Proxy类完成。

每个代理类的实例的调用处理器都是实现该接口实现的,而且是必备的,即每个动态代理实例的实现都必须拥有实现该接口的调用处理器,也可以这么说,每个动态代理实例都对应一个调用处理器。

这里要区分两个概念,代理类和代理实例,调用处理器是在创建代理实例的时候才与其关联起来的,所以它与代理实例是一一对应的,而不是代理类。

4.4 Proxy

Proxy类是JDK提供的用于生成动态代理类和其实例的类。

我们可以通过Proxy中的静态方法getProxyClass来生成代理类,需要的参数为类加载器和接口列表(数组),然后再通过反射调用代理类的构造器来生成代理实例,需要以一个InvocationHandler作为参数(体现出方法调用是与实例相关的,而非类)。

            InvocationHandler handler = new MyInvocationHandler(...);
            Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);
            Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);

我们也可以直接通过Proxy中的静态方法newProxyInstance方法来直接生产代理实例,需要提供参数为上面的三个参数,即类加载器,接口数组,InvocationHandler。

            Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class<?>[] { Foo.class },handler);

4.5 总结

我们总结下JDK动态代理的实现步骤:

  • 第一步:创建接口,JDK动态代理基于接口实现,所以接口必不可少(准备工作)
  • 第二步:实现InvocationHandler接口,重写invoke方法(准备工作)
  • 第三步:调用Proxy的静态方法newProxyInstance方法生成代理实例(生成实例时需要提供类加载器,我们可以使用接口类的加载器即可)
  • 第四步:使用新生成的代理实例调用某个方法实现功能。

我们的动态代理实现过程中根本没有涉及到真实类实例。
5、Cglib动态代理的实现
JDK动态代理拥有局限性,那就是必须面向接口编程,没有接口就无法实现代理,我们也不可能为了代理而为每个需要实现代理的类强行添加毫无意义的接口,这时我们需要Cglib,这种依靠继承来实现动态代理的方式,不再要求我们必须要有接口。

  • 第一步:添加Cglib的Maven依赖
            <dependency>
               <groupId>cglib</groupId>
               <artifactId>cglib</artifactId>
               <version>3.1</version>
            </dependency>
  • 第二步:创建具体实现类User.java
            public class User {
                public void eat(String s){
                    System.out.println("我要吃" + s);
                }
            }
  • 第三步:创建实现MethodInterceptor接口的代理类
            import net.sf.cglib.proxy.MethodInterceptor;
            import net.sf.cglib.proxy.MethodProxy;
            import java.lang.reflect.Method;
        
            public class UserInterceptor implements MethodInterceptor {
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    System.out.println("预处理");
                    Object object =  methodProxy.invokeSuper(o,objects);
                    System.out.println("后处理");
                    return object;
                }
            }
  • 第四步:创建测试类ProxyTest.java
            import net.sf.cglib.proxy.Enhancer;
        
            public class ProxyTest {
                public static void main(String[] args){
                    Enhancer enchancer = new Enhancer();//字节码增强器
                    enchancer.setSuperclass(User.class);//设置被代理类为父类
                    enchancer.setCallback(new UserInterceptor());//设置回调
                    User user = (User)enchancer.create();//创建代理实例
                    user.eat("葡萄");
                }
            }

执行结果:

            预处理
            我要吃葡萄
            后处理

来源:https://www.jianshu.com/u/c52a29db48b6


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文