zongxing

没有迈不过去的坎!

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  14 随笔 :: 16 文章 :: 33 评论 :: 0 Trackbacks

2009年10月8日 #

在网上找了一下,基本找不到。只有到诺基亚论坛里去找啦。

第五版sdk下地址
http://www.forum.nokia.com/info/sw.nokia.com/id/ec866fab-4b76-49f6-b5a5-af0631419e9c/S60_All_in_One_SDKs.html

给大家说下怎么一步一步的找到下载地址:
1:进入诺基亚论坛:
http://www.forum.nokia.com
2:菜单:I want to =>Develop Applications=>Java™ technology=>Download all-in-one S60 SDKs
这样一步一步就进入了sdk的下载页面。

ps:每一步的链接地址都是比较隐蔽的,不太好找,这就要看你有没有耐心了。

3:在Download version for: 下拉框里选择你想要的版本就可以啦。

有三个版本选择:Nokia N97 (569 MB),5th Edition (622 MB),3rd Edition, FP2 v1.1 (455 MB)

ps:想找资料,还是推荐直接去官方去找。

第一、二、三版下载地址
http://www.forum.nokia.com/info/sw.nokia.com/id/4a7149a5-95a5-4726-913a-3c6f21eb65a5/S60-SDK-0616-3.0-mr.html
posted @ 2009-10-08 11:36 zongxing 阅读(3680) | 评论 (3)编辑 收藏

2009年7月29日 #

项目里很多复杂的业务都是由存储过程或比较复杂的sql语句来实现的。
1:酬金计算
2:重要数据导入
3:报表
这些对oracle要非常熟悉才能做到游刃有余。而oracle数据库恰恰是我的软肋。
复杂sql不熟悉,常用函数不熟悉,存储过程不熟悉,等等,很多很多都不熟悉。
oracle之路……很长很长……

应对之道
老是不熟悉也不是办法呀!还要是有解决的办法。
1:常用的sql,各种关联用法。
2:常用函数
3:技巧
4:存储过程,包,触发器等用户

posted @ 2009-07-29 21:44 zongxing 阅读(144) | 评论 (0)编辑 收藏

2009年1月15日 #

在word中编辑时,突然无法显示文档中的图片,只能看到图片的边框。
顿时觉得莫名其妙。
本想找同事问一下。算了,还是自己想办法解决吧。这么小的事情。
在网上一查,发现,已经有人遇到过这类事情。
word选项-高级-显示文档内容-显示图片框
不勾选这个选项就可以啦。
posted @ 2009-01-15 14:34 zongxing 阅读(1023) | 评论 (0)编辑 收藏

2007年12月22日 #

                         mysql数据库在dos命令行下乱码的全套解决方案!
                                           2007年12月22日   15:01:52
       使用mysql数据库最常见的就是乱码问题了,提到乱码,相信搞java的人都是不陌生的,由于公司里统一了mysql数据库,所以各个员工都开始遇见了乱码问题,于是,笔者就把常见的问题列出来,并一一解决:
使用mysql可视化编程工具打开显示为正常编码,在dos命令行下为乱码,其实这个也是最主要的,也是首要解决的

      在配置mysql时(刚安装时首先要配置,以后的时间也可以配置),打开配置界面,一路下一步,到了要选择编码的地方,选中那一项,然后选择默认编码。

      问题1:  在这里选择编码就有学问了,也是dos下乱码的最佳解决方案。一般在国内的开发者都是要支持中文的,所以建议大家先用gb2312,这样在建库的时候就可以使用默认的gb2312编码了,如果你要用大字符集,比如gbk,utf8之类的,只需要在建库的时候设置上就可以了。如果按这样操作,无论是在可视化工具里还是在dos下,都不会出现乱码。如果看到这里,恭喜你,你已经不用再被mysql的乱码困扰了(与web 服务相关的暂不讲述).
      问题2:  如果你把默认的编码设为gbk了,以后你在dos下如果查看utf8编码的数据库,恭喜你,你也不会出现乱码。但是如果你要查看gb2312编码的数据库,那么,完了,你肯定是乱码了。如果你不是乱码,你可以给我发邮件52000100@qq.com,我和你共同探讨原因。当然在可视化工具里都不会出现乱码。
      问题3:如果你把默认的编码设为utf8了,你在dos下只能查看utf8编码的数据库,gb2312和gbk的都会是乱码,这个也不要问我为什么,在经历了这么长时间乱码的折磨,我才总结出这些规律,具体为什么会这样,我也不太清楚。如果有兴趣,你可以留言或是发邮件给我。

     相信看完文章,你已经搞定mysql中与此相关的乱码问题了,恭喜你!
   

posted @ 2007-12-22 14:53 zongxing 阅读(3974) | 评论 (2)编辑 收藏

2007年11月1日 #

1:两者在一起使用,就不能再用原来hibernate生成数据库表的方法了。
主外键关系一般是生不成的。
2:powerdesign12生成的mysql5.0版本的数据库表也是有问题的。主外键关系都是生不成的,还要手工修改方可(还没时间修改)。

posted @ 2007-11-01 17:09 zongxing 阅读(212) | 评论 (0)编辑 收藏

2007年10月30日 #

Java运行原理:
Java有一个垃圾回收机制,总是在内存剩余大概5%才启动,因为它中断权限最高,它运行,其他全部停止,因此,我们不希望垃圾回收机制频繁启动,那么就要控制内存不要触碰剩余5%底线。

而在普通JavaBeans系统中,每一次客户端请求访问时,系统总是new一个javabeans或Java Class,如果并发访问量很大,比如并发10人或100人,再加上你的系统复杂,有很多JavaBeans,假设有30个,那么这下子100个并发请求来,就有3000个Java对象创建,然后下一批有来一次100个请求,这象潮水一样。

每次请求产生的3000个对象会继续占用内存,不会被垃圾回收机制回收,因为垃圾回收机制只有等到内存剩余5%才启动,这样,你的内存无论多大,取决于访问量,总会被耗光,最后垃圾回收出来收拾残局,你的业务系统被暂停甚至缓慢。

所以,这里需要有资源控制,将内存能够控制住,不要被无限消耗,最后导致垃圾回收启动,造成系统好像死机。


控制资源就是使用Pool或Cache来控制,Spring/JdonFramework下可自行加入; EJB已经默认加入了。

这也是我一直反对使用Jsp+JavaBeans来写复杂或大访问量的系统,至于如何控制服务器资源,只有数据库连接池是不够的,因为Bean才是真正的资源消耗重点。

如果你理论上属于无知,又狂热追求Spring这些新玩艺(当初),那么,即使你使用Spring,性能还是和Jsp+JavaBeans一样,在大访问量情况下经常死机,因为Spring里面需要手工配置Pool或Cache这些资源控制机制。
如果说Java比C方便,因为对象使用之后不需要清理,那么有了Ioc/DI依赖注射以后,Java中对象使用之前也不需要创建了。
spring 的好处,不用创建javabean对象了。
posted @ 2007-10-30 13:53 zongxing 阅读(694) | 评论 (0)编辑 收藏

2007年10月20日 #

本文转自:http://www.dev-share.com/java/99953page2.html

0:前言
我们知道了tomcat的整体框架了, 也明白了里面都有些什么组件, 以及各个组件是干什么用的了。

http://www.csdn.net/Develop/read_article.asp?id=27225

我想,接下来我们应该去了解一下 tomcat 是如何处理jsp和servlet请求的。

1.  我们以一个具体的例子,来跟踪TOMCAT,看看它是如何把Request一层一层地递交给下一个容器,并最后交给Wrapper来处理的。

以http://localhost:8080/web/login.jsp为例子

(以下例子,都是以tomcat4 源码为参考)

这篇心得主要分为3个部分: 前期, 中期, 和末期。

 前期:讲解了在浏览器里面输入一个URL,是怎么被tomcat抓住的。

中期:讲解了被tomcat抓住后,又是怎么在各个容器里面穿梭, 最后到达最后的处理地点。

末期:讲解到达最后的处理地点后,又是怎么具体处理的。

