设计模式---代理模式
背景:听说Java设计模式中的代理模式是进入BAT的必经之路。
1、代理模式:
给某一对象提供一个代理对象,并由代理对象控制对原对象的引用;简而言之,就是在不改变源代码的情况下,实现对目标功能的扩展;比如,你计划下个月结婚,当然你可以自己筹划婚礼的过程,那样太闹腾了,于是你就把筹备婚礼的过程交由婚庆公司布置,并且只需要在婚礼当天使用婚庆公司筹划婚礼的结果即可。
2、代理模式的结构:
a、抽象对象角色
声明了目标对象和代理对象的共同接口,那么在任何可以使用目标对象的地方也均可使用代理对象;
b、目标对象角色
定义了代理对象所代表的目标对象;
c、代理对象角色
代理对象内部包含目标对象的引用,So可以自由地操作目标对象,代理对象提供一个与目标对象相同的接口,So可以在任何时候替换目标对象;
3、静态代理
从JVM加载类的角度看,静态代理和动态代理本质上是一样的,二者都是在原有类的基础上,加入一些多出的行为,甚至完全替换原有的行为;而静态代理采用的方式是我们手动的将某些行为进行替换,产生一个新的与原有类接口相同却行为不同的类型;
以淘宝网为例,模拟访问网站的场景;如今大家都经常访问淘宝网,几乎所有的Web项目尤其是像淘宝这样的大型网站,是不可能采用集中式的架构,使用的一定是分布式架构,而分布式架构对于用户来说,当我们发起链接的时候,链接指向的并不是最终的应用服务器,而是代理服务器如Nginx,用作负载均衡[用户访问淘宝网 --> 代理服务器 --> 最终服务器]。
定义一个服务器Server,以及用于获取页面标题的方法getPageTitle:
1 /**
2 * 用于获取网站数据的服务器接口
3 */
4 public interface Server {
5
6 //根据url获取页面标题
7 public String getPageTitle(String url);
8
9 }
访问淘宝网的TaobaoServer,传入url,获取页面标题:
1 /**
2 * 淘宝服务器
3 */
4 public class TaobaoServer implements Server{
5
6 @Override
7 public String getPageTitle(String url) {
8 //简单的if..else判断
9 if ("http://www.taobao.com".equals(url)) {
10 return "淘宝首页";
11 }else if ("http://www.tianmao.taobao.com".equals(url)) {
12 return "淘宝-天猫商城";
13 }
14 return "空空如也";
15 }
16 }
a、若是不使用代理,那么用户访问就相当于直接 new TaobaoServer()并且调用getPageTitle()方法即可;
b、而由于分布式架构的存在,因此需要一个NginxProxy类作为代理,到时候用户直接访问的就是NginxProxy而不直接与TaobaoServer打交道,由NginxProxy负责与最终的TaobaoServer打交道;
NginxProxy代理类:
1 import java.util.List;
2 import java.util.UUID;
3 import com.google.common.collect.Lists;
4
5 /**
6 * Nginx代理
7 */
8 public class NginxProxy implements Server{
9
10 //淘宝服务器列表
11 private static final List<String> TAOBAO_SERVER_ADDRESSES = Lists.newArrayList("127.0.0.1","127.0.0.2","127.0.0.3");
12
13 private Server server;
14
15 public NginxProxy(Server server) {
16 this.server = server;
17 }
18
19 @Override
20 public String getPageTitle(String url) {
21 //UUID模拟请求原始Ip,正常情况下是传入Request的
22 String remoteIp = UUID.randomUUID().toString();
23 //路由选择的算法,这里简单的定义为对remoteIp的Hash之的绝对值取模
24 int index = Math.abs(remoteIp.hashCode()) % TAOBAO_SERVER_ADDRESSES.size();
25 //选择淘宝服务器IP
26 String realTaobaoIp = TAOBAO_SERVER_ADDRESSES.get(index);
27 return "【页面标题: " + server.getPageTitle(url) + "】, 【来源IP: " + realTaobaoIp + "】";
28
29 }
30 }
用于测试,简单设计服务器列表几个IP,同时由于只是传入一个url而不是具体的Request,每次随机一个UUID并且对其hashCode的绝对值取模,模拟该请求被路由到哪台服务器上:
1 /**
2 * 静态代理测试类
3 */
4 public class StaticProxyTest {
5 public static void main(String[] args) {
6 //访问淘宝服务器
7 TaobaoServer taobaoServer = new TaobaoServer();
8 //访问Nginx代理
9 NginxProxy nginxProxy = new NginxProxy(taobaoServer);
10 System.out.println(nginxProxy.getPageTitle("http://www.taobao.com"));
11 }
12 }
由于淘宝服务器和代理服务器实际上都是服务器,So它们可以使用相同的接口Server;用户不和最终目标对象角色TaobaoServer打交道,而是与代理对象角色NginxProxy打交道,由代理对象角色NginxProxy控制用户的访问;多次运行来源IP会改变,其结果如下:
4、静态代理的缺点
静态代理的的特点是静态代理的代理类是由程序猿创建的,在程序运行之前静态代理的.class文件就已经存在了;静态代理在代理量较小时还OK,但是代理量增加就会存在两个比较明显的缺点:
a、静态代理的内容,即NginxProxy路由的选择这几段代码,只能服务一路Server该接口,而不能服务于其他接口,若是其他接口想要使用这几行代码如新增一个代理类,而不断的新增,又由于静态代理的内容无法复用,则必然会造成静态代理类过于庞大;
b、Server接口中若是新增了一个方法,如getPageInfo(String url)方法,而实际对象实现了这个方法,代理对象也必须新增getPageInfo(String url)方法,为其增加代理内容;
5、动态代理
由于静态代理的局限性,孕育而生了动态代理;静态代理是死的,在我们编译期间即按下CTRL+S的那一刻就给被代理对象生成了一个不可动态改变的代理类,而动态代理就是在运行期间动态生成代理类的一种更为灵活的代理模式;动态代理是JDK自带的功能,它需要实现一个InvocationHandler接口,并调用Proxy的静态方法产生代理类。
Nginx InvocationHandler:
1 import java.lang.reflect.InvocationHandler;
2 import java.lang.reflect.Method;
3 import java.util.List;
4 import java.util.UUID;
5 import com.google.common.collect.Lists;
6 /**
7 * Nginx InvocationHandler
8 */
9 public class NginxInvovationHandler implements InvocationHandler {
10
11 //淘宝服务器列表
12 private static final List<String> TAOBAO_SERVER_ADDRESSES = Lists.newArrayList("127.0.0.1","127.0.0.2","127.0.0.3");
13
14 private Object object;
15
16 public NginxInvovationHandler(Object object) {
17 this.object = object;
18 }
19
20 @Override
21 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
22 String remoteIp = UUID.randomUUID().toString();
23 int index = Math.abs(remoteIp.hashCode()) % TAOBAO_SERVER_ADDRESSES.size();
24 String realTaobaoIp = TAOBAO_SERVER_ADDRESSES.get(index);
25 //执行速度:StringBuilder > StringBuffer > String
26 StringBuilder sb = new StringBuilder();
27 sb.append(" 【页面标题: ");
28 sb.append(method.invoke(object, args));
29 sb.append("】,【来源Ip: ");
30 sb.append(realTaobaoIp);
31 sb.append("】");
32 return sb.toString();
33
34 }
35 }
上面的动态代理NginxInvovationHandler 将选择服务器的逻辑抽象成公共的代码,因为调用的是Object中的method,而Object又是所有类的超类/父类,So并不只限定于接口Server,任意的其他接口都可以使用该服务,因此该NginxInvovationHandler可以灵活的各处复用。
动态代理测试:
1 import java.lang.reflect.InvocationHandler;
2 import java.lang.reflect.Proxy;
3
4 /**
5 * 动态代理测试类
6 */
7 public class DynamicProxyTest {
8 public void testDynamicProxy(String url) {
9 TaobaoServer taobaoServer = new TaobaoServer();
10 InvocationHandler invocationHandler = new NginxInvovationHandler(taobaoServer);
11 //使用Proxy的newProxyInstance方法可以产生对目标接口对一个代理,代理内容则由InvocationHandler实现
12 Server proxy = (Server)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {Server.class}, invocationHandler);
13 System.out.println(proxy.getPageTitle(url));
14 }
15 public static void main(String[] args) {
16 String url = "http://www.taobao.com";
17 DynamicProxyTest dynamicProxyTest = new DynamicProxyTest();
18 dynamicProxyTest.testDynamicProxy(url);
19 }
20
21 }
动态代理程序运行结果:
6、动态代理的优/缺点
优点:
a、减少了类的数量,看起来比较直观;
b、代理内容可以复用;
c、最重要的是动态代理可以在不修改源代码的基础上在源代码上进行操作,如在吐司上抹果酱的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(一种字节码增强技术)来为某一个类实现代理。
7、GCLIB动态代理
HelloService业务类:
1 /**
2 * 没有实现任何接口的业务类
3 */
4 public class HelloService {
5
6 public HelloService() {
7 System.out.println("HelloService构造");
8 }
9
10 //该方法不被覆盖,cglib无法代理由final修饰的方法
11 final public String sayOthers(String name) {
12 System.out.println("HelloService : sayOthers >> " + name);
13 return null;
14 }
15
16 public void sayHello() {
17 System.out.println("HelloService : sayHello");
18 }
19
20 }
自定义MethodInterceptor类:
1 import java.lang.reflect.Method;
2 import org.springframework.cglib.proxy.MethodInterceptor;
3 import org.springframework.cglib.proxy.MethodProxy;
4
5 /**
6 * 自定义MethodInterceptor
7 */
8 public class MyMethodInterceptor implements MethodInterceptor{
9
10 /**
11 * sub: cglib生成的代理对象
12 * method: 被代理对象方法
13 * objects: 方法入参
14 * methodProxy: 代理方法
15 */
16 @Override
17 public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
18 System.out.println("<<<<<<<<<<<<插入前置通知>>>>>>>>>>>>");
19 Object object = methodProxy.invokeSuper(sub, objects);
20 System.out.println("<<<<<<<<<<<<插入后置通知>>>>>>>>>>>>");
21 return object;
22 }
23
24 }
CGLIB代理对象调用目标方法:
1 import org.assertj.core.internal.cglib.core.DebuggingClassWriter;
2 import org.springframework.cglib.proxy.Enhancer;
3
4 /**
5 * CGLIB代理对象调用目标方法
6 */
7 public class Client {
8 public static void main(String[] args) {
9 //代理类class文件存入本地磁盘,方便反编译查看源码
10 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/TJT/Code");
11 //通过cglib动态代理 获取代理对象的过程
12 Enhancer enhancer = new Enhancer();
13 //设置enhance 对象的父类
14 enhancer.setSuperclass(HelloService.class);
15 //设置enhance 的回调对象
16 enhancer.setCallback(new MyMethodInterceptor());
17 //创建代理对象
18 HelloService proxy = (HelloService)enhancer.create();
19 //通过代理对象调用目标方法
20 proxy.sayHello();
21 System.out.println();
22 //sayOthers() 被fianl修饰不能被cglib代理
23 proxy.sayOthers("涛姐涛哥");
24 System.out.println();
25 proxy.sayHello();
26 }
27
28 }
CGLIB动态代理程序运行结果如下: