前几天有个小朋友问到我代理模式,这个我拿手的嘛。废话不多说,直接开讲?这是不可能的。
一般呢,讲模式之前呢,礼貌上都要讲个前传。象我这种老了根本不怕没有故事祭奠的人,随手一个前传开始。
区区在下做为一名平时不怎么修边幅的小女子,突然之间接到电话,一个long long ago之前暗恋过的男神之一打来的。一直暗恋,从没明恋,他也从不知道曾被我暗恋过的男神说他出差到广州,老同学很多年没见了,问要不吃个便饭?那,就吃一个?怎么的我也算半个地主吧?
挂上电话之后,我浅浅的思考了一下,随随便便搞了一个购物清单。
虽早已不恋,男神之一当年仅有的最突出的优点---帅也早已不复存在,存在的只有突出的大肚子。但,什么也不能阻止我此时默默地开个屏~~
先找帽子,和卖帽子的店家进行售前沟通、咨询、比较,再找外套,然后衬衣~~~基本上是这样子的
就,心好累~~
简单一列就这样了,还不包括化妆品,护肤品,小饰品~~万一想穿裙子呢?要搞多几个造型对比选择呢?看着最后几个小……,世界已无爱~~确定要开屏吗?不认真地思考了一下,还是要的~~
然后看一眼,我买东西的店家其实也辛苦,它们要和我进行售前咨询,交易完成后还要保持与我的售后沟通服务。
这样看,每个商家每天要面对大量的客户,也是累并快乐着呀~~
如果,我是说如果哈,如果此时我有一名造型师呢?情况就变成这样子了
我只需要将我的需求告诉造型师,造型师帮我去找到各种商品。并保证我的售后沟通。
各商家呢?也再不用管售前咨询和售后沟通的问题,专注核心事业:搞产品。完美~~
至此,结论就来了:造型师,就是代理类。各商家就是被代理的类。我就是客户端的请求。
什么男神呀,都是不存在的,浮云。我爱代码,代码爱我,我为代码开个屏。
代理模式前传,懂?前几天问我代理模式的小朋友注意了哈,敲黑板了~~
没看懂的告诉我,看懂了的也可以告诉我。
先看懂前传,咱们后面接着静态代理~~
好了,正传开始,此处已无男神~~区区在下小女子目下无尘,只有代码。
现在,我们开始假装要搞一个商城了。商城先得有商品吧,有商品就得把商品存到数据库里去吧。那我们就在此顺势建一个将商品持久化的类。(注:此处可能有小朋友会举手问什么是持久化,持久化你目前可以理解为就是把商品的数据保存到数据库里,以后想看,可以拿出来看。持久化了的东东,就不是只活在内存里,一关机就不见了。而是放到数据库里,保存到硬盘上了的。如果还有小朋友想问什么是内存,什么是硬盘,来,给钱我,我慢慢告诉你~~放心,我不得打小朋友的~~)。
将商品保存到数据库,增删改查是必须的。但查太简单了,so easy(你确定?),暂时不理它~~(注:此处是胡说八道,实际情况是,查我们要单独处理,容后再说~~可能也许大概吧~~)
就此决定,先建三个方法:增删改:
|
|
正常运行,打印出"删除商品" 没有问题。
好了,这三个特别特别复杂的方法我们写好了,核心功能都实现了(是吧,是吧,都打印出来了的~~)。然后,对数据库操作,得有事务吧(不懂什么是事务的小朋友请举手,我们另外处理。),得写日志吧,还得先判断当前登录用户对这个商品是否有操作权限吧?(注:看明白了吧?至少查确实不能开个事务来独占资源,所以不一起说~~)
于是,这三个特别复杂的方法瞬间又上一个档次的复杂了起来。
|
代码这么多,还大多是重复的,这不应该是人干的活,这应该是ChatGPT干的事儿哇~~纵观以上代码,每个方法中,核心业务只有一行~~边边角角,又必须存在的代码有4行之多~~所以,我们必须想个办法消灭它们,让我们核心突出。主角得有主角的待遇。
所以,我们另外建一个类,专注处理边边角角,可以吗?那是当然~~来,走起~~
首先,这个代理类有商品持久化类ProductDaoImpl的各同名方法。
|
类里只需要一个成员变量productDao,该成员变量的类型就是ProductDaoImpl。
|
要保证只要代理类ProxyDao的对象被创建,其属性productDao就必须是一个已构建好的ProductDaoImpl的对象,而不会是null。所以用带参构造方法给productDao赋初值。ProxyDao只有一个构造方法,是带一个参数的构造方法,参数就是ProductDaoImpl的对象。
|
在代理类ProxyDao的各方法中,调用属性productDao的同名方法。
|
代码写到这里,相信大家也看出来了,我们使用这个代理类ProxyDao的各个方法,与直接使用商品持久化类ProductDaoImpl的各个方法,产生的效果是一样的。那么,如果我们在代理类ProxyDao的各方法前后各加一些边边角角的内容呢?象这样?
|
这样是不是商品持久化类ProductDaoImpl中的代码,只需要保留核心的那一行代码就可以了?
此时客户端测试,就不是直接调商品持久化类ProductDaoImpl对象的方法了,而是调代理类ProxyDao对象的方法。
|
再观察一下,我们还可以把代理类ProxyDao中重复的代码再抽一抽,该封装就封装一哈。
在客户端,productDao这个变量也只是做为参数传一传,就没用了。调方法根本不关它事,于是,感觉它可以不必拥有姓名?我们在new 代理类的时候,传一个商品持久化类ProductDaoImpl的匿名对象进去,不香吗?外面根本找不到,不用在客户端扰我视线,乱我思维。
于是最后代码成了这样
属性productDao是商品持久化类ProductDaoImpl的对象。并通过一个有参构造进行初始化。
ProxyDao类中有与ProductDaoImpl类相同的public方法,用于调用ProductDaoImpl类中的同名方法,处理核心业务。并在调用前后,进行事务、日志、判断操作权限等非核心的相关业务的处理。
注:此例中,将核心功能前后的业务处理都设计为相同,并分别封装在begin()和last()两个私有方法中。实际应用中,可根据具体情况进行处理。
|
商品持久化类ProductDaoImpl又恢复了原样,边边角角都交给别人去处理了,拒绝拼盘,从代理做起。
|
此时客户端测试
|
运行结果:
代理了?对不对?代理了哇~~别激动,这才刚开始呢~~
这个时候呢,我们就得思考了,商城哇,怎么也得有订单吧?订单呢?要不来个订单持久化先?
于是订单持久化来了
|
那订单持久化的边边角角怎么办呢?又搞一个代理类?格局小了吧~~
此时,理应接口闪亮登场了~~请跟着我念:面向接口编程~~
增加一个接口,并修改两个地方:
接口有增删改三个方法,与商品持久化类ProductDaoImpl的方法一致。
订单持久化类OrderDaoImpl和商品持久化类ProductDaoImpl都实现这个接口
代理类ProxyDao的成员变量改为接口,构造方法的形参也改为接口。
代码如下:
|
|
商品持久化类ProductDaoImpl
|
订单持久化类OrderDaoImpl
|
写到这里,是不是觉得想怎么代理就怎么代理了?轻松方便?
搞个测试?
|
运行结果:
好象挺完美了?世界有我,我有世界~~
但是,你以为就这样结束了吗?告诉你,并没有~~~
那设计模式,不是开玩笑的,是老几辈优秀的科学家智慧的结晶,严谨得很的~~(注:是的,long long ago之前的程序员这活,只有科学家才干得了~~)
谁能思考出哪里不严谨吗?举手没奖。
那个,代理类ProxyDao的几个public方法,是不是要和被代理的类的方法一致呀?所以呢,ProxyDao是不是也可以和被代理的类实现同一个接口呢?这样就能保证两者的方法一致,不至于被写错了?请再跟着我念:面向接口编程~~
所以,最后一改,就是把ProxyDao类改成接口的实现类。
本次静态代理模式示例代码最终的完整呈现如下:
|
|
商品持久化类
public class ProductDaoImpl implements IGeneralDao { public void insert(){ System.out.println("新增商品"); } public void update(){ System.out.println("修改商品"); } public void delete(){ System.out.println("删除商品"); }
} |
订单持久化类
public class OrderDaoImpl implements IGeneralDao { public void insert(){ System.out.println("新增订单"); } public void update(){ System.out.println("修改订单"); } public void delete(){ System.out.println("删除订单"); }
} |
public class ProxyTest { public static void main(String[] args) { IGeneralDao proxyDao=new ProxyDao(new ProductDaoImpl()); proxyDao.delete(); System.out.println("-----------------------------"); proxyDao=new ProxyDao(new OrderDaoImpl()); proxyDao.insert(); }
} |
运行结果:
好了,静态代理打完收工。都到这份上了,类图可以自己画否?
静态代理只能代理指定接口的实现类。即一个类只要实现了接口,就可以用一个实现了同一接口的代理类来代理它。那么问题来了,如果我有两个类,是分别实现了不同的接口,其内部方法各不相同,又都想有代理,且代理内容一样,怎么办呢?生成两个代理类?
比如,我们有一个订单管理类,一个商品管理类。订单管理类需要新增订单、更改订单状态、查询订单信息。商品管理类需要上架商品,下架商品,更新库存,查询商品信息。以上各业务每接受一次请求都需要写入日志。
我们学过静态代理模式,可以很快确定,写日志这件事,交给代理类去做,订单管理类和商品管理类专注核心功能即可。但,这就需要建两个代理类?这两个代理类干的边边角角的活还一样?明显不优雅了吧。
于是,动态代理他来了。
JDK提供了一种动态代理,只要被代理的类是实现了接口的类,就能被代理。注意:是只要实现了接口就可,不是要实现同一个接口才可。区别很大,思考一下就感觉到天地宽广了许多。
但有些类,它就是没有实现接口,又还是想要有代理类帮它处理边边角角怎么办呢?不慌,我们有CGLib动态代理。它不是java自带的,而是由第三方提供的优秀类库。CGLib动态代理的被代理类不需要实现接口,只要是能被继承类,都能被代理。(注:即被final修饰的类不能实现CGLib动态代理)
jdk动态代理,存在于java自带的核心内库,不需要引入jar包依赖什么的。
现在我们来回忆一下静态代理,代理类与被代理类实现了相同的接口,它们具有相同的方法进行代理和被代理。总结:我们需要代理类与被代理类的方法相同。
那么可否有一个方法,传出与被代理对象继承了同一个接口的代理对象。这样,我们就可以得到一个与被代理对象有同样的方法的代理对象了。
当然,方法得写在内中,那就建一个生产动态代理对象的类JdkDynamicProxy
被代理的对象还是设为类属性,通过构造方法传入。因为被代理类的类型不确定,设为Object类型。(注:别忘了所有实现了接口的类都可以做为被代理类,所以我们用顶级父类来接。)
方法getProxy()返回与被代理类实现了同一个接口的对象做为代理对象。同样,因为被代理类的类型不确定,这个方法的返回值也设为Object.
public class JdkDynamicProxy {
//被代理对象 private Object obj;
//一个参数的构造方法,传入被代理对象 public JdkDynamicProxy(Object obj){ this.obj=obj; } /** * * @return 代理类对象。它与被代理对象实现同样的接口 */ Object getProxy(){ return 返回一个与 obj实现了相同接口的对象,做为代理类; }
|
来个客户端测试
public class JdkDynamicProxyTest { public static void main(String[] args) { JdkDynamicProxy jdkDynamicProxy=new JdkDynamicProxy(new ProductDaoImpl()); IGeneralDao proxy =(IGeneralDao) jdkDynamicProxy.getProxy(); proxy.delete(); }
} |
好象架子搭起来了?
只是目前还有两个问题没解决
怎么造一个实现了被代理对象的接口的类,并生成一个代理对象出来。
proxy.delete()调用的方法在哪里?怎么写代码?
先来解决第一个问题:怎么造一个实现了被代理对象的接口的类,并生成一个代理对象出来。
此时Proxy类闪亮登场。Proxy类在java.lang.reflect包中,使用它的静态方法newProxyInstance()就可以得到我们想要的代理对象。
此方法需要传入三个参数,返回值是Object.很明显,返回的Object就是我们需要的代理对象。
参数列表:
1. ClassLoader loader:被代理对象的类加载器,用于定义代理类
得到类加载器的代码实现:
ClassLoader classLoader=obj.getClass().getClassLoader(); |
2. Class>[] interfaces:被代理对象实现的接口列表,代理类统统都要实现。(友情提醒,java类是可以实现多个接口的,所以这里是个数组)
得到接口列表代码实现:
Class>[] interfaces=obj.getClass().getInterfaces(); |
事情进展到这里,我们已经得到了类加载器,可以造类了。也得到了接口列表,可以造一个把这列表里的接口统统实现了的类,没问题吧。类动态构造好了,类里的方法又怎么动态写呢?比如:delete()这个方法,怎么在代理对象里加上边边角角,在核心被代理类的delete()方法里走一圈,又回到代理对象里加边边角角呢?newProxyInstance方法的第三个参数帮你解决所有疑问。
3. InvocationHandler h:
InvocationHandler是一个接口,那我们就写一个这个接口的实现类,再new它的对象传进去试试先?
这个接口只有一个方法invoke()方法。我们需要一个实现了这个接口的对象做为参数传入,只要实现这一个方法就可以了。不管三七二十一,走一波看看效果。
注:暂时将这个类写成JdkDynamicProxy的内部类
/** * InvocationHandler的实现类 */ class InvocationHandlerImpl implements InvocationHandler{ @Override public Object invoke( Object proxy, Method method, Object[] args) throws Throwable { //注意这句,测试效果看这里 System.out.println("成功了"); return null; }
} |
/** * @param obj 被代理的对象 * @return 代理对象,它与被代理对象实现同样的接口 */ public Object getProxy(){ //得到被代理对象的类加载器 ClassLoader classLoader= obj.getClass().getClassLoader(); //得到被代理对象实现的接口列表 Class>[] interfaces= obj.getClass().getInterfaces(); Object o = Proxy.newProxyInstance( classLoader, interfaces, new InvocationHandlerImpl()); // return o; } |
客户端测试运行结果:
显然有运行invoke方法
我们的俄罗斯套娃又进去一层,再来分析invoke()方法吧。
传入参数有三个
Object proxy:代理对象
Method method:要执行的方法(如delete)
Object[] args: 要执行的方法的参数列表(此例中delete方法没有参数,则args为null)
返回值:
Object:执行的方法的返回值(此例中delete的返回值是void)
这个方法里要怎么干,好像也很明显了?反射,强大的无所不能的反射出现了。
我们有对象,有方法,还怕不能调用吗?不可能撒。于是我们把这个方法改成这样试一试呢
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("进来代理了"); Object result=method.invoke(obj); System.out.println("我写日志了"); return result;
}
|
客户端测试,运行结果:
代理了成功!
动态代理成功,但还没打完。
我们继续思考一下那个内部类,它的存在好象有些累赘?不想要它,觉得碍眼,不优雅。此时我们有两种解决方案。
1. 用JdkDynamicProxy来实现InvocationHandler接口,重写invoke方法。在调用newProxyInstance方法时传入this即可。
2. 在调用newProxyInstance方法时,第三个参数直接new一个匿名内部类对象,用这个匿名内部 类实现InvocationHandler接口,并重写invoke方法。
顺手把边边角角也封装到begin 和last方法中
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class JdkDynamicProxy implements InvocationHandler{ //被代理对象 private Object obj; //一个参数的构造方法,传入被代理对象 public JdkDynamicProxy(Object obj){ this.obj=obj; } /** * * @return代理类对象。它与被代理对象实现同样的接口 */ public Object getProxy(){ //得到被代理对象的类加载器 ClassLoader classLoader= this.obj.getClass().getClassLoader(); //得到被代理对象实现的接口列表 Class>[] interfaces= this.obj.getClass().getInterfaces(); Object o = Proxy.newProxyInstance( classLoader, interfaces, this); return o; } @Override public Object invoke( Object proxy, Method method, Object[] args) throws Throwable { begin(); Object result=method.invoke(obj); last(); return result; } private void begin(){ System.out.println("进来代理了"); } private void last(){ System.out.println("我写日志了"); }
} |
注意这个方式中 调用Proxy.newProxyInstance()方法的第三个参数,是直接new的一个匿名类内部的对象,个人感觉有点乱,不是太推荐这种写法。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class JdkDynamicProxy{ //被代理对象 private Object obj; //一个参数的构造方法,传入被代理对象 public JdkDynamicProxy(Object obj){ this.obj=obj; } /** * * @return代理类对象。它与被代理对象实现同样的接口 */ public Object getProxy(){ //得到被代理对象的类加载器 ClassLoader classLoader= this.obj.getClass().getClassLoader(); //得到被代理对象实现的接口列表 Class>[] interfaces= this.obj.getClass().getInterfaces(); Object o = Proxy.newProxyInstance( classLoader, interfaces, new InvocationHandler() { @Override public Object invoke( Object proxy, Method method, Object[] args) throws Throwable { begin(); Object result=method.invoke(obj); last(); return result; } }); return o; } private void begin(){ System.out.println("进来代理了"); } private void last(){ System.out.println("我写日志了"); }
} |
客户端测试类
public class JdkDynamicProxyTest { public static void main(String[] args) { //将生成的代理类字节码文件写到磁盘上 路径在当前项目目录下 /com/sun/proxy目录下 System.getProperties(). put("sun.misc.ProxyGenerator.saveGeneratedFiles","true"); JdkDynamicProxy jdkDynamicProxy=new JdkDynamicProxy(new ProductDaoImpl()); IGeneralDao proxy =(IGeneralDao) jdkDynamicProxy.getProxy(); proxy.delete(); }
} |
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true"); 将生成的代理类字节码文件写到磁盘上 路径在当前项目目录下 /com/sun/proxy目录下
JDK动态代理讲完
今天讲另一种动态代理,CGLib动态代理。前面说过,JDK动态代理能代理实现了接口的类,那如果一些类没有实现任何接口,硬是需要动态代理一批批呢?这种情况,JAVA基本类库是不管的,但我们不慌。JAVA社区这么大,还能解决不了这问题?于是CGLib来了。
JDK动态代理的工作原理就是:根据接口,动态造一个实现类,实现一样的方法来达到代理的目的。
CGLib没有接口,那就反向思考,没有上一代我就造一个下一代。造个子类总能得相同的方法了吧。于是,造子类。动态造一个被代理类的子类做为代理类。
这是一个第三方类库,所以我们要导包。
普通java项目导两个包
cglib-3.3.0.jar 和 asm-7.1.jar
记着别漏了asm-7.1.jar包,否则会报错
maven项目呢,依赖如下:
|
准备工作完成,下面开始套娃讲解。
还是熟悉的配方,此时我们需要一个生产动态代理类,并造出代理对象的类。这个类得有一个属性,是被代理对象。
public class CGLibDynamicProxy { //被代理对象 private Object obj; //一个参数的构造方法,传入被代理对象 public CGLibDynamicProxy(Object obj){ this.obj=obj; } /** * * @return 代理对象。它与被代理对象实现同样的接口 */ public Object getProxy(){ return proxy; }
|
我们已知,getProxy方法需要造一个子类出来,再给这个子类造一个对象。那么,怎么造子类?
这里会用到这个类的三个方法:
1. setSuperclass(Class) 字面意思:设置父类,形参是一个Class。很明显,把被代理类传进去就可以了。
2. setCallback(Callback) 形参是一个空接口Callback。这个什么用呢?下层套娃再说,先放这里。
3. create()。没有参数,返回一个Object。这个就是造代理类和对象的方法了。
现在,我们可以来稍微完善一下getProxy()方法了。
public Object getProxy(){ Enhancer enhancer=new Enhancer(); enhancer.setSuperclass(obj.getClass()); enhancer.setCallback(?); Object proxy=enhancer.create(); return proxy;
} |
事情进展到这里,根据已有经验,还缺处理代理的方法。MethodInterceptor接口亮相的时候到了。这个接口继承了Callback接口,且必须实现的方法就一个intercept()。
参数:
Object 代理对象
2. Method 被代理对象要执行的方法
3. Object[] 被代理对象要执行的方法传入的参数列表
4. MethodProxy 生成的代理对象要执行的方法
返回值:
Object :执行的代理对象的方法的返回值
代码实现:
@Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("进来代理了"); Object result=method.invoke(obj,objects); System.out.println("我写日志了"); return result;
} |
此时就很明显了,我们需要一个实现了MethodInterceptor接口的类,在intercept方法中书写我们代理方法的代码。这个类自然也是实现了Callback接口的,于是可以做为Enhancer对象的setCallback方法参数。既然如此,我们不如就直接将CGLibDynamicProxy类做为MethodInterceptor接口的实现类。懒得再多写了嘛,而且还能保持优雅。
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CGLibDynamicProxy implements MethodInterceptor { //被代理对象 private Object obj; //一个参数的构造方法,传入被代理对象 public CGLibDynamicProxy(Object obj){ this.obj=obj; } /** * * @return 代理对象。它与被代理对象实现同样的接口 */ public Object getProxy(){ Enhancer enhancer=new Enhancer(); enhancer.setSuperclass(obj.getClass()); enhancer.setCallback(this); Object proxy=enhancer.create(); return proxy; } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { begin(); Object result=method.invoke(obj,objects); last(); return result; } private void begin(){ System.out.println("进来代理了"); } private void last(){ System.out.println("我写日志了"); }
} |
客户端测试:
public class CGLibDynamicProxyTest { public static void main(String[] args) { CGLibDynamicProxy cgLibDynamicProxy= new CGLibDynamicProxy(new ProductDaoImpl()); IGeneralDao proxy= (IGeneralDao)cgLibDynamicProxy.getProxy(); proxy.insert(); }
} |
运行结果:
整个代理模式,打完收工。
要不大家就以下场景写个代理?
约了朋友去海底捞吃饭,一去,发现人多得不得了,根本找不到位置。服务员小姐姐就特别热情的招呼我们,登记排队、搬凳子、拿水果、小吃、帮忙登记排队涂指甲什么的,搞得热闹得很。直到店里通知:有位置了(有资源),可以进来了。于是进店。
好不容易排队进去了,点菜之后继续等。服务员小哥哥又来忙了,端茶倒水、送热毛巾、上水果拼盘等等。终于厨房表示菜做好了(资源有了),于是上菜的小哥把菜上来了。
店里只提供两个资源:店中位置,菜品。
服务员就是代理,他们不断地询问有位没?有菜没?如果没有,他们就继承服务,直到我们需要的资源有了(店内座位或菜品),他们停止询问,我们获得资源。