2、 前期 Request的born.

    在这里我先简单讲一下request这个东西。

     我们先看着这个URL:http://localhost:8080/web/login.jsp 它是动用了8080端口来进行socket通讯的。

     我们知道, 通过

       InputStream in = socket.getInputStream() 和

       OutputStream out = socket.getOutputStream()

     就可以实现消息的来来往往了。

     但是如果把Stream给应用层看,显然操作起来不方便。

     所以,在tomcat 的Connector里面, socket被封装成了Request和Response这两个对象。

     我们可以简单地把Request看成管发到服务器来的数据,把Response看成想发出服务器的数据。

    

     但是这样又有其他问题了啊? Request这个对象是把socket封装起来了, 但是他提供的又东西太多了。

     诸如Request.getAuthorization(), Request.getSocket()。 像Authorization这种东西开发人员拿来基本上用不太着,而像socket这种东西,暴露给开发人员又有潜在的危险。 而且啊, 在Servlet Specification里面标准的通信类是ServletRequest和HttpServletRequest,而非这个Request类。 So, So, So. Tomcat必须得捣持捣持Request才行。 最后tomcat选择了使用捣持模式(应该叫适配器模式)来解决这个问题。它把org.apache.catalina.Request 捣持成了 org.apache.coyote.tomcat4.CoyoteRequest。 而CoyoteRequest又实现了ServletRequest和HttpServletRequest 这两种接口。 这样就提供给开发人员需要且刚刚需要的方法了。

 

    ok, 让我们在 tomcat的顶层容器 - StandardEngin 的invoke()方法这里设置一个断点, 然后访问

    http://localhost:8080/web/login.jsp, 我们来看看在前期都会路过哪些地方:

       1. run(): 536, java.lang.Thread, Thread.java

       CurrentThread

      2. run():666, org.apache.tomcat.util.threads.ThreadPool$ControlRunnable, ThreadPool.java

               ThreadPool

       3. runIt():589, org.apache.tomcat.util.net.TcpWorkerThread, PoolTcpEndpoint.java

         ThreadWorker

4.        processConnection():  549

org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler, Http11Protocol.java

                  http protocol parser

      5. Process(): 781, org.apache.coyote.http11.Http11Processor, Http11Processor.java

          http request processor

       6. service(): 193, org.apache.coyote.tomcat4.CoyoteAdapter,CoyoteAdapter.java

         adapter

       7. invoke(): 995, org.apache.catalina.core.ContainerBase, ContainerBase.java

   StandardEngin

    1. 主线程

    2. 启动线程池.

    3. 调出线程池里面空闲的工作线程。

    4. 把8080端口传过来由httpd协议封装的数据,解析成Request和Response对象。

    5. 使用Http11Processor来处理request

    6. 在Http11Processor里面, 又会call CoyoteAdapter来进行适配处理,把Request适配成实现了ServletRequest和HttpServletRequest接口的CoyoteRequest.

7. 到了这里,前期的去毛拔皮工作就基本上搞定,可以交给StandardEngin 做核心的处理工作了。

3. 中期。 在各个容器间的穿梭。

    Request在各个容器里面的穿梭大致是这样一种方式:

    每个容器里面都有一个管道(pipline), 专门用来传送Request用的。

    管道里面又有好几个阀门(valve), 专门用来过滤Request用的。

    在管道的低部通常都会放上一个默认的阀们。 这个阀们至少会做一件事情,就是把Request交给子容器。

    让我们来想象一下:

     当一个Request进入一个容器后, 它就在管道里面流动,波罗~ 波罗~ 波罗~ 地穿过各个阀门。在流到最后一个阀门的时候,吧唧~ 那个该死的阀门就把它扔给了子容器。 然后又开始 波罗~ 波罗~ 波罗~ ... 吧唧~.... 波罗~ 波罗~ 波罗~ ....吧唧~....

    就是通过这种方式, Request 走完了所有的容器。( 感觉有点像消化系统,最后一个地方有点像那里~ )

    OK, 让我们具体看看都有些什么容器, 各个容器里面又都有些什么阀门,这些阀们都对我们的Request做了些什么吧:

3.1 StandardEngin 的pipeline里面放的是:StandardEnginValve

在这里,VALVE做了三件事:

1.   验证传递过来的request是不是httpservletRequest.

2    验证传递过来的 request 是否携带了host header信息.

3    选择相应的host去处理它。(一般我们都只有一个host:localhost,也就是127.0.0.1)。

到了这个地方,我们的request就已经完成了在Engin这个部分的历史使命,通向前途未卜的下一站: host了。

3.2 StandardHost 的pipline里面放的是: StandardHostValve

1.   验证传递过来的request是不是httpservletRequest.

2.   根据Request来确定哪个Context来处理。

Context其实就是webapp,比如http://localhost:8080/web/login.jsp

这里web就是Context罗!

3.   既然确定了是哪个Context了,那么就应该把那个Context的classloader付给当前线程了。

        Thread.currentThread().setContextClassLoader(context.getLoader().getClassLoader());

   这样request就只看得见指定的context下面的classes啊, jar啊这些,而看不见tomcat本身的类,什么Engin啊, Valve啊。不然还得了啊!

4. 既然request到了这里了,看来用户是准备访问web这个web app了,咋们得更新一下这个用户的session不是! Ok , 就由manager更新一下用户的session信息

5. 交给具体的Context 容器去继续处理Request.

6. Context处理完毕了,把classloader还回来。

3.3 StandardContext 的pipline里面放的是: StandardContextValve

1.   验证传递过来的request是不是httpservletRequest.

2.   如果request意图不轨,想要访问/meta-inf, /web-inf这些目录下的东西,呵呵,没有用D!

3.   这个时候就会根据Request到底是Servlet,还是jsp,还是静态资源来决定到底用哪种Wrapper来处理这个Reqeust了。

4.   一旦决定了到底用哪种Wrapper,OK,交给那个Wrapper处理。

4. 末期。 不同的需求是怎么处理的.

StandardWrapper

之前对Wrapper没有做过讲解,其实它是这样一种东西。

我们在处理Request的时候,可以分成3种。

处理静态的: org.apache.catalina.servlets.DefaultServlet  

处理jsp的:org.apache.jasper.servlet.JspServlet

处理servlet的:org.apache.catalina.servlets.InvokerServlet

不同的request就用这3种不同的servlet去处理。

Wrapper就是对它们的一种简单的封装,有了Wrapper后,我们就可以轻松地拦截每次的Request。也可以容易地调用servlet的init()和destroy()方法, 便于管理嘛!

具体情况是这么滴:

   如果request是找jsp文件,StandardWrapper里面就会封装一个org.apache.jasper.servlet.JspServlet去处理它。

   如果request是找 静态资源 ,StandardWrapper里面就会封装一个org.apache.jasper.servlet.DefaultServlet 去处理它。

   如果request是找servlet ,StandardWrapper里面就会封装一个org.apache.jasper.servlet.InvokerServlet 去处理它。

StandardWrapper同样也是容器,既然是容器, 那么里面一定留了一个管道给request去穿,管道低部肯定也有一个阀门(注1),用来做最后一道拦截工作.

在这最底部的阀门里,其实就主要做了两件事:

   一是启动过滤器,让request在N个过滤器里面筛一通,如果OK! 那就PASS。 否则就跳到其他地方去了。

   二是servlet.service((HttpServletRequest) request,(HttpServletResponse) response); 这个方法.

     如果是 JspServlet, 那么先把jsp文件编译成servlet_xxx, 再invoke servlet_xxx的servie()方法。

     如果是 DefaultServlet, 就直接找到静态资源,取出内容, 发送出去。

     如果是 InvokerServlet, 就调用那个具体的servlet的service()方法。

   ok! 完毕。

注1: StandardWrapper 里面的阀门是最后一道关口了。 如果这个阀门欲意把request交给StandardWrapper 的子容器处理。 对不起, 在设计考虑的时候, Wrapper就被考虑成最末的一个容器, 压根儿就不会给Wrapper添加子容器的机会! 如果硬是要调用addChild(), 立马抛出IllegalArgumentException!

参考:

     <http://jakarta.apache.org/tomcat/>
   <http://www.onjava.com/pub/a/onjava/2003/05/14/java_webserver.html>

posted @ 2007-10-20 18:09 zongxing 阅读(366) | 评论 (0)编辑 收藏

本文转自:http://blog.csdn.net/qiqijava/articles/210499.aspx
1.关于Tomcat的基本情况

众所周知Tomcat是一个免费的开放源码的Serlvet容器,它是Apache基金会的Jakarta项目中的一个核心项目,也是sun公司官方推荐的servlet和jsp容器,同时它还获得过多种荣誉。servlet和jsp的最新规范都可以在tomcat的新版本中得到实现。Tomcat具有轻量级和灵活嵌入到应用系统中的优点,所以得到了广泛的应用。在Tomcat的发展中,Sun在1999年六月宣布参与Jakarta项目的Tomcat servlet容器和Jsp引擎的开发,使得Tomcat在3.x和4.x版之间系统设计上发生了比较大的变化。Tomcat的其他信息我就不多说了。有兴趣的朋友可以访问http://jakarta.apache.org/ 的官方网站得到更多的信息。

因为工作的原因,我改写了Tomcat的一些代码,所以我粗略的研究了一下Tomcat3.3和Tomcat4.0的源码,深深地被这个开放软件的设计和实现吸引,感觉到这个软件中有许多值得我们学习和借鉴的地方。我把自己的理解介绍给大家算是抛砖引玉,不足和偏差还望大家批评指正。下面就来让我们看看从Tomcat那里我们可以得到什么。

2.从Tomcat中学习设计模式

