设计模式 ---- 代理Proxy
代理其实本质很好理解,网络上那些花里胡哨的解释一律不用管,我们要抓住精髓:就是增强一个对象的功能。打个比方买火车票,12306的app就是一个代理,代理了火车站售票窗口的功能。小区当中的代售点也是代理,黄牛也是代理。他们替你买了,你就不需要去火车站售票窗口了,就相当于增强了售票窗口的功能。那么回到编程Java当中如何实现代理呢?更多Spring内容进入【Spring解读系列目录】。
Java实现代理有两种办法
静态代理和动态代理,本篇博客主要讲静态代理,并且由静态代理的缺点引出为什么要使用动态代理。首先要明白两个概念代理对象和目标对象。
- 代理对象:增强后的对象(app,黄牛等等)。
- 目标对象:被增强的对象(火车站窗口)。
Java中代理对象和目标对象不是绝对的,会根据情况发生变化。比如代理对象也可能是另一个代理的目标对象。这就好比买黄牛手里火车票的不一定是乘客,也有可能是二道贩子。那么黄牛是火车站售票窗口的代理,二道贩子就是黄牛的代理,这就是代理对象和目标对象的身份做了变换。
静态代理
静态代理也分为两种,一种叫做继承,一种叫做聚合。继承大家都懂,什么是聚合呢?名字高大上,其实就是实现实现接口。为了更好的展示我们还是写个小例子。比如,我们有一个业务类QueryDaoImpl
,和一个执行类MainTest
。直接运行,执行query()
方法是没有问题的。做完主要功能以后,有可能需求增加了,比如我要给QueryDaoImpl
加个日志。那该怎么做呢?正常来说,我们可以搞一个公共日志方法,然后在query()
方法里调用。或者我们在调用的时候的前面加上,这样就满足了当前的业务需求。
public class QueryDaoImpl {
public void query(){
//Util.log(); //方案一:加个静态log方法?
System.out.println("查询数据库内容");
}
}
public class MainTest {
public static void main(String[] args) {
QueryDaoImpl queryDao=new QueryDaoImpl();
//Util.log(); //方案二:或者加到这里?
queryDao.query();
}
}
方案一,我们设想这种情况,如果这里做的是一个公司的项目,加进来的是一个第三方jar包,手里根本就没有源码怎么加?而且就算有源码,加进去了,也违背了单一职责的编码原则。query()方法就是用来查询的,干嘛再加一个功能进去呢。
方案二,如果这个query()方法是一个通用方法,最后发现好几百个地方都用了,一个一个的加好几百次,那不是要疯了。今天这样写死了,万一有天不要加这个log了,又要返工好几百次,显然这样加很不像话。 那么问题就来了,如果不让动源码的话,怎么解决问题呢?在面向对象的思想下,应该能想到第一个解决方案:继承。
继承
假设我们现在没有源码,用继承怎么实现呢?一句话来说就是:代理对象继承目标对象,重写需要增强的方法。
那么我们从新建一个类QueryDaoLogImpl,用来实现日志功能,并且继承QueryDaoImpl重写里面query()方法。然后再修改我们的调用类,让其中实现QueryDaoLogImpl。
public class QueryDaoLogImpl extends QueryDaoImpl {
@Override
public void query() {
System.out.println("---extends log---");
super.query();
}
}
public class MainTest {
public static void main(String[] args) {
QueryDaoImpl queryDao=new QueryDaoLogImpl();
queryDao.query();
}
}
输出结果,完成了目标:
---extends log---
查询数据库内容
我们先不考虑耦合度这种问题,在这个例子中,QueryDaoImpl就是目标对象,而QueryDaoLogImpl就是代理对象,我们现在写的这个例子,就是完成了一个代理的模型。可以很清楚的看出,代理其实已经是一个类级别的改动了,代理出来以后,虽然我们使用的了执行了原来的逻辑,但是对象已经改变,不再是原来的对象了。这点一定要记好。
那我们再看代理对象和目标对象是相对的这句话。假如boss看这个改动不爽了,换成记录时间的,那怎么办呢?依葫芦画瓢,再整一个QueryDaoTimeImpl。
public class QueryDaoTimeImpl extends QueryDaoImpl {
@Override
public void query() {
System.out.println("---extends time---");
super.query();
}
}
MainTest:QueryDaoImpl queryDao=new QueryDaoTimeImpl();
输出结果,完成了目标:
---extends time---
查询数据库内容
又过了一天产品说业务改动了,这俩都得要。得,接着改QueryDaoTimeLogImpl。
public class QueryDaoTimeLogImpl extends QueryDaoLogImpl {
@Override
public void query() {
System.out.println("---extends time---");
super.query();
}
}
MainTest:QueryDaoImpl queryDao=new QueryDaoTimeLogImpl();
---extends time---
---extends log---
查询数据库内容
结果刚改完第二天,业务又说了,想要先打印log再打印time。那我们就又要多搞一个QueryDaoLogTimeImpl。第三天业务又想加个权限认证,再搞一个类。第四天又要把日志,时间,权限整合一起,类+1。第五天又要分开,类+2,等等等等。说了这么多,想必大家已经看到继承的缺点了。如果我们使用继承作为静态代理,随者需求的变化创造代理类将是无限的,各种需求之间进行种种的排列组合,这种链是继承的关系也是非常复杂。此外链式继承本身就是一个代理角色替换的例子。显然,继承不是一个合理的解决方案,那我们接着看聚合会不会好一点。
聚合
也可以总结为一句话:目标对象和代理对象实现同一个接口,代理对象当中要包含目标对象。
既然说到了接口,那么首先要有一个接口QueryDao,然后用主业务类QueryDaoImpl去实现它。那么为了实现代理,还是要有一个代理类QueryDaoLog,同样我们还要修改MainTest去运行。
public interface QueryDao {
void query();
}
public class QueryDaoImpl implements QueryDao{ //实现接口
public void query(){
System.out.println("查询数据库内容");
}
}
public class QueryDaoLog implements QueryDao { //实现同一个接口
QueryDao queryDao;
public QueryDaoLog(QueryDao queryDao) {
this.queryDao=queryDao;
}
@Override
public void query() {
System.out.println("---interface log---");
queryDao.query();
}
}
public class MainTest {
public static void main(String[] args) {
QueryDao target=new QueryDaoImpl();
QueryDao proxy=new QueryDaoLog(target);
proxy.query();
}
}
输出结果
---interface log---
查询数据库内容
我们通过让代理对象实现同一个接口,然后传入目标对象完成了这个需求。这个就是聚合,代理对象通过把目标对象聚合到自身来完成任务,就叫做聚合。
如果完成time的代理,也需要添加一个新的代理类,毕竟是一个新功能对吧。
public class QueryDaoTime implements QueryDao {
QueryDao dao;
public QueryDaoTime(QueryDao dao) {
this.dao=dao;
}
@Override
public void query() {
System.out.println("---interface time---");
dao.query();
}
}
输出结果
---interface time---
查询数据库内容
但是要把他们合并在一起的时候,就看出来区别了,比如我们要Time和Log一起用,只需要修改调用过程就好了。如果想要Time和Log反过来,只要把代理顺序换一下就搞定了。完全不需要构造新的类出来,把已有功能聚合在一起就可以完成了。
public class MainTest {
public static void main(String[] args) {
QueryDao target=new QueryDaoLog(new QueryDaoImpl());
QueryDao proxy=new QueryDaoTime(target);
//QueryDao target=new QueryDaoTime(new QueryDaoImpl()); //换一下顺序就可以完成切换
//QueryDao proxy=new QueryDaoLog(target);
proxy.query();
}
}
输出结果
---interface time---
---interface log---
//---interface log---
//---interface time---
查询数据库内容
可以看出,聚合能够避免我们每有一个新功能就创建一个类的繁琐操作。但是大家想一个问题:我们当前的代理对象仅仅能够代理QueryDao,万一未来需要一个UserDao,是不是我们要把这一套再来一遍。如果还有OrderDao,又要重复做一遍。我相信在座的肯定不止写过一个Dao,有百个Dao难道要把这整一套过程也重复个几百遍?所以可以看出,虽然聚合比继承要灵活,但是其缺点和继承一样,也会产生类爆炸,只不过比继承少一点点。
总结
代理主要目的是为了在不需要修改代码的情况下就能够增强代码功能。然后才是增强安全性,增加扩展度,灵活度等等。但是如果在需求不确定的情况下,使用静态代理将会造成毁灭性的编码灾难。因为一旦开始构建需求,就会构建对应的类。一旦类产生了,就会因为需求的改变,演变成新的类,进而引起类爆炸。为了解决这个问题,码农的前辈们就整出了动态代理,那么我们下一篇【什么是动态代理?】就会讲讲动态代理怎么解决这个问题的。