1 引言:代理模式
故事要从代理(Proxy)模式说起。代理模式是一种设计模式,举个例子来理解。
你的老板终于要发工资了,但是睾贵的老板怎么可能亲自给员工发工资呢?所以老板肯定是叫管账的会计来发工资。这个会计就是介于你和老板之间的发工资代理人。也就是说,“老板发工资”这一行为,并不是直接由老板和你交互,而是老板经由中间人跟你交互。那这样的模式有什么好处呢?对老板而言,你只是底层员工,而他尊贵显赫,通过代理模式,他就可以不用与你这种下民打交道。对作为中间人的会计而言,好处就更大了,他这时候就可以尽情地干偷鸡摸狗的事了。假设老板给会计100元让他发给10个人,每个人10块,但是会计作为发工资代理人,在工资发到你手上之前,他完全可以从中作梗,每个人抽走5块给自己。试想如果不采用代理模式,老板直接把10块钱给你,会计还有机会抽走一部分钱吗?
例子虽然负面,但是代理模式的特点也体现出来了。代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。再举个例子。
老师收作业都不直接收的,一般是叫班长学委或者助教之类的人去收。有一次,因为你交作业的前一晚不小心把班长的女朋友给上了,被班长发现了,于是班长怀恨在心,准备报复你。今天你交作业给班长之后,班长在你的作业上写了个“老师你个傻叉,劳资早就看你不爽了”,然后再交给老师。于是你,卒。
这个例子中,班长就是代理,他可以在你把作业转交给老师的途中引入一定的间接性,从而达成某些目的。
现在看一下代理模式正常的描述,相信这时候已经非常好理解了。
代理模式是常用的Java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。简单的说就是,我们在访问某个对象(即委托类的对象)时,是通过代理对象来访问的。代理模式就是在访问委托类对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。
上面的描述中,代理类其实就是之前例子中的会计和班长,委托类其实就是老板和老师。委托类本来是直接和你交互,现在它委托代理类去完成这个交互,你们的交互要途经代理类这个中间人。具体的实现方案是代理类和委托类实现共同的接口,然后在代理类接口的实现中调用委托类接口的方法,那么你在想和委托类交互时,是通过“你调代理类接口,代理类再调委托类接口”的方式实现的。在代理类调委托类接口的前后去进行一些预处理或后处理的操作即可方便地达成一些其他的目的。
2 静态代理
静态代理就是程序运行之前你就把代理类写好。
看老师的那个例子吧。先写个接口,定义交互行为。
public interface IHomeworkManager {
//name标识是谁的作业,用一个homework字符串代替作业内容,boolean类型的返回值表示交作业是否成功了的回应
boolean giveMeHomework(String name, String homework);
}
然后写个老师类(委托类)和班长类(代理类)。
public class Teacher implements IHomeworkManager {
@Override
public boolean giveMeHomework(String name , String homework) {
System.out.println("老师看到了" + name + "的作业,内容是:" + homework);
//你在作业里骂老师,肯定交作业失败,除此之外都认为交作业成功
return !homework.contains("老师傻叉");
}
//老师的代理人,即班长
public static class TeacherProxy implements IHomeworkManager {
Teacher teacher;
public TeacherProxy(Teacher teacher) {
this.teacher = teacher;
}
@Override
public boolean giveMeHomework(String name, String homework) {
System.out.println("班长收到了" + name + "的作业,内容是:" + homework);
//你把班长女朋友绿了,所以他怀恨在心要搞你
if (name.equals("你"))
homework = "老师傻叉,劳资早就看你不爽了";
//班长把作业转交给老师,由老师最终决定作业有没有交成功
return teacher.giveMeHomework(name, homework);
}
}
}
然后测试一下。
public class StaticProxyTest {
public static void main(String[] args) {
//老师
Teacher teacher = new Teacher();
//班长
Teacher.TeacherProxy teacherProxy = new Teacher.TeacherProxy(teacher);
//现在你把作业交给班长了
boolean isSuccess = teacherProxy.giveMeHomework("你", "床前明月光,疑是地上霜");
System.out.println("交作业" + (isSuccess ? "成功" : "失败"));
}
}
最后看一下结果。
班长收到了你的作业,内容是:床前明月光,疑是地上霜
老师看到了你的作业,内容是:老师傻叉,劳资早就看你不爽了
交作业失败
静态代理就是这样了。
3 动态代理
3.1 动态代理的概念和写法
动态代理和静态代理的区别就是动态和静态的区别。静态代理是你自己得在程序跑之前把代理类写好,而动态代理不需要你提前写好代理类,你可以通过Java代码在运行时自动生成一个代理类,现在动态代理的好处还体现不出来,别急,先看看代码怎么写,之后再讲好处。
还是刚才的例子吧,对比静态代理,Teacher类没有变动,有变动的地方是,我们不需要写TeacherProxy类,而改成了一个ProxyHandler类,这个类需要实现InvocationHandler接口。这个ProxyHandler其实就代替了TeacherProxy类的作用。
public class ProxyHandler implements InvocationHandler {
private Object target;
//让代理绑定委托类对象,这个例子里target就是老师,绑定以后返回一个动态生成的代理类对象,即班长
//搞不懂ClassLoader等传参是啥的话,就照着一模一样写就行了,这里的代码都是写死的
//这个ProxyHandler类的核心在底下的invoke方法里
public Object bind(Object target) {
this.target = target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), this);
}
//method是你调了哪个方法,args是method方法的传参
//这个例子里method就是giveMeHomework(),因为你在底下测试的代码里先把ProxyHandler对象强转成了IHomeworkManager类型,然后你调用了iHomeworkManager的giveMeHomework()方法,最终体现为ProxyHandler类里的这个invoke方法被调用了
@Override
public Object invoke(Object o, Method method, Object[] args) throws Throwable {
System.out.println("班长收到了" + args[0] + "的作业,内容是:" + args[1]);
//班长又要搞你了
if (args[0].equals("你")) {
args[1] = "老师傻叉,劳资早就看你不爽了";
}
//执行method方法,即target(即teacher)的giveMeHomework()方法
return method.invoke(target, args);
}
}
Teacher类没有变动,现在你要交作业了,我们测试一下。
public static void main(String[] args) {
//TeacherProxy类不再需要,我们用ProxyHandler取而代之,先让ProxyHandler绑定一个老师,然后强转成IHomeworkManager类型
IHomeworkManager iHomeworkManager = (IHomeworkManager) new ProxyHandler().bind(new Teacher());
//然后调用giveMeHomework()方法交作业,ProxyHandler类里的invoke方法就会被调用(即作业交给了班长),然后在invoke方法里又会调用target的giveMeHomework()方法(即作业交给了老师),整个流程非常像静态代理
boolean isSuccess = iHomeworkManager.giveMeHomework("你", "床前明月光,疑是地上霜");
System.out.println("交作业" + (isSuccess ? "成功" : "失败"));
}
最后看一下结果,和静态代理一模一样。
班长收到了你的作业,内容是:床前明月光,疑是地上霜
老师看到了你的作业,内容是:老师傻叉,劳资早就看你不爽了
交作业失败
3.2 动态代理的作用
上面就是动态代理的写法了,但是看到这里,动态代理的优势貌似不是很明显,现在我们让场景稍微变动一下。
现在班长是个海王,养了一池塘鱼。你挨个把班长的七八个女朋友全部上了,你真是太厉害了。班长在得知这件事以后整个人气炸了,于是决定不仅要搞你,而且要搞死你,往死里搞。所以,他原来只改了你一门课的作业,现在你每门课的作业他都要改,让你得罪所有老师,生不如死。
对于上面的例子,如果采用静态代理,那么假设有三个不同的老师类,即有三个不同的委托类,那么你是不是需要写三个相应的代理类?再有,现在你和老师的交互仅仅限于交作业一件事,即IHomeworkManager接口里只有一个方法,假设这时候需要修改或新增方法,有三个不同的老师类的情况下,是不是得去所有老师类和代理类里实现这些接口?一旦接口有变动,委托类和代理类都要同时修改,维护起来非常不方便。
总结一下,静态代理主要有两个缺点。
- 当有多个委托类的时候,需要自己写多个代理类,烦
- 当委托类和代理类共同实现的接口有改动的时候,所有类都得去修改,烦
那么接下来体会一下动态代理是如何避免这两个问题的。
先写三个老师类,并实现IHomeworkManager接口。
public abstract class AbstractTeacher implements IHomeworkManager {
String whatTeacher;
@Override
public boolean giveMeHomework(String name, String homework) {
System.out.println(whatTeacher + "老师看到了" + name + "的作业,内容是:" + homework + "\n");
return !homework.contains("老师傻叉");
}
}
public class MathTeacher extends AbstractTeacher implements IHomeworkManager {
public MathTeacher() {
whatTeacher = "数学";
}
}
public class ChineseTeacher extends AbstractTeacher implements IHomeworkManager {
public ChineseTeacher() {
whatTeacher = "语文";
}
}
public class EnglishTeacher extends AbstractTeacher implements IHomeworkManager {
public EnglishTeacher() {
whatTeacher = "英语";
}
}
现在我们采用动态代理交作业。
public static void main(String[] args) {
IHomeworkManager mathHomeworkManager = (IHomeworkManager)
new ProxyHandler().bind(new MathTeacher());
IHomeworkManager chineseHomeworkManager = (IHomeworkManager)
new ProxyHandler().bind(new ChineseTeacher());
IHomeworkManager englishHomeworkManager = (IHomeworkManager)
new ProxyHandler().bind(new EnglishTeacher());
//分别交作业
boolean isMathSuccess = mathHomeworkManager.giveMeHomework("你", "3x+2=5解得x=1");
boolean isChineseSuccess = chineseHomeworkManager.giveMeHomework("你", "举头望明月,低头思故乡");
boolean isEnglishSuccess = englishHomeworkManager.giveMeHomework("你", "My name is LiHua");
System.out.println("交数学作业" + (isMathSuccess ? "成功" : "失败"));
System.out.println("交语文作业" + (isChineseSuccess ? "成功" : "失败"));
System.out.println("交英语作业" + (isEnglishSuccess ? "成功" : "失败"));
}
结果如下,肥肠的完美,作业全部被班长改掉了。
班长收到了你的作业,内容是:3x+2=5解得x=1
数学老师看到了你的作业,内容是:老师傻叉,劳资早就看你不爽了
班长收到了你的作业,内容是:举头望明月,低头思故乡
语文老师看到了你的作业,内容是:老师傻叉,劳资早就看你不爽了
班长收到了你的作业,内容是:My name is LiHua
英语老师看到了你的作业,内容是:老师傻叉,劳资早就看你不爽了
交数学作业失败
交语文作业失败
交英语作业失败
采用动态代理的方式,我们只写了一个ProxyHandler,却充当了多个委托类的代理,太方便了。而且,对于第二个缺点,即接口有改动的情况,我们只需要更改委托类里面的接口实现就行了,由于只有一个ProxyHandler,而不是每个委托类都对应一个代理类,我们对接口实现的改动直接少了一半,爽爽子。
有人可能会抬杠,说静态代理其实也可以只写一个代理类去处理与三个不同老师的交互。确实可以,但是代码量一定比动态代理多,代码逻辑一定没有动态代理简单清晰。其实ProxyHandler类可以提炼成一个模板,如下。
public class ProxyHandler implements InvocationHandler {
private Object target;
//绑定委托对象,并返回代理类
public Object bind(Object target) {
this.target = target;
//绑定该类实现的所有接口,取得代理类
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
//预处理
result = method.invoke(tar,args);
//后处理
return result;
}
}
动态代理的话,整个程序里即使各个委托类毫不相干,但是只要想要的预处理和后处理是一样的,就可以共用这一个ProxyHandler类。而如果用静态代理,共用代理类在委托类差异很大的时候就会把不同的委托类耦合到一起,肯定是不好的。说白了动态代理就是写了一个万能模板,谁都可以用。为了兼容各种不同的委托类,只有在代码动态执行的时候去获取这个类是啥,实现了什么接口等信息,才能做到。最后举个例子,比如想要打印很多类里面的方法的执行时间。这个例子我们想进行的预处理和后处理是一样的,就是计算时间差获得方法执行时间,那我就可以不直接调用委托类里的方法,而是通过ProxyHandler类去调用,巧妙的实现了这个需求。