Tomcat的设计和实现处处体现着设计模式的思想,它的基本流程是首先通过解析xml格式的配置文件,获得系统的配置和应用信息,然后加载定制的组件模块提供各种系统服务。系统的各部分功能都是通过可以配置的组件模块来实现的。Tomcat实现中像Observer,Facade,Adapter,Singleton等多种设计模型在Tomcat的源码中随处可见,为我们提供了一个很好的学习设计模式的平台。我主要介绍一下Tomcat中程序流程控制所采用的设计模式,这是一个程序运行的框架。前面提到由于Sun公司的参与,Tomcat虽然基本的流程没有变化,但是Tomcat3.3和Tomcat4.0版本之间在概念上还是有很大地不同的。Tomcat3.3整体上是模块化的设计,而Tomcat4.0可以看作是采用面向组件技术进行设计的。组件是比模块更高级的一个层次。我们可以通过比较它们之间的不同来了解实现一个服务器软件可以采用的设计模式和实现方式。

2.1Tomcat3.3的基本结构设计

Tomcat3.3采用的是一种模块化的链状的控制结构,它的主要设计模式有:

Chain of responsibility(责任链)

作为一个基于请求响应模式的服务器,在Tomcat3.3中采用一种链状处理的控制模式。请求在链上的各个环节上传递,在任一环节上可以存在若干个"监听器"处理它。这样做的目的是避免请求的发送者和接受者之间的直接耦合,从而可以为其他的对象提供了参与处理请求的机会。采用这个方式不但可以通过"监听器"实现系统功能,而且可以通过添加新的"监听器"对象实现系统功能的扩展。

Interceptor(监听器)

"监听器"是一个过去使用的名称,它可以看作 "模块(module)"的同义词。它是Tomcat功能模块构建和扩展的方式。Tomcat3.3的大部分功能都是通过"监听器"实现的。在Tomcat中提供了一种简单的钩子(Hook)机制,监听器对钩子中感兴趣的事件进行注册,事件发生后通过钩子唤醒已注册的"监听器"对象,"监听器"对象对Tomcat内核事件进行处理。这些模块都是围绕着"责任链"和"策略"的模式进行设计。通过"监听器"你可以监听各种特殊事件,进而控制处理请求的各个步骤---解析,认证,授权,会话,响应提交,缓冲区提交等等。

Strategy(策略)

所谓策略是指"定义一组规则,按照规则进行对象封装,使得他们只在规则内部进行交互"。通过策略模式使得Tomcat作为一个开源项目在开放环境下的开发和演变变得更轻松。通过这种模式把复杂的算法分成模块然后不同的开发组提供各自的实现。从而实现调用模块的代码和模块的具体实现代码的分别开发。这样可以使我们专注于问题的重点,并且减少问题之间的依赖性。在Tomcat中大量采用了策略的设计模式,通过这种方式每一种服务都提供了多种的实现(例如Tomcat中有2-3种认证模块),在代码完成后可以从稳定性和性能表现的考虑选择更好的实现。策略模式对开放式环境下的软件开发非常有用。

我们通过简化的类图(见图一)和时序图(见图二),描述一下Tomcat3.3的程序流程控制如何通过监听器和责任链实现。

图一 简化的类图
图一 简化的类图

 

关于类图的简单说明:

BaseInterceptor:是所有监听器的基类,描述了基本的模块功能和对各种事件的缺省处理。

ContextManage:系统的核心控制对象,进行请求处理和系统配置。它维护了全局的属性、web应用的内容和全局模块等多种信息,责任链的钩子实现也在其中。

PoolTcpConnector:一个处理TCP连接的连接器对象,从BaseIntercepor派生。它包括一个具体处理socket连接的PoolTcpEndPoint类对象。

PoolTcpEndPoint:处理实际的tcp连接。它有一个连接池对象ThreadPool和运行在独立线程中的应用逻辑类TcpWorkThread。

TcpWorkThead:处理socket连接事务,调用接口TcpConnectionHandler中的请求处理方法。

Http10Interceptor:从PoolTcpConnector派生,实现了TcpConnectionHandler接口,是一个真正的监听器对象。它按照Http1.0的协议标准对tcp连接进行处理,调用核心对象ContextManage的服务方法。

图二 简化的时序图
图二 简化的时序图

 

关于时序图中需要说明的地方:

  1. 在contextManager初始化后会根据配置信息,加载基本的应用模块和各种监听器对象,创建钩子(Hook)机制,注册监听器对象,形成一个责任链。然后对各个监听器对象发出engineInit,engineStart消息。
  2. 一个请求在经过http10interceptor基本处理后提交到contextManager处理。
  3. ContextManager的processRequest方法进行请求的处理。按照处理的步骤会顺序地发出H_postReadRequest,H_contextMap, H_requestMap等消息。然后从hook中取得对该消息注册的监听器对象,调用他们的处理方法,从而实现责任链方式。以下的代码片断说明了这种方式:
    BaseInterceptor ri[];//取得注册对象ri=defaultContainer.getInterceptors(Container.H_postReadRequest);//执行注册对象的对消息的处理方法for( int i=0; i< ri.length; i++ ) { status=ri[i].postReadRequest( req );	......}
  4. 系统退出时contextManager发出engineStop消息。

Tomcat3.3的基本程序结构就是采用上面介绍的方式设计的。它给我们的设计和开发提供了一个很好的思路,通过这种模式可以轻松的实现一个事件驱动的基于模块化设计的应用程序。各个功能通过模块实现,通过对责任链上的消息和处理步骤的改动或者添加新的监听器对象可以非常方便的扩展Tomcat的功能。所以这是一个非常好的设计。

2.2Tomcat4.0的基本结构设计

虽然Tomcat3.x已经实现了一个非常好的设计体系,但是在Sun公司加入后, Tomcat4.0中还是引入了不同的实现方式。主要的区别是Tomcat4.0采用了面向组件的设计方式, Tomcat4.0中的功能是由组件提供的,控制流程通过组件之间的通讯完成。这不同于Tomcat3.3中的基于模块的链式控制结构。

面向组件的技术(CO)是比面向对象的技术(OOP)更高一层的抽象,它融合了面向对象的优点,加入了安全性和可扩展的模块设计,可以更好的映射问题域空间。采用面向组件的设计会带来很多好处,可以提高复用性、降低耦合度和通过组装构成系统等等。面向组件编程中有许多概念与原来面向对象的编程是不同的,例如:

Message(消息):定义抽象操作; Method(方法):定义具体的操作;
Interface(接口):一组消息的集合; Implementation(实现):一组方法的集合;
Module(模块):静态的数据结构, Type(类型):动态的数据结构。

软件组件不同与功能模块,它具有以下特性:

  • 组件是一个自包容的模块,具有定义清楚的界线,对外提供它的能力、属性和事件。
  • 组件自身不保留状态。
  • 组件可以是一个类,大部分情况下是一组类。

在Java 语言中对面向组件编程的支持是通过JavaBeans模型获得的。JavaBean组件框架提供了对事件和属性的支持。Tomcat4.0的组件的就是通过JavaBean技术实现的。这是它和Tomcat3.3中最大的不同。下面我们来看一下Tomcat4.0是如何通过面向组件编程来实现程序流程控制的。

面向组件编程时设计组件是关键,从Tomcat4.0中可以看出主要使用了以下的设计模式:

Separation of Concerns(SOC)

设计组件时应该从不同的问题领域,站在不同的观点上分析,把每一种属性分别考虑。举一个例子FileLogger组件,它用于把系统日志信息保存到文件系统中。按照这种模式分析,我们从不同的角度看待它:它如何启动服务、停止服务和进行通讯?它的具体的功能有哪些?别的组件可以发给它哪些消息?基于这些考虑,FileLogger组件应该实现两种接口:Lifecycle(生存期接口)和LoggerBase(功能接口)。

Inversion of Control(IOC)这个模式定义的是,组件总是通过外部进行管理的。组件需要的信息总是来源于外部,实际上组件在生存期的各个阶段都是被创建它的组件管理的。在Tomcat4.0中就是通过这种组件之间的相互控制和调用实现各个功能的。

按照这些模式分析得到的Tomcat4.0中的组件是既有共性又有特性。共性是Lifecycle接口,特性是不同的功能接口。其中生存期接口处理组件的整个生存期中的各个阶段的事件,功能接口提供给其他的组件使用。

具体的功能如何实现我在这里不多介绍了,我主要介绍一下Tomcat4.0中组件的Lifecycle接口的设计。Lifecycle接口实现了组件的生存期管理,控制管理和通讯。创建一个软件组件和创建一个JavaBean对象一样,可以参考JavaBean进行理解。我通过一个模拟的Lifecycle接口组件的类图来描述。(见图三)

图三 Lifecycle接口组件类图
图三 Lifecycle接口组件类图

 

