• Java设计模式9:代理模式


    代理模式

    代理模式的定义很简单:给某一对象提供一个代理对象,并由代理对象控制对原对象的引用

    代理模式的结构

    有些情况下,一个客户不想或者不能够直接引用一个对象,可以通过代理对象在客户端和目标对象之间起到中介作用。代理模式中的角色有:

    1、抽象对象角色

    声明了目标对象和代理对象的共同接口,这样一来在任何可以使用目标对象的地方都可以使用代理对象

    2、目标对象角色

    定义了代理对象所代表的目标对象

    3、代理对象角色

    代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象

    静态代理示例

    这里模拟的是作为访问网站的场景,以新浪网举例。我们通常访问新浪网,几乎所有的Web项目尤其是新浪这种大型网站,是不可能采用集中式的架构的,使用的一定是分布式的架构,分布式架构对于用户来说,我们发起链接的时候,链接指向的并不是最终的应用服务器,而是代理服务器比如Nginx,用以做负载均衡。

    所以,我们的例子,简化来说就是用户访问新浪网-->代理服务器-->最终服务器。先定义一个服务器接口Server,简单定义一个方法,用于获取页面标题:

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

    我们访问的是新浪网,所以写一个SinaServer,传入url,获取页面标题:

     1 /**
     2  * 新浪服务器
     3  */
     4 public class SinaServer implements Server {
     5 
     6     @Override
     7     public String getPageTitle(String url) {
     8         if ("http://www.sina.com.cn/".equals(url)) {
     9             return "新浪首页";
    10         } else if ("http://http://sports.sina.com.cn/".equals(url)) {
    11             return "新浪体育_新浪网";
    12         }
    13         
    14         return "无页面标题";
    15     }
    16     
    17 }

    这里写得比较简单,就做了一个if..else if判断,大家理解意思就好。写到这里,我们说明两点:

    • 如果不使用代理,那么用户访问相当于就是直接new SinaServer()出来并且调用getPageTitle(String url)方法即可
    • 由于分布式架构的存在,因此我们这里要写一个NginxProxy,作为一个代理,到时候用户直接访问的是NginxProxy而不是和SinaServer打交道,由NginxProxy负责和最终的SinaServer打交道

    因此,我们写一个NginxProxy:

     1 /**
     2  * Nginx代理
     3  */
     4 public class NginxProxy implements Server {
     5     
     6     /**
     7      * 新浪服务器列表
     8      */
     9     private static final List<String> SINA_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 realSinaIp = SINA_SERVER_ADDRESSES.get(index);
    25         
    26         return "【页面标题:" + server.getPageTitle(url) + "】,【来源Ip:" + realSinaIp + "】";
    27     }
    28     
    29 }

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

    调用方这么写:

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

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

    程序最终运行的结果为:

    【页面标题:新浪首页】,【来源Ip:192.168.1.2

    当然,多运行几次,来源Ip一定是会变的,这就是一个静态代理的例子,即用户不和最终目标对象角色(SinaServer)打交道,而是和代理对象角色(NginxProxy)打交道,由代理对象角色(NginxProxy)控制用户的访问

    静态代理的缺点

    静态代理的特点是静态代理的代理类是程序员创建的,在程序运行之前静态代理的.class文件已经存在了

    从静态代理模式的代码来看,静态代理模式确实有一个代理对象来控制实际对象的引用,并通过代理对象来使用实际对象。这种模式在代理量较小的时候还可以,但是代理量一大起来,就存在着两个比较大的缺点:

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

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

    利用JDK中的代理类Proxy实现动态代理的示例

    由于静态代理的局限性,所以产生了动态代理的概念。

    上面的例子我们采用动态代理的方式,动态代理的核心就是将公共的逻辑抽象到InvocationHandler中。关于动态代理,JDK本身提供了支持,因此实现一下InvocationHandler接口:

     1 /**
     2  * Nginx InvocationHandler
     3  */
     4 public class NginxInvocationHandler implements InvocationHandler {
     5 
     6     /**
     7      * 新浪服务器列表
     8      */
     9     private static final List<String> SINA_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()) % SINA_SERVER_ADDRESSES.size();
    21         String realSinaIp = SINA_SERVER_ADDRESSES.get(index);
    22         
    23         StringBuilder sb = new StringBuilder();
    24         sb.append("【页面标题:");
    25         sb.append(method.invoke(object, args));
    26         sb.append("】,【来源Ip:");
    27         sb.append(realSinaIp);
    28         sb.append("】");
    29         return sb.toString();
    30     }
    31     
    32 }

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

    调用的时候这么写:

     1 /**
     2  * 动态代理测试
     3  */
     4 public class DynamicProxyTest {
     5 
     6     @Test
     7     public void testDynamicProxy() {
     8         Server sinaServer = new SinaServer();
     9         InvocationHandler invocationHandler = new NginxInvocationHandler(sinaServer);
    10         Server proxy = (Server)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Server.class}, invocationHandler);
    11         
    12         System.out.println(proxy.getPageTitle("http://www.sina.com.cn/"));
    13     }
    14     
    15 }

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

    看一下运行结构,和静态代理是一样的:

    【页面标题:新浪首页】,【来源Ip:192.168.1.2

    动态代理写法本身有点不好理解,需要开发者多实践,多思考,才能真正明白动态代理的含义及其实际应用。

    动态代理的优点

    1、最直观的,类少了很多

    2、代理内容也就是InvocationHandler接口的实现类可以复用,可以给A接口用、也可以给B接口用,A接口用了InvocationHandler接口实现类A的代理,不想用了,可以方便地换成InvocationHandler接口实现B的代理

    3、最重要的,用了动态代理,就可以在不修改原来代码的基础上,就在原来代码的基础上做操作,这就是AOP即面向切面编程

    动态代理的缺点

    动态代理有一个最大的缺点,就是它只能针对接口生成代理,不能只针对某一个类生成代理,比方说我们在调用Proxy的newProxyInstance方法的时候,第二个参数传某个具体类的getClass(),那么会报错:

    Exception in thread "main" java.lang.IllegalArgumentException: proxy.DynamicHelloWorldImpl is not an interface

    这是因为java.lang.reflect.Proxy的newProxyInstance方法会判断传入的Class是不是一个接口:

    ...
    /*
      * Verify that the Class object actually represents an
      * interface.
      */
     if (!interfaceClass.isInterface()) {
     throw new IllegalArgumentException(
        interfaceClass.getName() + " is not an interface");
    }
    ...

    而实际使用中,我们为某一个单独的类实现一个代理也很正常,这种情况下,我们就可以考虑使用CGLIB(一种字节码增强技术)来为某一个类实现代理了。

  • 相关阅读:
    Leetcode Binary Tree Preorder Traversal
    Leetcode Minimum Depth of Binary Tree
    Leetcode 148. Sort List
    Leetcode 61. Rotate List
    Leetcode 86. Partition List
    Leetcode 21. Merge Two Sorted Lists
    Leetcode 143. Reorder List
    J2EE项目应用开发过程中的易错点
    JNDI初认识
    奔腾的代码
  • 原文地址:https://www.cnblogs.com/xrq730/p/4907999.html
Copyright © 2020-2023  润新知