一、代理模式定义

代理模式(英语:Proxy Pattern)是程序设计中的一种设计模式。

定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

二、代理模式组成

  • 抽象角色:通过接口或抽象类声明真实角色实现的业务方法。
  • 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
  • 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。

三、代理模式结构

一个是真正的你要访问的对象(目标类),一个是代理对象,真正对象与代理
对象实现同一个接口,先访问代理类再访问真正要访问的对象。
代理模式分为静态代理、动态代理。

  • 静态代理:是由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。
  • 动态代理:是在实现阶段不用关心代理类,而在运行阶段才指定哪一个对象。

 四、代理模式通用示例 

代理模式的类图如下:

首先定义AbstractObject类,在其中定义代理类和目标类共有的接口

package com.ssy.wlj.proxy;

/**
 * 抽象对象角色
 * @author Administrator
 * @since 2010/05/23
 *
 */
public abstract class AbstractObject {
    protected abstract void operation();
}

然后定义目标实现类:

package com.ssy.wlj.proxy;

/**
 * 测试类
 * @author Administrator
 * @since 2019/05/23
 *
 */
public class ProxyTest {
    
    public static void main(String[] args) {
        AbstractObject proxy = new ProxyObject(new RealObject());
        proxy.operation();
    }

}

然后是是代理类:

package com.ssy.wlj.proxy;

/**
 * 代理类
 * 
 * @author Administrator
 * @since 2019/05/23
 *
 */
public class ProxyObject extends AbstractObject {

    // 对目标类的引用
    private RealObject realObject;
    public ProxyObject(RealObject realObject) {
        this.realObject = realObject;
    }

    @Override
    protected void operation() {
        System.out.println("do something before real peration...");
        if (realObject == null) {
            realObject = new RealObject();
        }
        realObject.operation();
        System.out.println("do something after real operation...");
    }

}

最后编写测试类:

package com.ssy.wlj.proxy;

/**
 * 测试类
 * @author Administrator
 * @since 2019/05/23
 *
 */
public class ProxyTest {    
    public static void main(String[] args) {
        AbstractObject proxy = new ProxyObject(new RealObject());
        proxy.operation();
    }
}

 运行测试类得到结果如下:

do something before real peration...
do operation……
do something after real operation...

 五、动态代理

动态代理有别于静态代理,是根据代理的对象,动态创建代理类。这样,就可以避免静态代理中代理类接口过多的问题。动态代理是实现方式,是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。

其步骤如下:
编写一个委托类的接口,即静态代理的(Subject接口)
实现一个真正的委托类,即静态代理的(RealSubject类)
创建一个动态代理类,实现InvocationHandler接口,并重写该invoke方法
在测试类中,生成动态代理的对象。
第一二步骤,和静态代理一样,不过说了。第三步,代码如下:

public class DynamicProxy implements InvocationHandler {
  private Object object;
  public DynamicProxy(Object object) {
    this.object = object;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object result = method.invoke(object, args);
    return result;
  }
}

第四步,创建动态代理的对象

Subject realSubject = new RealSubject();
DynamicProxy proxy = new DynamicProxy(realSubject);
ClassLoader classLoader = realSubject.getClass().getClassLoader();
Subject subject = (Subject) Proxy.newProxyInstance(classLoader, new Class[]{Subject.class}, proxy);
subject.visit();

创建动态代理的对象,需要借助Proxy.newProxyInstance。该方法的三个参数分别是:
ClassLoader loader表示当前使用到的appClassloader。
Class<?>[] interfaces表示目标对象实现的一组接口。
InvocationHandler h表示当前的InvocationHandler实现实例对象。 

六、静态代理和动态代理场景展示

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

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

/**
* 服务器接口,用于获取网站数据
*/
public interface Server {
  /**
  * 根据url获取页面标题
  */
  public String getPageTitle(String url);
}

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

/**
* 新浪服务器
*/
public class SinaServer implements Server {

  @Override
  public String getPageTitle(String url) {
  if ("http://www.sina.com.cn/".equals(url)) {
    return "新浪首页";
  } else if ("http://http://sports.sina.com.cn/".equals(url)) {
    return "新浪体育_新浪网";
  }
  return "无页面标题";
  }
}

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

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

package com.ssy.wlj.proxy2;

import java.util.List;
import java.util.UUID;

import com.google.inject.internal.util.Lists;

/**
 * Nginx代理
 * @author Administrator
 * @since 2019/05/23
 *
 */
public class NginxProxy implements Server {

      /**
      * 新浪服务器列表
      */
      private static final List<String> SINA_SERVER_ADDRESSES = Lists.newArrayList("192.168.1.1", "192.168.1.2", "192.168.1.3"); 
    private Server server;
    public NginxProxy(Server server) {
        this.server = server;
      }

      @Override
      public String getPageTitle(String url) {
        // 这里就简单传了一个url,正常请求传入的是Request,使用UUID模拟请求原始Ip
        String remoteIp = UUID.randomUUID().toString();
        // 路由选择算法这里简单定义为对remoteIp的Hash值的绝对值取模
        int index = Math.abs(remoteIp.hashCode()) % SINA_SERVER_ADDRESSES.size();
        // 选择新浪服务器Ip
        String realSinaIp = SINA_SERVER_ADDRESSES.get(index);
    return "【页面标题:" + server.getPageTitle(url) + "】,【来源Ip:" + realSinaIp + "】";
      }
    }

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

调用方这么写:

package com.ssy.wlj.proxy2;

import org.junit.Test;

/**
 * 静态代理测试
 * @author Administrator
 * @since 2019/05/23
 *
 */
public class StaticProxyTest {
    @Test
    public void testStaticProxy() {
      Server sinaServer = new SinaServer();
      Server nginxProxy = new NginxProxy(sinaServer);
      System.out.println(nginxProxy.getPageTitle("http://www.sina.com.cn/"));
    }

}

第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接口:

package com.ssy.wlj.proxy2;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
import java.util.UUID;

import com.google.inject.internal.util.Lists;

/**
 * Nginx InvocationHandler
 * @author Administrator
 * @since 2019/05/23
 *
 */
public class NginxInvocationHandler implements InvocationHandler {
    /**
     * 新浪服务器列表
     */
    private static final List<String> SINA_SERVER_ADDRESSES = Lists.newArrayList("192.168.1.1", "192.168.1.2",
            "192.168.1.3");
    private Object object;

    public NginxInvocationHandler(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String remoteIp = UUID.randomUUID().toString();
        int index = Math.abs(remoteIp.hashCode()) % SINA_SERVER_ADDRESSES.size();
        String realSinaIp = SINA_SERVER_ADDRESSES.get(index);
        StringBuilder sb = new StringBuilder();
        sb.append("【页面标题:");
        sb.append(method.invoke(object, args));
        sb.append("】,【来源Ip:");
        sb.append(realSinaIp);
        sb.append("】");
        return sb.toString();
    }
}

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

调用的时候这么写:

package com.ssy.wlj.proxy2;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

import org.junit.Test;

/**
 * 动态代理测试
 * 
 * @author Administrator
 * @since 2019/05/23
 *
 */
public class DynamicProxyTest {

    @Test
    public void testDynamicProxy() {
        Server sinaServer = new SinaServer();
        InvocationHandler invocationHandler = new NginxInvocationHandler(sinaServer);
        Server proxy = (Server) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] { Server.class },
                invocationHandler);
        System.out.println(proxy.getPageTitle("http://www.sina.com.cn/"));
    }

}

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(一种字节码增强技术)来为某一个类实现代理了。

 

posted on 2019-05-23 10:21  happy_2010  阅读(108)  评论(0编辑  收藏  举报