对模拟的Lifecycle接口组件的类图的说明

  1. Lifecycle Interface(接口)定义一组组件通讯的Message(消息)。
  2. 组件实现Lifecycle接口,组件内部定义一个LifecycleSupport对象。需要和该组件通讯的其他组件必须实现LifecycleListener接口,该组件通过add/removeLifecycleListener方法管理需要通讯的其他组件。
  3. 组件内部状态改变时如果需要和其他组件通讯时,通过LifecycleSupport对象的fireLifecycleEvent方法通知其他组件。
  4. 其他组件通过lifecycleEvent方法获得通知的消息。LifecycleEvent对象是从java.util.EventObject派生的。
  5. 当然在组件设计和实现中我们也可以直接使用JavaBeans中已经提供的的类如:java.beans.PropertyChangeListener;java.beans.PropertyChangeSupport这样可以获得更多的功能特性支持。

通过上面的分析我们可以看到组件成为Tomcat4.0中的核心概念,系统的功能都是通过组件实现的,组件之间的通讯构成了系统的运行控制机制。我们把Tomcat3.3中模块化的链状控制机制和Tomcat4.0的面向组件的设计进行比较,就会发现Tomcat4.0在设计思想上软件组件的概念非常明确。Tomcat4.0和Tomcat3.3最主要的区别就在于此。至于面向对象和面向组件的关系和区别,我在这里就不介绍了,有兴趣的朋友可以找到很多这方面的资源。

3.从Tomcat源码中得到高效的软件组件

Tomcat不但为我们提供了设计和实现系统时的新思路,同时因为它是由组件或者模块构成的,所以它还为我们提供了大量可用的高效软件组件。这些组件都可以在我们的程序开发中使用。我简单列举一些,需要时可以直接从源码中取得。

  • 一些特殊集合类数据结构如池、队列、缓存等可用于服务端开发。
    \src\share\org\apache\tomcat\util\collections
  • 一个简单的钩子(Hooks)机制的实现。
    src\share\org\apache\tomcat\util\hooks
  • 一个简单线程池(ThreadPool)的实现。
    src\share\org\apache\tomcat\util\threads
  • 组件Lifecycle接口的设计和实现。
    \src\catalina\src\share\org\apache\Catalina
  • 常用的日志信息的管理(Logger)的实现。
    src\catalina\src\share\org\apache\catalina\logger
  • 对xml格式的配置信息进行处理(XmlMapper)的实现。
    src\catalina\src\share\org\apache\catalina\util\xml
  • 对socket通讯的高级管理和实现(net)。
    \src\catalina\src\share\org\apache\catalina\net

通过以上对Tomcat的简单的介绍,我们可以看出,作为一个开放源码的项目,Tomcat不但为我们提供了一个应用的平台,同时它还为我们提供了一个学习和研究设计模式、面向组件技术等理论的实践平台。

参考资料

Tomcat3.3源码和Tomcat4.0源码http://jakarta.apache.org/tomcat/index.html
《设计模式》

posted @ 2007-10-20 18:04 zongxing 阅读(353) | 评论 (0)编辑 收藏

本文转自:http://www.moon-soft.com/doc/18332.htm

TOMCAT源码分析(启动框架)
前言:
   本文是我阅读了TOMCAT源码后的一些心得。 主要是讲解TOMCAT的系统框架, 以及启动流程。若有错漏之处,敬请批评指教!
建议:
   毕竟TOMCAT的框架还是比较复杂的, 单是从文字上理解, 是不那么容易掌握TOMCAT的框架的。 所以得实践、实践、再实践。 建议下载一份TOMCAT的源码, 调试通过, 然后单步跟踪其启动过程。 如果有不明白的地方, 再来查阅本文, 看是否能得到帮助。 我相信这样效果以及学习速度都会好很多!
  
1. Tomcat的整体框架结构
   Tomcat的基本框架, 分为4个层次。
   Top Level Elements:
    Server
    Service  
   Connector
    HTTP
    AJP
   Container
   Engine
     Host
   Context
   Component 
    manager
   logger
   loader
   pipeline
   valve
         ...
   站在框架的顶层的是Server和Service
   Server:  其实就是BackGroud程序, 在Tomcat里面的Server的用处是启动和监听服务端事件(诸如重启、关闭等命令。 在tomcat的标准配置文件:server.xml里面, 我们可以看到“<Server port="8005" shutdown="SHUTDOWN" debug="0">”这里的"SHUTDOWN"就是server在监听服务端事件的时候所使用的命令字)
   Service: 在tomcat里面, service是指一类问题的解决方案。  通常我们会默认使用tomcat提供的:Tomcat-Standalone 模式的service。 在这种方式下的service既给我们提供解析jsp和servlet的服务, 同时也提供给我们解析静态文本的服务。
  
   Connector: Tomcat都是在容器里面处理问题的, 而容器又到哪里去取得输入信息呢?
Connector就是专干这个的。 他会把从socket传递过来的数据, 封装成Request, 传递给容器来处理。
   通常我们会用到两种Connector,一种叫http connectoer, 用来传递http需求的。 另一种叫AJP, 在我们整合apache与tomcat工作的时候, apache与tomcat之间就是通过这个协议来互动的。 (说到apache与tomcat的整合工作, 通常我们的目的是为了让apache 获取静态资源, 而让tomcat来解析动态的jsp或者servlet。)
   Container: 当http connector把需求传递给顶级的container: Engin的时候, 我们的视线就应该移动到Container这个层面来了。
   在Container这个层, 我们包含了3种容器: Engin, Host, Context.
   Engin: 收到service传递过来的需求, 处理后, 将结果返回给service( service 是通过 connector 这个媒介来和Engin互动的 ).
   Host: Engin收到service传递过来的需求后,不会自己处理, 而是交给合适的Host来处理。
Host在这里就是虚拟主机的意思, 通常我们都只会使用一个主机,既“localhost”本地机来处理。
   Context: Host接到了从Host传过来的需求后, 也不会自己处理, 而是交给合适的Context来处理。
   比如: <http://127.0.0.1:8080/foo/index.jsp>
         <http://127.0.1:8080/bar/index.jsp>
   前者交给foo这个Context来处理, 后者交给bar这个Context来处理。
   很明显吧! context的意思其实就是一个web app的意思。
   我们通常都会在server.xml里面做这样的配置
   <Context path="/foo" docBase="D:/project/foo/web" />
   这个context容器,就是用来干我们该干的事儿的地方的。
  
   Compenent: 接下来, 我们继续讲讲component是干什么用的。
   我们得先理解一下容器和组件的关系。
   需求被传递到了容器里面, 在合适的时候, 会传递给下一个容器处理。
   而容器里面又盛装着各种各样的组件, 我们可以理解为提供各种各样的增值服务。
   manager: 当一个容器里面装了manager组件后,这个容器就支持session管理了, 事实上在tomcat里面的session管理, 就是靠的在context里面装的manager component.
   logger: 当一个容器里面装了logger组件后, 这个容器里所发生的事情, 就被该组件记录下来啦! 我们通常会在logs/ 这个目录下看见 catalina_log.time.txt 以及 localhost.time.txt 和localhost_examples_log.time.txt。 这就是因为我们分别为:engin, host以及context(examples)这三个容器安装了logger组件, 这也是默认安装, 又叫做标配 :)
   loader: loader这个组件通常只会给我们的context容器使用, loader是用来启动context以及管理这个context的classloader用的。
    pipline: pipeline是这样一个东西, 当一个容器决定了要把从上级传递过来的需求交给子容器的时候, 他就把这个需求放进容器的管道(pipeline)里面去。 而需求傻呼呼得在管道里面流动的时候, 就会被管道里面的各个阀门拦截下来。 比如管道里面放了两个阀门。 第一个阀门叫做“access_allow_vavle”, 也就是说需求流过来的时候,它会看这个需求是哪个IP过来的, 如果这个IP已经在黑名单里面了, sure, 杀! 第二个阀门叫做“defaul_access_valve”它会做例行的检查, 如果通过的话,OK, 把需求传递给当前容器的子容器。 就是通过这种方式, 需求就在各个容器里面传递,流动, 最后抵达目的地的了。
    valve: 就是上面所说的阀门啦。
   Tomcat里面大概就是这么些东西, 我们可以简单地这么理解tomcat的框架,它是一种自上而下, 容器里又包含子容器的这样一种结构。
