一、代理模式定义
代理模式(英语: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(一种字节码增强技术)来为某一个类实现代理了。