zoukankan      html  css  js  c++  java
  • 设计模式5-代理模式

    一、概念

    结构型模式

    给某一对象提供给一个代理类,并由代理对象控制对原对象的引用。

    代理模式实现(三要素)

     一句话:客户端通过代理类来调用目标方法,代理类会将所有的方法调用分派到目标对象上反射执行,还可以在分派过程中添加"前置通知"和后置处理。

    • 一个接口,接口中的方法是具体要实现的;
    • 一个被代理类,实现上述接口, 这是真正去执行接口中方法的类;
    • 一个代理类,实现上述接口,同时分装被代理类对象,帮助被代理类去实现方法。

     代理模式分为静态代理模式动态代理模式,首先我们来看下静态代理模式。

    二、静态代理模式

    这里模拟的是网站的访问。几乎所有的Web项目尤其是大型网站,不可能采用集中式的架构的,必定是采用分布式架构。 对于分布式架构,但用户发起请求的时候,请求指向的并不是最终的服务器端,而是代理服务器,比如Nginx,用以负载均衡。

    示例:用户访问网站->代理服务器->最终服务器。先定义一个服务器接口Server,简单定义一个方法:

     1 /**
     2  * 服务器接口,用于获取网站数据
     3  */
     4 public interface Server {
     5 
     6     /**
     7      * 根据url获取页面标题
     8      */
     9     String getPageTitle(String url);
    10 
    11 }
    View Code

    例如我们访问的是百度,所以定义一个BdServer(此处采用简单的if...else判定,便于理解):

     1 public class BdServer implements Server{
     2     public BdServer() {
     3         System.out.println("BdServer Constructor");
     4     }
     5 
     6     @Override
     7     public String getPageTitle(String url) {
     8         if ("https://www.baidu.com/".equals(url)) {
     9             return "百度首页";
    10         } else if ("http://top.baidu.com/?fr=mhd_card".equals(url)) {
    11             return "百度风云榜";
    12         }
    13         System.out.println("url not match.");
    14         return "无页面标题";
    15     }
    16 }
    View Code

    若不采用代理,那么访问网站,就会直接new BdServer()出来并且调用getPageTitle(String url)方法即可;

    由于分布式架构的存在,因此我们这里要写一个NginxProxy,作为一个代理,到时候用户直接访问的是NginxProxy而不是和BdServer打交道,由NginxProxy负责和最终的BdServer打交道。

     1 /**
     2  * Nginx代理
     3  */
     4 public class NginxProxy implements Server {
     5 
     6     /**
     7      * 服务器列表
     8      */
     9     private static final List<String> SERVER_ADDRESSES = Lists.newArrayList("192.168.1.1", "192.168.1.2", "192.168.1.3");
    10 
    11     private Server server;
    12 
    13     public NginxProxy(Server server) {
    14         this.server = server;
    15     }
    16 
    17     @Override
    18     public String getPageTitle(String url) {
    19         // 这里就简单传了一个url,正常请求传入的是Request,使用UUID模拟请求原始Ip
    20         String remoteIp = UUID.randomUUID().toString();
    21         // 路由选择算法这里简单定义为对remoteIp的Hash值的绝对值取模
    22         int index = Math.abs(remoteIp.hashCode()) % SINA_SERVER_ADDRESSES.size();
    23         // 选择服务器Ip
    24         String realIp = SERVER_ADDRESSES.get(index);
    25 
    26         return "【页面标题:" + server.getPageTitle(url) + "】,【来源Ip:" + realIp + "】";
    27     }
    28 
    29 }
    View Code

    这里同样为了简单起见,服务器列表写死几个ip,同时由于只传一个url而不是具体的Request,每次随机一个UUID,对UUID的HashCode绝对值取模,模拟这次请求被路由到哪台服务器上。

    调用方法:

     1 /**
     2  * 静态代理测试
     3  */
     4 public class StaticProxyTest {
     5 
     6     @Test
     7     public void testStaticProxy() {
     8         Server bdServer = new BdServer();
     9         Server nginxProxy = new NginxProxy(bdaServer );
    10         System.out.println(nginxProxy.getPageTitle("https://www.baidu.com/"));
    11     }
    12 
    13 }

    第8行表示的是要访问的是百度服务器,第9行表示的是用户实际访问的是Nginx代理而不是真实的百度服务器,由于百度服务器和代理服务器实际上都是服务器,因此他们可以使用相同的接口Server。

    以上就是一个静态代理的例子,即用户不和最终目标对象角色(BdServer)打交道,而是和代理对象角色(NginxProxy)打交道,由代理对象角色(NginxProxy)控制用户的访问

    但静态代理也有相应的缺点

    • 静态代理的特点是静态代理的代理类是程序员创建的,在程序运行之前静态代理的.class文件已经存在了
    • 从静态代理模式的代码来看,静态代理模式确实有一个代理对象来控制实际对象的引用,并通过代理对象来使用实际对象。这种模式在代理量较小的时候还可以,但是代理量一大起来,就存在着两个比较大的缺点:

               1)静态代理的内容,即NginxProxy的路由选择这几行代码,只能服务于Server接口而不能服务于其他接口,如果其它接口想用这几行代码,比如新增一个静态代理类。久而久之,由于静态代理的内容无法复用,必然造成静态代理类的不断庞大

               2)Server接口里面如果新增了一个方法,比如getPageData(String url)方法,实际对象实现了这个方法,代理对象也必须新增方法getPageData(String url),去给getPageData(String url)增加代理内容(假如需要的话)

    三、动态代理

    动态代理有两种方式,利用JDK中的代理类Proxy实现动态代理(只能针对接口生成代理类,不可为某一个类生成代理类,底层反射机制)以及使用CGLIB(一种字节码增强技术,底层继承实现)来为某一个类或接口实现代理。

    1、利用JDK中的代理类Proxy实现动态代理

    JDK动态代理是由JDK的java.lang.reflect.*包提供支持的, 只针对接口生成代理类,使用的是JDK自带的Proxy类+InvocationHandler接口invoke反射调用,提供的代理类提供真实对象的绑定和代理方法。

    实现:

    • 通过实现InvocationHandler接口来自定义自己的InvocationHandler(该接口只有一个方法);
    • 通过Proxy.getProxyClass获取动态代理类;
    • 通过反射机制获取代理类的构造方法,方法签名为getConstructor(InvocationHandler.class);
    • 通过构造函数获得代理对象并将自定义的InvocationHandler实例对象作为参数传入;
    • 通过代理对象调用目标方法。

    在举例前着重看下InvacationHandler接口和Proxy类

    InvocationHandler接口(只有一个方法)

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    • proxy:代理类对象
    • method:被代理的方法
    • args:被代理的方法的参数列表
    Proxy 类
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException
    • loader:类加载器
    • interfaces:代理类实现的所有接口
    • h:InvocationHandler接口的一个实例  this当前对象
    因为我们想使用jdk动态代理 必须是 代理类 实现 InvocationHandler! 它让我们传递父接口 我们传递 自身!
     

    示例

    将上述的NginxProxy.java改为以下的NginxInvacationHandler.java

     1 /**
     2  * Nginx InvocationHandler
     3  */
     4 public class NginxInvocationHandler implements InvocationHandler {
     5 
     6     /**
     7      * 服务器列表
     8      */
     9     private static final List<String> SERVER_ADDRESSES = Lists.newArrayList("192.168.1.1", "192.168.1.2", "192.168.1.3");
    10 
    11     private Object object;
    12 
    13     public NginxInvocationHandler(Object object) {
    14         this.object = object;
    15     }
    16 
    17     @Override
    18     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    19         String remoteIp = UUID.randomUUID().toString();
    20         int index = Math.abs(remoteIp.hashCode()) % SERVER_ADDRESSES.size();
    21         String realIp = SERVER_ADDRESSES.get(index);
    22 
    23         StringBuilder sb = new StringBuilder();
    24         sb.append("【页面标题:");
    25         sb.append(method.invoke(object, args)); //这里的method.invoke执行的就是BdServer中的getPageTitle方法,使用的反射
    26         sb.append("】,【来源Ip:");
    27         sb.append(realIp );
    28         sb.append("】");
    29         return sb.toString();
    30     }
    31 
    32 }

     此处增加一个反射示例

     1 public class A {
     2     
     3     public void foo(String name) {
     4     System.out.println("Hello, " + name);
     5     }
     6 }
     7 
     8 通过TestClassLoader类来反射调用A上的方法
     9 public class TestClassLoad {
    10     
    11     public static void main(String[] args) throws Exception {
    12       
    13         //获得代理类
    14         Class<?> clz = Class.forName("com.jincou.study.A");
    15         //获得代理类对象
    16         Object o = clz.newInstance();
    17         //获得foo方法
    18          Method m = clz.getMethod("foo", String.class);
    19         //执行foot方法
    20          m.invoke(o,"张三");
    21    }
    22 }
    View Code

    这里就将选择服务器的逻辑抽象成为了公共的代码了,因为调用的是Object里面的method,Object是所有类的超类,因此并不限定非要是Sever,A、B、C都是可以的,因此这个NginxInvocationHandler可以灵活地被各个地方给复用。

    调用:

     1   /**
     2    * 动态代理测试
     3   */
     4   public class DynamicProxyTest {
     5   
     6       @Test
     7       public void testDynamicProxy() {
     8           Server bdServer = new BdServer();
     9           InvocationHandler invocationHandler = new NginxInvocationHandler(bdServer);
    10          //将自定义的InvocationHandler示例传入,获取动态代理对象,此处的Server.class不可替换成实现类如BdServer.class,不然会报Exception in thread "main" java.lang.IllegalArgumentException: proxy.DynamicBdServer is not an interface
    11          Server proxy = (Server)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Server.class}, invocationHandler);
    12     
    13         //通过代理对象调用目标方法
    14          System.out.println(proxy.getPageTitle("https://www.baidu.com/"));
    15      }
    16  
    17  }

    以上第11行是封装好的创建动态代理对象的方法,也可采用如下方式获取代理对象:

    //获取动态代理类(proxyClazz:class com.sun.proxy.$Proxy0)
    Class<?> proxyClazz = Proxy.getProxyClass(this.getClass().getClassLoader(),Server.class);

    //获得代理类的构造函数,并传入参数类型InvocationHandler.class (constructor: public com.sun.proxy.$Proxy0(java.lang.reflect.InvocationHandler
    Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);

    //通过构造函数来创建动态代理对象,将自定义的InvocationHandler实例传入(Server:com.frieda.pattern.NginxInvacationHandler$BdServer@12b42d12)
    Server server = (Server) constructor.newInstance(new NginxInvocationHandler(new BdServer()));

    以上第一步已获得了代理对象,代理对象的名称是:$Proxy0;

    第二部发现代理类的构造函数需要传入一个java.lang.reflect.InvacationHandler类;

    第三部通过构造函数创建对象,构造函数里传入NginxInvacationHandler类,而它是现实InvacationHandler接口 。

    以上就是通过反射机制来实现创建类的。

    Proxy本身也是JDK提供给开发者的,使用Proxy的newProxyInstance方法可以产生对目标接口的一个代理,至于代理的内容,即InvocatoinHandler的实现。

    2、通过CGLIB实现动态代理

    Cglib,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。
    Cglib是一个强大的、高性能的代码生成包,它可以在运行期扩展java类与实现java接口,它广泛被许多AOP框架使用,为他们提供方法的拦截。
    Cglib包的底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类.不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉.。
     

    要实现CGLIB代理,需要实现MEthodInterceptor的代理方法。

    依赖jar(Spring的核心包中已经包括了Cglib功能, spring-core):

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

    实现:

    • 声明增强类实例,用于生产代理类;
      • Enhancer enhancer = new Enhancer();
    • 设置被代理类字节码,CGLIB根据字节码生成被代理类的子类;
      • enhancer.setSuperclass(target.getClass());
    • 设置回调函数,即一个方法拦截;
      • enhancer.setCallback(this);
    • 创建代理。
      • return (A)enhancer.create();

    同样在举例前着重看下MethodInterceptor接口和Enhancer类

    MethodInterceptor接口

    public Object intercept(Object obj, Method method,Object[] args,MethodProxy proxy) throws Throwable;
    intercept是所有拦截器执行的方法,类似于jdk动态代理中的invoke

    Enhancer类

    设置委托类和代理类的公共接口或者公共的父类
     public void setSuperclass(Class superclass) {
                      if (superclass != null && superclass.isInterface()) {
                          setInterfaces(new Class[]{ superclass });
                      } else if (superclass != null && superclass.equals(Object.class)) {
                          // affects choice of ClassLoader
                          this.superclass = null;
                      } else {
                          this.superclass = superclass;
                      }
                  }
    代理类执行完毕 通知委托类
    public void setCallback(final Callback callback) {
                          setCallbacks(new Callback[]{ callback });
                      }
    在Enhancer类的父类AbstractClassGenerator中有一个方法 创建我们需要的代理类
    protected Object create(Object key)

    示例

     1 /**
     2  * CGLIB动态代理
     3  */
     4 public class CglibProxy implements MethodInterceptor {
     5 
     6     /**
     7      *
     8      * @param clazz Class类
     9      * @param argTypes
    10      * @param args
    11      * @return Class的动态代理对象
    12      */
    13     public Object getInstance(Class clazz, Class[] argTypes, Object[] args){
    14         //CGLIB enchancer增强类对象
    15         Enhancer enhancer = new Enhancer();
    16         //设置增强类型
    17         enhancer.setSuperclass(clazz);
    18         //定义代理对象为当前对象,要求当前对象实现MethodInterceptor
    19         enhancer.setCallback(this);
    20         //生产并返回代理对象(argTypes, atgs 为构造函数参数)
    21         return enhancer.create(argTypes, args);
    22     }
    23 
    24 
    25     /**
    26      *
    27      * @param proxy 代理对象
    28      * @param method 方法
    29      * @param args 方法参数
    30      * @param methodProxy 方法代理
    31      * @return
    32      * @throws Throwable 抛出异常
    33      */
    34     @Override
    35     public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    36         System.out.println("调用真实对象前");
    37 
    38         //CGLIB反射调用真实对象方法
    39         Object result = methodProxy.invokeSuper(proxy, args);
    40 
    41         System.out.println("调用真实对象后");
    42 
    43         return result;
    44     }
    45 
    46 }
    View Code 

    关于cglib动态代理中invokeSuper和invoke的区别参考:https://blog.csdn.net/z69183787/article/details/106878203/

    调用示例

        public static void main(String[] args) {
            CglibProxy cglibProxy = new CglibProxy();
            Class[] argumentTypes = new Class[]{};
            Object[] arguments = new Object[]{};
            //获取动态代理对象
            BdServer server = (BdServer) cglibProxy.getInstance(BdServer.class, argumentTypes ,arguments);
            server.getPageTitle("2222");
    
        }

    结果:

    BdServer Constructor
    调用真实对象前
    url not match.
    调用真实对象后

     注意:

    1代理的类不能为final,否则报错, 如上述BdServer不能用final修饰, 否则报错“Exception in thread "main" java.lang.IllegalArgumentException: Cannot subclass final class class com.frieda.zxm.spring.pattern.strategy.BdServer”。

    2、目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.。

      例如上述BdServer中getPageUrl方法采用static或final修饰,结果为:

    BdServer Constructor
    url not match.

     四、代理模式在Java中的应用以及解读

    MyBatis的Mapper就是一个接口,它采用的就是JDK的动态代理;
    在MyBatis中通常在延迟加载的时候回用到CGLIB的动态代理;
    Spring AOP(面向切面编程),扩展功能不能修改源代码,采用横向抽取机制,取代了传统纵向继承体系重复代码。

    那分别在什么场景下使用JDK动态代理和CGLIB动态代理?

    •  如果加入容器的目标对象有实现接口,用JDK代理
    •  如果目标对象没有实现接口,用Cglib代理   
    •  如果目标对象实现了接口,且强制使用cglib代理,则会使用cglib代理
     
    强制使用CGLIB动态代理(没有实践过~)

    Spring中强制使用Cglib代理

    <aop:aspectj-autoproxy proxy-target-class="true" />

    SpringBoot中强制使用Cglib代理

     

  • 相关阅读:
    版本控制
    1121 Reverse the lights(“玲珑杯”线上赛 Round #15 河南专场)
    LightOJ 1055
    LightOJ 1053
    LightOJ 1052
    4512 吉哥系列故事——完美队形I(LCIS)
    ZOJ 2432-Greatest Common Increasing Subsequence
    病毒 (湖南省第八届大学生计算机程序设计竞赛)
    1328 台球碰撞 (湖南省第六届大学生计算机程序设计竞赛)
    zzuli 1332 内部收益率 (湖南省第六届大学生计算机程序设计竞赛)
  • 原文地址:https://www.cnblogs.com/dudu2mama/p/14151057.html
Copyright © 2011-2022 走看看