2. Tomcat的启动流程
   这篇文章是讲tomcat怎么启动的,既然我们大体上了解了TOMCAT的框架结构了, 那么我们可以望文生意地就猜到tomcat的启动, 会先启动父容器,然后逐个启动里面的子容器。 启动每一个容器的时候, 都会启动安插在他身上的组件。 当所有的组件启动完毕, 所有的容器启动完毕的时候, tomcat本身也就启动完毕了。
   顺理成章地, 我们同样可以猜到, tomcat的启动会分成两大部分, 第一步是装配工作。 第二步是启动工作。
   装配工作就是为父容器装上子容器, 为各个容器安插进组件的工作。 这个地方我们会用到digester模式, 至于digester模式什么, 有什么用, 怎么工作的. 请参考 <http://software.ccidnet.com/pub/article/c322_a31671_p2.html>
   启动工作是在装配工作之后, 一旦装配成功了, 我们就只需要点燃最上面的一根导线, 整个tomcat就会被激活起来。 这就好比我们要开一辆已经装配好了的汽车的时候一样,我们只要把钥匙插进钥匙孔,一拧,汽车的引擎就会发动起来,空调就会开起来, 安全装置就会生效, 如此一来,汽车整个就发动起来了。(这个过程确实和TOMCAT的启动过程不谋而和, 让我们不得不怀疑 TOMCAT的设计者是在GE做JAVA开发的)。
2.1 一些有意思的名称:
   Catalina
   Tomcat
   Bootstrap
   Engin
   Host
   Context
   他们的意思很有意思:
   Catalina: 远程轰炸机
   Tomcat: 熊猫轰炸机 -- 轰炸机的一种(这让我想起了让国人引以为豪的熊猫手机,是不是英文可以叫做tomcat??? , 又让我想起了另一则广告: 波导-手机中的战斗机、波音-客机中的战斗机 )
   Bootstap: 引导
   Engin: 发动机
   Host: 主机,领土
   Context: 内容, 目标, 上下文
  
   ... 在许多许多年后, 现代人类已经灭绝。 后现代生物发现了这些单词零落零落在一块。 一个自以为聪明的家伙把这些东西翻译出来了:
   在地勤人员的引导(bootstrap)下, 一架轰炸架(catalina)腾空跃起, 远看是熊猫轰炸机(tomcat), 近看还是熊猫轰炸机! 凭借着优秀的发动机技术(engin), 这架熊猫轰炸机飞临了敌国的领土上空(host), 对准目标(context)投下了毁天灭地的核弹头,波~ 现代生物就这么隔屁了~
 
   综上所述, 这又不得不让人联想到GE是不是也参与了军事设备的生产呢?
   反对美帝国主义! 反对美霸权主义! 和平万岁! 自由万岁!
  
2.2  历史就是那么惊人的相似! tomcat的启动就是从org.apache.catalina.startup.Bootstrap这个类悍然启动的!
   在Bootstrap里做了两件事:
   1. 指定了3种类型classloader:
      commonLoader: common/classes、common/lib、common/endorsed
      catalinaLoader: server/classes、server/lib、commonLoader
      sharedLoader:  shared/classes、shared/lib、commonLoader
   2. 引导Catalina的启动。
      用Reflection技术调用org.apache.catalina.startup.Catalina的process方法, 并传递参数过去。
  
2.3 Catalina.java
   Catalina完成了几个重要的任务:
   1. 使用Digester技术装配tomcat各个容器与组件。
      1.1 装配工作的主要内容是安装各个大件。 比如server下有什么样的servcie。 Host会容纳多少个context。 Context都会使用到哪些组件等等。
      1.2 同时呢, 在装配工作这一步, 还完成了mbeans的配置工作。 在这里,我简单地但不十分精确地描述一下mbean是什么,干什么用的。
          我们自己生成的对象, 自己管理, 天经地义! 但是如果我们创建了对象了, 想让别人来管, 怎么办呢? 我想至少得告诉别人我们都有什么, 以及通过什么方法可以找到  吧! JMX技术给我们提供了一种手段。 JMX里面主要有3种东西。Mbean, agent, connector.
       Mbean: 用来映射我们的对象。也许mbean就是我们创建的对象, 也许不是, 但有了它, 就可以引用到我们的对象了。
       Agent:  通过它, 就可以找到mbean了。
       Connector: 连接Agent的方式。 可以是http的, 也可以是rmi的,还可以直接通过socket。
      发生在tomcat 装配过程中的事情:  GlobalResourcesLifecycleListener 类的初始化会被触发:
         protected static Registry registry = MBeanUtils.createRegistry();  会运行
         MBeanUtils.createRegistry()  会依据/org/apache/catalina/mbeans/mbeans-descriptors.xml这个配置文件创建 mbeans. Ok, 外界就有了条途径访问tomcat中的各个组件了。(有点像后门儿)
   2. 为top level 的server 做初始化工作。 实际上就是做通常会配置给service的两条connector.(http, ajp)
   3. 从server这个容器开始启动, 点燃整个tomcat.
   4. 为server做一个hook程序, 检测当server shutdown的时候, 关闭tomcat的各个容器用。
   5. 监听8005端口, 如果发送"SHUTDOWN"(默认培植下字符串)过来, 关闭8005serverSocket。
2.4 启动各个容器
   1. Server
      触发Server容器启动前(before_start), 启动中(start), 启动后(after_start)3个事件, 并运行相应的事件处理器。
      启动Server的子容器:Servcie.
   2. Service
      启动Service的子容器:Engin
      启动Connector
   3. Engin
      到了Engin这个层次,以及以下级别的容器, Tomcat就使用了比较一致的启动方式了。
      首先,  运行各个容器自己特有一些任务
      随后,  触发启动前事件
      立即,  设置标签,就表示该容器已经启动
      接着,  启动容器中的各个组件: loader, logger, manager等等
      再接着,启动mapping组件。(注1)
      紧跟着,启动子容器。
      接下来,启动该容器的管道(pipline)
      然后,  触发启动中事件
      最后,  触发启动后事件。
 
      Engin大致会这么做, Host大致也会这么做, Context大致还是会这么做。 那么很显然地, 我们需要在这里使用到代码复用的技术。 tomcat在处理这个问题的时候, 漂亮地使用了抽象类来处理。 ContainerBase. 最后使得这部分完成复杂功能的代码显得干净利落, 干练爽快, 实在是令人觉得叹为观止, 细细品来, 直觉如享佳珍, 另人齿颊留香, 留恋往返啊!
     
      Engin的触发启动前事件里, 会激活绑定在Engin上的唯一一个Listener:EnginConfig。
      这个EnginConfig类基本上没有做什么事情, 就是把EnginConfig的调试级别设置为和Engin相当。 另外就是输出几行文本, 表示Engin已经配置完毕, 并没有做什么实质性的工作。
      注1: mapping组件的用处是, 当一个需求将要从父容器传递到子容器的时候, 而父容器又有多个子容器的话, 那么应该选择哪个子容器来处理需求呢? 这个由mapping 组件来定夺。
   
   4. Host
       同Engin一样, 也是调用ContainerBase里面的start()方法, 不过之前做了些自个儿的任务,就是往Host这个容器的通道(pipline)里面, 安装了一个叫做
 “org.apache.catalina.valves.ErrorReportValve”的阀门。
       这个阀门的用处是这样的:  需求在被Engin传递给Host后, 会继续传递给Context做具体的处理。 这里需求其实就是作为参数传递的Request, Response。 所以在context把需求处理完后, 通常会改动response。 而这个org.apache.catalina.valves.ErrorReportValve的作用就是检察response是否包含错误, 如果有就做相应的处理。
   5. Context
       到了这里, 就终于轮到了tomcat启动中真正的重头戏,启动Context了。
 StandardContext.start() 这个启动Context容器的方法被StandardHost调用.
 5.1 webappResources 该context所指向的具体目录
 5.2 安装defaultContex, DefaultContext 就是默认Context。 如果我们在一个Host下面安装了DefaultContext,而且defaultContext里面又安装了一个数据库连接池资源的话。 那么其他所有的在该Host下的Context, 都可以直接使用这个数据库连接池, 而不用格外做配置了。
  5.3 指定Loader. 通常用默认的org.apache.catalina.loader.WebappLoader这个类。   Loader就是用来指定这个context会用到哪些类啊, 哪些jar包啊这些什么的。
 5.4 指定 Manager. 通常使用默认的org.apache.catalina.session. StandardManager 。 Manager是用来管理session的。
     其实session的管理也很好实现。 以一种简单的session管理为例。 当需求传递过来的时候, 在Request对象里面有一个sessionId 属性。 OK, 得到这个sessionId后, 我们就可以把它作为map的key,而value我们可以放置一个HashMap. HashMap里边儿, 再放我们想放的东西。
 5.5 postWorkDirectory (). Tomcat下面有一个work目录。 我们把临时文件都扔在那儿去。 这个步骤就是在那里创建一个目录。 一般说来会在%CATALINA_HOME%/work/Standalone\localhost\ 这个地方生成一个目录。
