原文地址:http://herman-liu76.iteye.com/blog/2349026
有时候看源代码是非常有趣的事情,象是思考游戏,象是思考棋局...
平时做J2EE项目中,一直都是以做业务为主,如果用框架,那更多的是写 bean, dao, service, action,功能上也是增删改查为主。这样的代码必然索然无味,不过之前分析过几个开源的系统代码,发现研究那些代码非常有趣的一件事,而且有些设计很自然的在生活中找到原型,或者感觉就是自己设计一个工厂在加工产品,或者感觉是设计一个游乐场服务公众。看这些代码最多的体会是以下方面:
[深]-代码中的设计思想,体现着作者思考深度;
[规]-代码的风格规范体现着作者是工作态度,以及领会规范的意义;
[综]-体现了模块化风格,把东西综合在一起,体现了高聚合低耦合的特征;
[博]-广泛的新技术使用,体现作者深入研究过同类代码或者技术。
本文就以之前研究过的阿里的druid分析为主(druid是阿里巴巴的连接池产品,号称为监控而生),谈谈体会,以及如果是自己面对这样的功能需求,如何应该一步步做出这样的产品来。面向对象的三个基本特征是:封装、继承、多态,但在设计复杂软件的时候,体会最多的是下面几点( 写的有点啰嗦,不过有些过程要细细体会,省了回头看的过程):
一、组合(或持有、引用)是最重要的技术之一
人的本质是各种社会关系的组合,人类社会如些复杂,也是因为各种人体、组织关系交织在一起。
1.长期组合
无论是一个车,一个飞,甚至一个人,都是由无数的子系统以及无数的零件组合而成的,所以组合是实现复杂软件的重要技术。
组合主要是一个类作为另一个类的引用属性,可以简单的说,知道对方在哪里,只有引用对方才能使用对方的功能。很多时候更是相互引用,我知道对方,对方也知道我,常见的代码就是我引用对方时,把自已this再传给对方。
更复杂一点的情况就是已经组合出一个复杂的对象,这个复杂的对方与另一对方建立了引用关系,那另一对象就可以使用复杂对象中的对象。如果在一个大的软件系统中,对象之间的引用是十分复杂的...只有抽象出核心对象的关系动态静态模型才能做出复杂的软件。
还有一个对象内部持有的对象是线程对象,一直为自己服务。
比如:DruidDataSource是一个数据源对象,它必然有很多属性外,持有的重要对象有:
DruidConnectionHolder[],暂且认为数据源有一个数组对象,放的都是这个数据源的连接,所谓连接池吧。
CreateConnectionThread,DestroyConnectionThread,这两个是创建连接和销毁连接的线程对象。如同在一个大的房间,如果人多就会动态增加日光灯,如果人少就减少日光灯数量,动态的改变连接池的大小,算是节能吧
ReentrantLock,这是一把共享锁,上面的线程都在为一个数据源服务,打架的话就要用锁了。
List
平时代码里的serivce与dao,通过spring实现了长期组合的简单关系,而且是单方向的持有。以前没有spring的时候,有些人是通过构造方法传入,有的是使用时传入,有的是直接设置属性,比较乱。
2.用时组合(持有)
用时组合一般是一个比较稳定的对象,处理一个变化的对象,这个过程可能比上面要复杂多了。类似于提供服务,比如医生与医院是一人比较稳定的组合,但医生与病人就是一个临时组合。又类似于打印机与纸张关系,进去是空的,出来是有图案文字的。
druid中的每一个连接就是一个变化的对象,有点象兵营里的士兵,有点象学校里的学生,铁打的营盘流水的兵。如果少了要补充,如果多了要退伍。DruidConnectionHolder[]放置着连接,两个线程不时清点人数。
druid中还有一个重要的变化对象就是过滤链FilterChain,前面提到过滤器Filter,它由数据源持有DruidDataSource。那这三者关系如何呢?举个例子吧,如果你去体检,那每个人手中的体检单就是过滤链,每个科室(医生)就是过滤器,而医院就是数据源。每次新来一个体检者,都产生一个体检单,体检单持有医院这个对象,你不能中途跨医院体检。或者说,有一个工厂,里面有数台加工设备,那每一个加工委托单就是一个过滤链。感觉的出过滤链实际是一个很轻的临时对象,过滤器却是很重的永久对象。
实际的加工过程是怎样的呢?首先一个数据源持有一组过滤器(比如统计过滤器,比较日志过滤器,安全过滤器)每产生一个要监控的对象,比如getgetConnection时,如果这个数据源配置了filter,就生成一个过滤链filterChain(每个过滤链都持有同一个数据源,持有数据源就找的到过滤器),过滤链负责对真正执行功能的前后进行过滤操作。过滤链里面核心的是一个计数器,如同体检完成一个项目打个勾一样。过滤链的最后一个操作一定是直正执行最后的功能。而在这之前,都交给过滤链持有的数据源里的过滤器来一个个过滤,过滤时标记位置。
暂时汇总一下,执行一个功能,先生成过滤链,过滤链上一个个找过滤器来过滤,最后才执行正式的功能。
如果真和体检一样,一个个过滤了,再执行核心功能,那是比较简单的了。但我们发现更复杂一点的是,过滤链条调用过滤器时,把自己,还有数据源都传给了过滤器?干嘛把自己传给过滤器?为什么把体检表交给医生?为什么我还要告诉医生这是哪个医院?我不给医生体检表,我自己做过一个体检我自己标一下,再做下一个为何不可以?
实际上考虑的是,过滤器并不一定在核心功能前做过滤,也可以在核心功能完成后做过滤啊。这之中存在递归调用的问题。就是你到我这里体检,但我这个医生(过滤器)要求先做其它的体检和核心功能后我再做我的步骤(过滤),你的东西要压在我这里,所以你要给我体检单和医院,我安排下一个医生(过滤器)先工作,下一个工作的时候需要你的单据和其它的医生(过滤器--由数据源持有,所以传入数据源)信息,因为也可能下一个医生也这个干,把以传给我单据与医院,我转手让单据进行下一个步骤,产生一个调用栈。
以从DruidDataSource获取连接getConnection的过程为例回顾下整个过程以及为何传递的一些参数:
1.如果数据源配置有filter的时候,需要new一个过滤链filterChain,这时传递了一个this表示本数据源。否则直接获取连接。
2.如果需要filterChain时,那获取连接的任务就交给它了。为何不是让它只做过滤呢?完成后返回给自己来获取呢?原因就是前面说的,过滤是核心功能前后,存在递归。
3.既然把核心功能让filterChain做了,那它也要有条件来做这件事情,虽然真正还是要DruidDataSource来做,那就需要把DruidDataSource作为参数传递给filterChain,或者说把自己this传给它,是让它适当的时候通知自己来做。类似的模式如监听器,回调都类似。注意到1中new的时候传了this(长期组合),现在做事时又传了this(用时组合),后面说明。
4.filterChain的方法是过滤与核心功能的发起者,看看它的dataSource_connect方法。如果计数器表示还有过滤器,那就由过滤器产生connection来返回;如果计数器表示已经完成了,那就直接产生connection返回。是否感觉这里已经有点递归调用的意思了?
5.filterChain现在调用filter来做事情,我们可以猜测的出来,过滤器是不会直正做核心事情的,那让filter叙事的时候到底传什么参数呢?首先filterChain要把自己传进去,因为filter做好后,让filterChain接着做,filterChain让下一个filter做,下一个再回调filterChain,如果还有再安排下一个filter做...
6.传自己外,另外还传递了DruidDataSourcec参数给filter,filterChain要做核心工作,那需要这个参数,filter要这个干嘛?实际上是filter再回调filterChain时还给它。现实场景比如我拿着碗准备吃饭,想起去WC,就把自己告诉别人,把碗也让他拿着,一会他回调我时,把碗还给我。这里我也有一点不清楚,比如1中new出来的时候,我已经一直持有这个碗了,我要吃的时候还要告诉我一下这个碗在哪里,去WC的时候,其实我一直持有这个碗不用交给别人,别人还要还给我,不用这么麻烦吧?
7.filterChain把自己和数据源传给filter后,filter会做自己的事情,还会再调用filterChain,并把碗还给它。这两件事先后可以根据需要设计,也许调用之前做自己的记录,也许调用后做自己的记录。
8.最后提一下后面介绍的代理,filterchian最后是做核心工作,比如产生connection,或者产生resultset,这些对象都是原生对象被wrap后的对象。
总结:
看的出数据源(医院)、过滤器(医生)、每个过滤链(单据)就这三个核心对象之间,调用与持有关系都是比较复杂的,实际上想清楚动态过程就容易理解了。其实J2EE中web.xml中配置的filter,有一个dofilter方法,核心也是这么搞的,这不是直接抄代码,而是抄思路。
说起递归,对象与对象之间相互递归相对方法递归略显复杂,既然是相互调用,那必须相互引用,这里就是过滤器与过滤链相互引用,我调用你,并把我传给你,你再调用我,把你传给我...什么?传的不是过滤器,是数据源,不是正好数据源持有过滤器啊。
引伸:
我还看过一个代码是使用freemarker的,传对象给模板,模板里再使用对象,对象再调用模板....也是比较少见的代码。
另外,有一次网上看到一个代码,是三个线程要依次打印自己的内容,原代码是需要一个锁,而且线程获取锁后还要判断是不是自己可以运行,运行后通知其它wait()的线程,但可能唤醒的还是自己,效率有问题。但是我突然想到,是不是可以用递推把三个线程对象串起来,三个对象应该由一个协调器对象持有,每个对象只与它关联引用。这样虽然还是多线程关系了,但没有效率问题。又可以设想一个现实中的场景:一个大人指挥三个小朋友吃东西,让第一个吃并吃好后告诉自己(大人把自己传给小朋友),真的吃好后告诉大人时需要小朋友把自己告诉大人(小朋友把自己传给大人),这样大人可以判断下一个是谁,并检测东西还剩下多少。依次调用真到吃完为止。
再说一个问题,我之前看过一些分析,看着类图就有点晕,看着泳道又太简单,我目前不知道有什么UML图可以清楚的表达这个动静结合的设计。也许是个类初始变化,也许是类调用变化过程的一个flash动图一样的东西,也许常见的对象递归调用可以是UML中的模板。
二、代理proxy(或包装wrap、适配器adapter)是重要的技术之一
与上面提到了组合有一定的关系,如果组合的部分是人家已经开发好的完善的模块,而你要使用,那就要注意这个技术的使用了。
在druid的设计中最明显的是几乎所有的JDBC操作相关已有对象都变成了Proxy对象了。当然了,为了监控每个JDBC对象的操作,硬要每一步中插入自己要做的事情,那每个对象都要安装一个代理Proxy了。每件事都交给代理来做,代理把中间增加的事情(过滤)做好了,再让核心对象来做原来的事情。
java.sql.Connection被代理成ConnectionProxy,代理对象必然持有被代理对象,当然也有继承的,继承的方法中,自己做的事情做好后,就做super的正式的工作。
还是回到为监控而生druid,就是说做任何一个操作时,都要被监控,换句话说,就是要被过滤器过滤一遍,实际上就要产生一个过滤链,这个链在执行这个操作的过程中产生,并消亡,生命周期很短。可以看出过滤器应该是单例的,不持有过程数据,而这个过滤链是每个动作产生的,持有计数功能,他们之间还要不断的递归调用。
以ConnectionProxyImpl中的createBlob()为例:
1.createChain()---应该是每一个操作产生一个,源码几乎也是new一下,但为何那么写?
2.Blob value = chain.connection_createBlob(this);---递归调用filter的起点必然是filterchain的方法发起,最终的功能也必然在filterchain里面。
3.recycleFilterChain(chain);---ConnectionProxyImpl的每个方法调用完都重置计数为0,事实上不用重新new一个,只要置计数0就是新的了。说明这个ConnectionProxyImpl中的所有方法不可能并发调用的,否则就出问题了,所以那么写。
4.产生connection的时候有一个fillterchain,而connection本身又持有一个fillterchain,connection每做一个事情都重置计数,核心功能还是靠所持有的原生对象来完成的。
这个技术从根本上说,调用方根本不知道真正调用的是什么,是原始对象,代理对象,适配器,适配器也许自己也不知道适配谁,要由调用方的参数决定。说到adapter,看的最多的还是阿里的dubbo中,用的非常多,比如用适配器来适配不同的通讯方式。
三、复杂软件的核心功能的理解却不是很复杂,实现却相当的有难度
要做的好,知识要非常全面,即有深度,又有广度,还有规范,而每一个地方的开发,都要即了解全局,要又向上面一样细节上考量。比如这么多知识点:mbean,spi,mock,nio,protocal.zookeeper,classloader,redis,factory,serilize,anotation,multicase,netty,invoke,threadpool,reentrantLock,handler,holder,LoadBalance,Cluster,ConsistentHash,md5,sha1,LRU..还要和其它已有产品配合,比如配合spring的parser,init...
看到这么多技术,与我们平时做项目用到的比较,实在一个天上,一下地下。如果学习java,那think in java也只是入门的书籍。只有读几块砖头厚的书,紧跟最新的技术,站在前人的基础上才能做出完美的产品。
向阿里巴巴的牛人致敬!