设计模式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代理

 

原文地址:https://www.cnblogs.com/dudu2mama/p/14151057.html