5.6  Binding thread。到了这里, 就应该发生 class Loader 互换了。 之前是看得见tomcat下面所有的class和lib. 接下来需要看得见当前context下的class。 所以要设置contextClassLoader, 同时还要把旧的ClassLoader记录下来,因为以后还要用的。
5.7  启动 Loader. 指定这个Context具体要使用哪些classes, 用到哪些jar文件。 如果reloadable设置成了true, 就会启动一个线程来监视classes的变化, 如果有变化就重新启动Context。
5.8  启动logger
5.9  触发安装在它身上的一个监听器。
 lifecycle.fireLifecycleEvent(START_EVENT, null);
 作为监听器之一,ContextConfig会被启动. ContextConfig就是用来配置web.xml的。 比如这个Context有多少Servlet, 又有多少Filter, 就是在这里给Context装上去的。
 5.9.1 defaultConfig. 每个context都得配置 tomcat/conf/web.xml 这个文件。
 5.9.2 applicationConfig 配置自己的 WEB-INF/web.xml 文件
5.9.3 validateSecurityRoles 权限验证。 通常我们在访问/admin 或者/manager的时候,需要用户要么是admin的要么是manager的, 才能访问。 而且我们还可以限制那些资源可以访问, 而哪些不能。 都是在这里实现的。
5.9.4 tldScan: 扫描一下, 需要用到哪些标签(tag lab)
5.10 启动 manager
5.11 postWelcomeFiles() 我们通常会用到的3个启动文件的名称:
index.html、index.htm、index.jsp 就被默认地绑在了这个context上
 5.12 listenerStart 配置listener
 5.13 filterStart 配置 filter
 5.14 启动带有<load-on-startup>1</load-on-startup>的Servlet.
  顺序是从小到大: 1,2,3… 最后是0
  默认情况下, 至少会启动如下3个的Servlet:
  org.apache.catalina.servlets.DefaultServlet  
      处理静态资源的Servlet. 什么图片啊, html啊, css啊, js啊都找他
  org.apache.catalina.servlets.InvokerServlet
      处理没有做Servlet Mapping的那些Servlet.
  org.apache.jasper.servlet.JspServlet
      处理JSP文件的.
       5.15  标识context已经启动完毕。
 走了多少个步骤啊, Context总算是启动完毕喽。
    OK! 走到了这里, 每个容器以及组件都启动完毕。 Tomcat终于不辞辛劳地为人民服务了!
3. 参考文献:
    <http://jakarta.apache.org/tomcat/>
    <http://www.onjava.com/pub/a/onjava/2003/05/14/java_webserver.html>
   
4. 后记
    这篇文章是讲解tomcat启动框架的,还有篇文章是讲解TOMCAT里面的消息处理流程的细节的。 文章内容已经写好了, 现在正在整理阶段。 相信很快就可以做出来, 大家共同研究共同进步。
    这篇文章是独自分析TOMCAT源码所写的, 所以一定有地方是带有个人主观色彩, 难免会有片面之处。若有不当之处敬请批评指教,这样不仅可以使刚开始研究TOMCAT的兄弟们少走弯路, 我也可以学到东西。
    email: sojan_java@yahoo.com.cn

5. tomcat源码分析(消息处理)

posted @ 2007-10-20 18:02 zongxing 阅读(299) | 评论 (0)编辑 收藏

2007年10月13日 #

本文转自:http://www.cnblogs.com/lane_cn/archive/2007/01/25/629731.html

我使用OO技术第一次设计软件的时候,犯了一个设计者所能犯的所有错误。那是一个来自国外的外包项目,外方负责功能设计,我们公司负责程序设计、编码和测试。

第一个重要的错误是,我没有认真的把设计说明书看明白。功能点设计确实有一些问题,按照他们的设计,一个重要的流程是无法实现的。于是我在没有与投资方沟通的情况下,擅自改动了设计,把一个原本在Linux系统上开发的模块改到了Windows系统上。结果流程确实是实现了,但是很不幸,根本不符合他们的需要,比起原先的设计差的更多。在询问了这个流程的设计意图之后,我也清楚了这一点。对方的工程师承认了错误,但是问题是:“为什么不早说啊,我们都跟领导讲过了产品的构架,也保证了交货时间了,现在怎么去说啊?”。他们设计的是一个苹果,而我造了一个桔子出来。最后和工程师商议的结果是:先把桔子改成设计书上的苹果,按时交货,然后再悄悄的改成他们真正需要的香蕉。的这时候距离交货的时间已经不足三天了,于是我每天加班工作到天明,把代码逐行抽出来,用gcc编译调试。好在大部分都是体力活,没有什么技术含量,即使在深夜大脑半休眠的情况下仍然可以接着干。

项目中出现的另外一个错误是:我对工作量的估计非常的不准确。在第一个阶段的时候,按照功能设计说明书中的一个流程,我做了一个示例,用上了投资方规定的所有的技术。当我打开浏览器,看到页面上出现了数据库里的“Tom,Jerry,王小帅”,就愉快的跑到走廊上去呼吸了一口新鲜空气,然后乐观的认为:设计书都已经写好了,示例也做出来了,剩下的事情肯定就象砍瓜切菜一样了。不就是把大家召集起来讲讲设计书,看看示例,然后扑上去开工,然后大功告成。我为每个画面分配的编码工作量是三个工作日。结果却是,他们的设计并不完美,我的理解也并不正确,大家的思想也并不一致。于是我天天召集开会,朝令夕改,不断返工。最后算了一下,实际上写完一个画面用的时间在十个工作日以上。编码占用了太多的时间,测试在匆忙中草草了事,质量……能掩盖的问题也就只好掩盖一下了,性能更是无暇顾及了。

还有一个方面的问题是出在技术上的,这方面是我本文要说的重点。按照投资方的方案,系统的主体部分需要使用J2EE框架,选择的中间件是免费的JBoss。再加上Tomcat作为Web服务器,Struts作为表示层的框架。他们对于这些东西的使用都是有明确目的,但是我并不了解这些技术。新手第一次进行OO设计,加上过多的新式技术,于是出现了一大堆的问题。公司原本安排了一个牛人对我进行指导,他熟悉OO设计,并且熟悉这些开源框架,曾熟读Tomcat和Struts源代码。可是他确实太忙,能指导我的时间非常有限。

投资方发来设计书以后,很快就派来了两个工程师对这个说明书进行讲解。这是一个功能设计说明书,包括一个数据库设计说明书,和一个功能点设计说明。功能点说明里面叙述了每一个工作流程,画面设计和数据流程。两位工程师向我们简单的说明了产品的构想,然后花了一个多星期的时间十分详细的说明了他们的设计,包括数据表里每一个字段的含义,画面上每一个控件的业务意义。除了这些功能性的需求以外,他们还有一些技术上的要求。

为了减少客户的拥有成本,他们不想将产品绑定在特定的数据库和操作系统上,并且希望使用免费的平台。于是他们选择了Java作为开发语言,并且使用了一系列免费的平台。选用的中间件是JBoss,使用Entity Bean作为数据库访问的方式。我们对Entity Bean的效率不放心,因为猜测他运用了大量的反射技术。在经过一段时间的技术调查之后,我决定不采用Entity Bean,而是自己写出一大堆的Value Object,每个Value Object对应一个数据库表,Value Object里面只有一些setter和getter方法,只保存数据,不做任何事情。Value Object的属性与数据库里面的字段一一对应。与每个Value Object对应,做一个数据表的Gateway,负责把数据从数据库里面查出来塞到这些Value Object里面,也负责把Value Object里面的数据塞回数据库。

按照这样的设计,需要为每一个数据表写一个Gateway和一个Value Object,这个数量是比较庞大的。因此我们做了一个自动生成代码的工具,到数据库里面遍历每一个数据表,然后遍历表里面的每一个字段,把这些代码自动生成出来。

这等于自己实现了一个ORM的机制。当时我们做这些事情的时候,ORM还是一个很陌生的名词,Hibernate这样的ORM框架还没听说过。接着我们还是需要解决系统在多种数据库上运行的问题。Gateway是使用JDBC连接数据库的,用SQL查询和修改数据的。于是问题就是:要解决不同数据库之间SQL的微小差别。我是这样干的:我做了一个SqlParser接口,这个接口的作用是把ANSI SQL格式的查询语句转化成各种数据库的查询语句。当然我没必要做的很全面,只要支持我在项目中用到的查询方式和数据类型就够了。然后再开发几个具体的Parser来转换不同的数据库SQL格式。

到这个时候,数据库里面的数据成功转化成了程序里面的对象。非常好!按道理说,剩下的OO之路就该顺理成章了。但是,很不幸,我不知道该怎样用这些Value Object,接下来我就怀着困惑的心情把过程式的代码嫁接在这个OO的基础上了。

我为每一个画面设计出了一个Session Bean,在这个Session Bean里面封装了画面所关联的一切业务流程,让这个Session Bean调用一大堆Value Object开始干活。在Session Bean和页面之间,我没有让他们直接调用,因为据公司的牛人说:“页面直接调用业务代码不好,耦合性太强。”这句话没错,但是我对“业务代码”的理解实在有问题,于是就硬生生的造出一个Helper来,阻挡在页面和Session Bean中间,充当了一个传声筒的角色。

