一、概念
结构型模式
给某一对象提供给一个代理类,并由代理对象控制对原对象的引用。
代理模式实现(三要素)
一句话:客户端通过代理类来调用目标方法,代理类会将所有的方法调用分派到目标对象上反射执行,还可以在分派过程中添加"前置通知"和后置处理。
- 一个接口,接口中的方法是具体要实现的;
- 一个被代理类,实现上述接口, 这是真正去执行接口中方法的类;
- 一个代理类,实现上述接口,同时分装被代理类对象,帮助被代理类去实现方法。
代理模式分为静态代理模式和动态代理模式,首先我们来看下静态代理模式。
二、静态代理模式
这里模拟的是网站的访问。几乎所有的Web项目尤其是大型网站,不可能采用集中式的架构的,必定是采用分布式架构。 对于分布式架构,但用户发起请求的时候,请求指向的并不是最终的服务器端,而是代理服务器,比如Nginx,用以负载均衡。
示例:用户访问网站->代理服务器->最终服务器。先定义一个服务器接口Server,简单定义一个方法:
1 /** 2 * 服务器接口,用于获取网站数据 3 */ 4 public interface Server { 5 6 /** 7 * 根据url获取页面标题 8 */ 9 String getPageTitle(String url); 10 11 }
例如我们访问的是百度,所以定义一个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 }
若不采用代理,那么访问网站,就会直接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 }
这里同样为了简单起见,服务器列表写死几个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类
- proxy:代理类对象
- method:被代理的方法
- args:被代理的方法的参数列表
- loader:类加载器
- interfaces:代理类实现的所有接口
- h:InvocationHandler接口的一个实例 this当前对象
示例
将上述的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 }
这里就将选择服务器的逻辑抽象成为了公共的代码了,因为调用的是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代理,需要实现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接口
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 }); }
示例
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 }
关于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中的应用以及解读
那分别在什么场景下使用JDK动态代理和CGLIB动态代理?
- 如果加入容器的目标对象有实现接口,用JDK代理
- 如果目标对象没有实现接口,用Cglib代理
- 如果目标对象实现了接口,且强制使用cglib代理,则会使用cglib代理
Spring中强制使用Cglib代理
<aop:aspectj-autoproxy proxy-target-class="true" />
SpringBoot中强制使用Cglib代理