于是在开发中就出现了下面这副景象:每当设计发生变更,我就要修改数据库的设计,用代码生成工具重新生成Value Object,然后重新修改Session Bean里面的业务流程,按照新的参数和返回值修改Helper的代码,最后修改页面的调用代码,修改页面样式。

实际情况比我现在说起来复杂的多。比如Value Object的修改,程序规模越来越大以后,我为了避免出现内存的大量占用和效率的下降,不得不把一些数据库查询的逻辑写到了Gateway和Value Object里面,于是在发生变更的时候,我还要手工修改代码生成工具生成的Gateway和Value Object。这样的维护十分麻烦,这使我困惑OO到底有什么好处。我在这个项目中用OO方式解决了很多问题,而这些问题都是由OO本身造成的。

另一个比较大的问题出在Struts上。投资方为系统设计了很灵活的界面,界面上的所有元素都是可以配置出来,包括位置、数据来源、读写属性。并且操作员的权限可以精确到每一个查看、修改的动作,可以控制每一个控件的读写操作。于是他们希望使用Struts。Struts框架的每一个Action恰好对应一个操作,只需要自己定义Action和权限角色的关系,就可以实现行为的权限控制。但是我错误的理解了Struts的用法,我为每一个页面设计了一个Action,而不是为每一个行为设计一个Action,这样根本就无法做到他们想要的权限控制方式。他们很快发现了我的问题,于是发来了一个说明书,向我介绍Struts的正确使用方式。说明书打印出来厚厚的一本,我翻了一天,终于知道了错在什么地方。但是一大半画面已经生米煮成熟饭,再加上我的Session Bean里面的流程又是按画面来封装的,于是只能改造小部分能改造的画面,权限问题另找办法解决了。

下面就是这个系统的全貌,场面看上去还是蔚为壮观的:

系统经历过数次较大的修改,这个框架不但没有减轻变更的压力,反而使得变更困难加大了。到后来,因为业务流程的变更的越来越复杂,现有流程无法修改,只得用一些十分曲折的方式来实现,运行效率越来越低。由于结构过于复杂,根本没有办法进行性能上的优化。为了平衡效率的延缓,不得不把越来越多的Value Object放在了内存中缓存起来,这又造成了内存占用的急剧增加。到后期调试程序的时候,服务器经常出现“Out of memory”异常,各类对象庞大繁多,系统编译部署一次需要10多分钟。投资方原先是希望我们使用JUnit来进行单元测试,但是这样的流程代码测试起来困难重重,要花费太多的时间和人手,也只得作罢。此外他们设计的很多功能其实都没有实现,并且似乎以后也很难再实现了。设计中预想的很多优秀特点在这样框架中一一消失,大家无奈的接受一个失望的局面。

在我离开公司两年以后,这个系统仍然在持续开发中。新的模块不断的添加,框架上不断添加新的功能点。有一次遇到仍然在公司工作的同事,他们说:“还是原来那个框架,前台加上一个个的JSP,然后后台加上一个个的Value Object,中间的Session Bean封装越来越多的业务流程。”

我的第一个OO系统的设计,表面上使用了OO技术,实际上分析设计还是过程主导的方式。设计的时候过多、过早、过深入的考虑了需要做哪些画面,画面上应该有哪些功能点,功能点的数据流程。再加上一个复杂的OO框架,名目繁多的对象,不仅无法做到快速的开发,灵活的适应需求的变化,反而使系统变得更加复杂,功能修改更加的麻烦了。

在面条式代码的时代,很多人用汇编代码写出了一个个优秀的程序。他们利用一些工具,或者共同遵守一些特别的规约,采用一致的变量命名方式,规范的代码注释,可以使一个庞大的开发团队运行的井井有条。人如果有了先进的思想,工具在这些人的手中就可以发挥出超越时代的能量。而我设计的第一个OO系统,恰好是一个相反的例子。

实际上,面向对象的最独特之处,在于他分析需求的方式。按照这样的方式,不要过分的纠缠于程序的画面、操作的过程,数据的流程,而是要更加深入的探索需求中的一些重要概念。下面,我们就通过一个实例看一看,怎样去抓住需求中的这些重要概念,并且运用OO方法把他融合到程序设计中。也看看OO技术是如何帮助开发人员控制程序的复杂度,让大家工作的更加简单、高效。

我们来看看一个通信公司的账务系统的开发情况。最开始,开发人员找到电信公司的职员询问需求的情况。电信公司的职员是这样说的:

“账务系统主要做这样几件事情:每个月1日凌晨按照用户使用情况生成账单,然后用预存冲销这个账单。还要受理用户的缴费,缴费后可以自动冲销欠费的账单,欠费用户缴清费用之后要发指令到交换上,开启他的服务。费用缴清以后可以打印发票,发票就是下面这个样子。”

经过一番调查,开发人员设计了下面几个主要的流程:

1、 出账:根据一个月内用户的消费情况生成账单;

2、 销账:冲销用户账户上的余额和账单;

3、 缴费:用户向自己的账户上缴费,缴清欠费后打印发票。

弄清了流程,接着就设计用户界面来实现这样的流程。下面是其中一个数据查询界面,分为两个部分:上半部分是缴费信息,记录了用户的缴费历史;下半部分是账单信息,显示账单的费用和销账情况。

界面上的数据一眼看起来很复杂,其实结合出账、缴费、销账的流程讲解一下,是比较容易理解的。下面简单说明一下。

缴费的时候,在缴费信息上添加一条记录,记录下缴费金额。然后查找有没有欠费的账单,如果有就做销账。冲抵欠费的金额记录在“欠费金额”的位置。如果欠费时间较长,就计算滞纳金,记录在“滞纳金”的位置上。冲销欠费以后,剩余的金额记录在“预存款”的位置上。“其他费用”这个位置是预留的,目前没有作用。

每个月出账的时候,在账单信息里面加上一条记录,记录下账单的应收和优惠,这两部分相减就是账单的总金额。然后检查一下账户上有没有余额,如果有就做销账。销账的时候,预存款冲销的部分记录在“预存划拨”的位置,如果不足以冲抵欠费,账单就暂时处于“未缴”状态。等到下次缴费的时候,冲销的金额再记录到“新交款”的位置。等到所有费用缴清了,账单状态变成“已缴”。

销账的流程就这样融合在缴费和出账的过程中。

看起来一切成功搞定了,最重要的几个流程很明确了,剩下的事情无疑就像砍瓜切菜一样。无非是绕着这几个流程,设计出其他更多的流程。现在有个小问题:打印发票的时候,发票的右侧需要有上次结余、本次实缴、本次话费、本次结余这几个金额。

上次结余:上个月账单销账后剩下来的金额,这个容易理解;

本次结余:当前的账单销账后剩下的金额,这个也不难;

本次话费:这是账单的费用,还是最后一次完全销账时的缴费,应该用哪一个呢?

本次缴费:这个和本次话费有什么区别,他在哪里算出来?

带着问题,开发者去问电信公司的职员。开发者把他们设计的界面指点给用户看,向他说明了自己的设计的这几个流程,同时也说出了自己的疑问。用户没有直接回答这个疑问,却提出了另一个问题:

“缴费打发票这个流程并不总是这样的,缴费以后不一定立刻要打印发票的。我们的用户可以在银行、超市这样的地方缴话费,几个月以后才来到我们这里打印发票。并且缴费的时间和销账的时间可以相距很长的,可以先缴纳一笔话费,后面几个月的账单都用这笔钱销账;也可以几个月都不缴费,然后缴纳一笔费用冲销这几个账单。你们设计的这个界面不能很好的体现用户的缴费和消费情况,很难看出来某一次缴费是在什么时候用完的。必须从第一次、或者最后一次缴费余额推算这个历史,太麻烦了。还有,‘预存划拨’、‘新交款’这两个概念我们以前从来没有见过,对用户解释起来肯定是很麻烦的。”

开发人员平静了一下自己沮丧(或愤怒)的心情,仔细想一想,这样的设计确实很不合理。如果一个会计记出这样的账本来,他肯定会被老板开除的。

看起来流程要改,比先前设计的更加灵活,界面也要改。就好像原先盖好的一栋房子忽然被捅了几个窟窿,变得四处透风了。还有,那四个数值到底应该怎样计算出来呢?我们先到走廊上去呼吸两口新鲜空气,然后再回来想想吧。

现在,让我们先忘记这几个变化多端的流程,花一点时间想一想最基本的几个概念吧。系统里面最显而易见的一个概念是什么呢?没错,是账户(Account)。账户可以缴费和消费。每个月消费的情况是记录在一个账单(Bill)里面的。账户和账单之间是一对多的关系。此外,账户还有另一个重要的相关的概念:缴费(Deposit)。账户和缴费之间也是一对多的关系。在我们刚才的设计中,这些对象是这样的:

这个设计看来有些问题,使用了一些用户闻所未闻的概念(预存划拨,新交款)。并且他分离了缴费和消费,表面上很清楚,实际上使账单的查询变得困难了。在实现一些功能的时候确实比较简单(比如缴费和销账),但是另一些功能变得很困难(比如打印发票)。问题到底在什么地方呢?

涉及到账务的行业有很多,最容易想到的也许就是银行了。从银行身上,我们是不是可以学到什么呢?下面是一个银行的存折,这是一个委托收款的账号。用户在账户上定期存钱,然后他的消费会自动从这里扣除。这个情景和我们需要实现的需求很相似。可以观察一下这个存折,存入和支取都是记录在同一列上的,在支出或者存入的右侧记录当时的结余。

有两次账户上的金额被扣除到0,这时候金额已经被全部扣除了,但是消费还没有完全冲销。等到再次存入以后,会继续支取。这种记账的方式就是最基本的流水账,每一条存入和支出都要记录为一条账目(Entry)。程序的设计应该是这样:

这个结构看上去和刚才那个似乎没有什么不同,其实差别是很大的。上面的那个Deposit只是缴费记录,这里的Entry是账目,包括缴费、扣费、滞纳金……所有的费用。销账扣费的过程不应该记录在账单中,而是应该以账目的形式记录下来。Account的代码片段如下:

 

public class Account
{
    
public Bill[] GetBills()
    {
        
//按时间顺序返回所有的账单
    }
    
    
public Bill GetBill(DateTime month)
    {
        
//按照月份返回某个账单
    }
    
    
public Entry[] GetEntrees()
    {
        
//按时间顺序返回所有账目
    }
    
    
public void Pay(float money)
    {
        
//缴费
        
//先添加一个账目,然后冲销欠费的账单
    }
    
    
public Bill GenerateBill(DateTime month)
    {
        
//出账
        
//先添加一个账单,然后用余额冲销这个账单
    }
    
    
public float GetBalance()
    {
        
//返回账户的结余
        
//每一条账目的金额总和,就是账户的结余
    }
    
    
public float GetDebt()
    {
        
//返回账户的欠费
        
//每一个账单的欠费金额综合,就是账户的欠费
    }
}

Entry有很多种类型(存入、支取、滞纳金、赠送费),可以考虑可以为每一种类型创建一个子类,就像这样:

搞成父子关系看起来很复杂、麻烦,并且目前也看不出将这些类型作为Entry的子类有哪些好处。所以我们决定不这样做,只是简单的把这几种类型作为Entry的一个属性。Entry的代码片段如下:

public class Entry
{
    
public DateTime GetTime()
    {
        
//返回账目发生的时间
    }
    
    
public float GetValue()
    {
        
//返回账目的金额
    }
    
    
public EntryType GetType()
    {
        
//返回账目的类型(存入、扣除、赠送、滞纳金)
    }
    
    
public string GetLocation()
    {
        
//返回账目发生的营业厅
    }
    
    
public Bill GetBill()
    {
        
//如果这是一次扣除,这里要返回相关的账单
    }
}

Entry是一个枚举类型,代码如下:

public enum EntryType
{
    Deposit 
= 1,
    Withdrawal 
= 2,
    Penalty 
= 3,
    Present 
= 4
}

下面的界面显示的就是刚才那个账户的账目。要显示这个界面只需要调用Account的GetEntrees方法,得到所有的账目,然后按时间顺序显示出来。这个界面上的消费情况就明确多了,用户很容易弄明白某个缴费是在哪几个月份被消费掉的。

并且,发票上的那几个一直搞不明白的数值也有了答案。比如2005年6月份的发票,我们先看看2005年6月份销账的所有账目(第六行、第八行),这两次一共扣除73.66元,这个金额就是本次消费;两次扣除之间存入200元,这个就是本次实缴;第五行的结余是17.66元,这就是上次结余;第八行上的结余是144元,这个就是本次结余。

用户检查了这个设计,觉得这样的费用显示明确多了。尽管一些措辞不符合习惯的业务词汇,但是他们的概念都是符合的。并且上次还有一个需求没有说:有时候需要把多个月份的发票合在一起打印。按照这样的账目表达方式,合并的发票数值也比较容易搞清楚了。明确了这样的对象关系,实现这个需求其实很容易。

面向对象的设计就是要这样,不要急于确定系统需要做哪些功能点和哪些界面,而是首先要深入的探索需求中出现的概念。在具体的流程不甚清楚的情况下,先把这些概念搞清楚,一个一个的开发出来。然后只要把这些做好的零件拿过来,千变万化的流程其实就变得很简单了,一番搭积木式的装配就可以比较轻松的实现。

另一个重要的类型也渐渐清晰的浮现在我们的眼前:账单(Bill)。他的代码片段如下:

public class Bill
{
    
public DateTime GetBeginTime()
    {
        
//返回账单周期的起始时间
    }
    
    
public DateTime GetEndTime()
    {
        
//返回账单周期的终止时间
    }
    
    
public Fee GetFee()
    {
        
//返回账单的费用
    }
    
    
public float GetPenalty()
    {
        
//返回账单的滞纳金
    }
    
    
public void CaculatePenalty()
    {
        
//计算账单的滞纳金
    }
    
    
public float GetPaid()
    {
        
//返回已支付金额
    }
    
    
public float GetDebt()
    {
        
//返回欠费
        
//账单费用加上滞纳金,再减去支付金额,就是欠费
        return GetFee().GetValue() + GetPanalty() - GetPaid();
    }
    
    
public Entry GetEntrees()
    {
        
//返回相关的存入和支取的账目
    }
    
    
public Bill Merge(Bill bill)
    {
        
//合并两个账单,返回合并后的账单
        
//合并后的账单可以打印在一张发票上
    }
}

Bill类有两个与滞纳金有关的方法,这使开发者想到了原先忽略的一个流程:计算滞纳金。经过与电信公司的确认,决定每个月进行一次计算滞纳金的工作。开发人员写了一个脚本,先得到系统中所有的欠费账单,然后一一调用他们的CaculatePenalty方法。每个月将这个脚本执行一次,就可以完成滞纳金的计算工作。

Bill对象中有账户的基本属性和各级账目的金额和销账的情况,要打印发票,只有这些数值是不够的。还要涉及到上次结余、本次结余和本次实缴,这三个数值是需要从账目中查到的。并且发票有严格的格式要求,也不需要显示费用的细节,只要显示一级和二级的费用类就可以了。应该把这些东西另外封装成一个类:发票(Invoice):

通信公司后来又提出了新的需求:有些账号和银行签订了托收协议,每个月通信公司打印出这些账户的托收单交给银行,银行从个人结算账户上扣除这笔钱,再把一个扣费单交给通信公司。通信公司根据这个扣费单冲销用户的欠费。于是开发人员可以再做一个托收单(DeputyBill):

账单中的GetFee方法的返回值类型是Fee,Fee类型包含了费用的名称、金额和他包含的其他费用。例如下面的情况:

我们可以用这样的一个类来表示费用(Fee),一个费用可以包含其他的费用,他的金额是子费用的金额和。代码片段如下:

public class Fee
{
    
private float valuee = 0;
    
    
public string GetName()
    {
        
//返回费用的名称
    }
    
    
public bool HasChildren()
    {
        
//该费用类型是否有子类型
    }
    
    
public Fee[] GetChildren()
    {
        
//返回该费用类型的子类型
    }
    
    
public float GetValue()
    {
        
//返回费用的金额
        if (HasChildren())
        {
            
float f = 0;
            Fee[] children 
= GetChildren();
            
for (int i = 0; i < children.Length; i ++)
            {
                f 
+= children[i].GetValue();
            }
            
return f;
        }
        
else
        {
            
return valuee;
        }
    }
}

现在开发者设计出了这么一堆类,构成软件系统的主要零件就这么制造出来了。下面要做的就是把这些零件串在一起,去实现需要的功能。OO设计的重点就是要找到这些零件。就像是设计一辆汽车,仅仅知道油路、电路、传动的各项流程是不够的,重要的是知道造一辆汽车需要先制造哪些零件。要想正确的把这些零件设计出来不是一件容易的事情,很少有开发者一开始就了解系统的需求,设计出合理的对象关系。根本的原因在于领域知识的贫乏,开发者和用户之间也缺乏必要的交流。很多人在软件开发的过程中才渐渐意识到原来的设计中存在一些难受的地方,然后探索下去,才知道了正确的方式,这就是业务知识的一个突破。不幸的是,当这个突破到来的时候,程序员经常是已经忙得热火朝天,快把代码写完了。要把一切恢复到正常的轨道上,需要勇气,时间,有远见的领导者,也需要有运气。

posted @ 2007-10-13 22:14 zongxing 阅读(257) | 评论 (0)编辑 收藏

仅列出标题  下一页