2005年11月10日
  今天,越来越多的开发者想要编写企业级的分布式的事务处理应用程序,而这些应用程序必须可以发挥速度、安全性和服务器端技术的可靠性。如果你已经在这一领域从事工作,你应该了解在现在这个高速发展、要求苛刻的电子商务和信息技术的世界中,企业级的应用程序必须具有以下特点:花费更少的金钱、具有更快的速度、占用更少的资源。

  为了减少费用,快速设计和开发企业级的应用程序,Java 2 Platform, Enterprise Edition (J2EE)技术提供了一个基于组件的方法来设计、开发、装配和部署企业级应用程序。J2EE平台提供了一个多层结构的分布式的应用程序模型,该模型具有重用组件的能力、基于扩展标记语言(XML)的数据交换、统一的安全模式和灵活的事务控制。你不仅可以比以前更快地发表对市场的新的解决方案,而且你的独立于平台的基于组件的J2EE解决方案不再受任何提供商的产品和应用程序编程界面(APIs)的限制。提供商和买主都可以自己选择最合适于它们的商业应用和所需技术的产品和组件。

  1、分布式的多层应用程序

  J2EE平台使用了一个多层的分布式的应用程序模型。应用程序的逻辑根据其实现的不同功能被封装到组件中,组成J2EE应用程序的大量应用程序组件根据在其所属的多层的J2EE的环境中所处的层被安装到不同的机器中。图1-1表示了两个多层的J2EE应用程序根据下面的描述被分为不同的层。在图1-1中涉及的J2EE应用程序的各个部分将在J2EE组件中给出详细描述。

  1、运行在客户端机器的客户层组件。
  2、运行在J2EE服务器中的Web层组件。
  3、运行在J2EE服务器中的商业层组件。
  4、运行在EIS服务器中的企业信息系统(EIS)层软件。

  尽管从图1-1中可以看到J2EE应用程序既可以是三层结构,也可以是四层结构,但是我们通常将J2EE应用程序的多层结构考虑为三层结构。这是因为它们分布在三个不同的位置:客户端机器、J2EE服务器机器和在后端的传统的机器。三层结构的应用程序可以理解为在标准的两层结构的客户端/服务器模式的客户端应用程序和后端存储资源中间增加了一个多线程的应用程序服务器。



图1-1:多层结构的应用程序
  2、J2EE组件

  J2EE应用程序由组件组成。一个J2EE组件就是一个自带功能的软件单元,它随同它相关的类和文件被装配到J2EE应用程序中,并实现与其它组件的通信。J2EE规范是这样定义J2EE组件的:
  客户端应用程序和applet是运行在客户端的组件。
  Java Servlet和JavaServer Pages (JSP)是运行在服务器端的Web组件。
  Enterprise JavaBean(EJB)组件(enterprise bean)是运行在服务器端的商业软件。
  J2EE组件由Java编程语言写成,并和用该语言写成的其它程序一样进行编译。J2EE组件和"标准的"Java类的不同点在于:它被装配在一个J2EE应用程序中,具有固定的格式并遵守J2EE规范,它被部署在产品中,由J2EE服务器对其进行管理。

  3、J2EE客户端

  一个J2EE客户端既可以是一个Web客户端,也可以是一个应用程序客户端。

  3.1 Web客户端
  一个Web客户端由两部分组成:由运行在Web层的Web组件生成的包含各种标记语言(HTML、XML等等) 的动态Web页面和接受从服务器传送来的页面并将它显示出来的Web页面。
  一个Web客户端有时被称之为瘦客户端。瘦客户端一般不做象数据库查询、执行复杂的商业规则及连接传统应用程序这样的操作。当你使用一个瘦客户端时,象这样的重量级的操作被交给了在J2EE服务器执行的enterprise bean。这样就可以充分发挥J2EE服务器端技术在安全性、速度、耐用性和可靠性方面的优势。

  3.2 Applets
  从Web层接收的一个Web页面可以包含内嵌的applet。一个applet是一个用Java编程语言编写的小的客户端应用程序,它在安装在Web浏览器中的Java虚拟机中运行。然而,为了在Web浏览器中成功地运行applet,客户端系统很可能需要Java插作和安全策略文件。
  Web组件是用来建立一个Web客户端程序的首选的API,因为这样在客户端系统中就不需要插件和安全策略文件。同样的,使用Web组件可以有效地改善应用程序设计,因为它们提供了一个将应用程序设计和Web页面设计有效分离的途径。Web页面的设计者可以不必关心Java编程语言的语法就能很好地完成自己的工作。
3.3 应用程序客户端
  一个J2EE应用程序客户端运行在客户端机器上,它使得用户可以处理需要比标记语言所能提供的更丰富的用户界面的任务。具有代表性的是用Swing或抽象窗口工具包(AWT)API建立的图形用户界面(GUI),但是一个命令行界面也是当然可能的。
  应用程序客户端直接访问运行在商业层的enterprise bean。然而,如果应用程序需要授权, 一个J2EE应用程序客户端可以打开一个HTTP连接来与一个运行在Web层的servlet建立通信。

  4、JavaBeans组件体系结构

  服务器层和客户层也可以包含以JavaBean组件体系结构(JavaBeans组件)为基础的组件来管理在一个应用程序客户端或applet与运行在J2EE服务器上的组件之间的数据流动以及服务器端组件与数据库之间的数据流动。在J2EE规范中JavaBeans组件不被认为是J2EE组件。

  JavaBeans组件具有实例变量以及用来访问实例变量中的数据的get方法和set方法。作这种用途的JavaBeans组件在设计和执行时相当简单,但是它必须遵守JavaBeans组件体系结构的命令和设计惯例。

  5、J2EE服务器通信

  图1-2显示了客户层组成的多种方式。客户端可以直接和运行在J2EE服务器中的商业层进行通信。如果是一个运行在浏览器中的客户端,也可以通过运行在Web层中的JSP页面和Servlet进行这种通信。
你的J2EE应用程序是采用瘦客户端还是胖客户端。要作出这样的决定,你应该明白将胖客户端是将功能留在客户端,使它与用户更接近,而瘦客户端是将功能的实现尽可能地交给服务器。由服务器处理更多的功能,就更容易分发、部署和管理应用程序;而将更多的功能留在客户端对于有经验的用户也许是个明智的选择。



图1-2 服务器通信
  5.1 Web组件
  J2EE的Web组件既可以是servlet也可以是JSP页面。Servlets是一个Java编程语言类,它可以动态地处理请求并作出响应。JSP页面是一个基于文本的文档,它以servlet的方式执行,但是它可以更方便建立静态内容。
  在装配应用程序时,静态的HTML页面和applet被绑定到Web组件中,但是它们并不被J2EE规范视为Web组件。服务器端的功能类也可以被绑定到Web组件中,与HTML页面一样,它们也不被J2EE规范视为Web组件。
正如图1-3中所示,和客户层一样,Web层也可以包含一个JavaBeans组件以管理用户的输入并将输入发送到运行在商业层的enterprise bean进行处理。



图1-3 Web层和J2EE应用程序
  5.2 商业组件
  商业代码,表示了例如银行、零售和财政这样的特定的商业领域的相适应的逻辑。它由运行在商业层的enterprise bean处理。图1-4显示了一个enterprise bean如何从客户端接受数据,对它进行处理(如果需要),并将其发送到企业信息系统层以作存储。一个enterprise bean也可以从存储器获取数据,对它进行处理(如果需要),并将其发送到客户端应用程序。



图1-4 商业层和EIS层

有三种类型的enterprise beans:session beans、entity beans和message-driven beans。一个session bean描述了与客户端的一个短暂的会话。当客户端的执行完成后,session bean和它的数据都将消失。与些相对应的是一个entity bean描述了存储在数据库的表中的一行的持久稳固的数据。如果客户端终止或者服务结束,底层的服务会负责entity bean数据的存储。
  一个message-driven bean结合了一个session bean和一个Java信息服务(JMS)信息监听者的功能,它允许一个商业组件异步地接受JMS消息。这份指南只介绍entity bean和session bean。有关message-driven bean的介绍,请参看Java消息服务指南:
http://java.sun.com/products/jms/tutorial/index.html

  6、企业信息系统层

  企业信息系统层处理企业信息系统软件并包含诸如企业资源计划(ERP)、主机事务处理、数据库系统和其它传统系统这样的底层系统。J2EE应用程序组件可能需要访问企业信息系统,例如是获得一个数据库连接。

  6.1 J2EE容器
  通常,瘦客户端的多层应用程序是很难编写的,这是因为这得包括许多行复杂的代码以处理事务、状态管理、多线程、资源池和其它复杂的底层详细资料。基于组件并与平台无关的J2EE体系结构使得J2EE应用程序易于编写,这是因为商业逻辑被封装到可重用的组件中,此外,J2EE服务器以容器的形式为每一个组件类型提供底层服务。因为我们不需要自己开发这些服务,这使我们可以全力以赴地着手处理商业问题。

  6.2 容器服务
  容器是一个组件和支持组件的底层平台特定功能之间的接口,在一个Web组件、enterprise bean或者是一个应用程序客户端组件可以被执行前,它们必须被装配到一个J2EE应用程序中,并且部署到它们的容器。
装配的过程包括为J2EE应用程序中的每一个组件以及J2EE应用程序本身指定容器的设置。容器设置定制了由J2EE服务器提供的底层支持,这将包括诸如安全性、事务管理、Java命名目录接口(JNDI)搜寻以及远程序连接。下面是其中的主要部分:

  1、J2EE的安全性模式可以让你对一个Web组件或enterprise bean进行配置以使得只有授权用户访问系统资源。
  2、J2EE的事务模式可以让你指定方法之间的关系以组成一个单个的事务,这样在一个事务中的所有方法将被视为一个单一的整体。
  3、JNDI搜寻服务为企业中的多种命名目录服务提供一个统一的接口,这使得应用程序组件可以访问命名目录服务。
  4、J2EE远程连接模式管理客户端和enterprise bean之间的底层通信。在一个enterprise bean被建立后,客户端在调用其中的方法时就象这个enterprise bean就运行在同一个虚拟机上一样。

  实际上,J2EE体系结构提供了可配置的服务意味着在相同的J2EE应用程序中的应用程序组件根据其被部署在什么在地方在实际运行时会有所不同。例如,一个enterprise bean可能在一个产品环境中拥有包含访问数据库数据的某种级别的安全性设置,而在另一个产品环境中是另一个访问数据库的级别。
  容器还管理诸如一个enterprise bean和servlet的生存周期、数据库连接资源池以及访问在J2EE APIs中介绍的J2EE平台API这样不能配置的服务。尽管数据持久化是一个不能配置的服务,但是J2EE体系统结构允许你在你想要获得比默认的容器管理持久化所能提供更多的控制时,通过在你的enterprise bean执行中包含适当的代码以重载容器管理持久化。例如,你可以使用bean管理持久化以实现你自己的finder(查找)方法或者是建立一个定制的数据库缓冲区。

  6.3容器类型
  部署时会将J2EE应用程序组件安装到J2EE容器中,就象图1-5中所示那样。

  1、J2EE服务器:是J2EE产品的运行部分。一个J2EE服务器提供EJB容器和Web容器。Enterprise JavaBeans(EJB)容器:管理J2EE应用程序的enterprise bean的执行。Enterprise bean和它的容器运行在J2EE服务器中。
  2、Web容器:管理J2EE应用程序的JSP页面和servlet组件的执行。Web组件和它的容器也运行在J2EE服务器中。
  3、客户端应用程序容器:管理应用程序客户端组件的运行。应用程序客户端和它的容器运行在客户端中。
  4、Applet容器:管理applet的执行。由运行在客户端的一个Web浏览器和Java插件一同组成。





图1-5 J2EE服务器和容器

7、封装

  J2EE组件被分别打包并绑定到一个J2EE应用程序中以供部署。每一个组件、它的诸如GIF、HTML文件和服务器端功能类这样的相关文件以及一个部署说明组成了一个模块并被添加到J2EE应用程序中。一个J2EE应用由一个或几个enterprise bean组件模块、Web组件模块或应用程序客户端组件模块组成。根据不同的设计需求,最终的企业解决方案可以是一个J2EE应用程序,也可以由两个或更多的J2EE应用程序组成。
  一个J2EE应用程序以及它的每一个模块有它自己的部署说明。一个部署说明就是一个具有.xml扩展名的XML文件,它描述了一个组件的部署设置。例如,一个enterprise bean模块的部署说明,描述了一个enterprise bean的事务属性和安全性授权。因为部署说明信息是公开的,因此它可以被改变而不必修改bean的源代码。在运行时,J2EE服务器将读取这个部署说明并遵照执行。
  一个J2EE应用以及它的所有模块被提交到一个Enterprise ARchive (EAR)文件中。 一个EAR文件就是一个具有.ear扩展名的标准的Java Archive (JAR)文件。在J2EE SDK应用程序部署工具的GUI版本中,你首先建立一个EAR文件,并在其中添加JAR文件和Web Archive(WAR)文件。然而,如果你使用的是命令行的打包工具,你必须首先建立JAR和WAR文件,然而才是建立EAR文件。有关J2EE SDK工具的详细介绍请参看工具。

  1、每一个EJB JAR文件包含一个部署说明、enterprise bean文件以及相关的文件。
  2、每一个应用程序客户端JAR文件包含一个部署说明、应用程序客户端的类文件以及相关的文件。
  3、每一个WAR文件包含一个部署说明、Web组件文件以及相关的资源。

  使用模块和EAR文件使得运用同一组件以装配许多不同的J2EE应用程序成为可能。不需要额外的编程工作,你唯一要做的是在J2EE EAR文件中添加各种J2EE模块。

  8、开发角色

  可重用的模块使得将应用程序的开发过程和部署过程分成不同的角色成为可能。这样,不同的人或公司可以在这一过程的各个部分承担不同的任务。
  最先的两个角色承担购买和安装J2EE产品和工具的任务。在购买和安装好软件之后,应用程序组件提供商就开始开发J2EE组件,应用程序装配者负责装配,而应用程序部署者负责部署。在一个大的组织中,每个这样的角色可能对应于不同的个人和小组。作这样的分工是因为前一个角色将会输出一个可移植的文件,而这将是后一个角色的输入。例如,在应用程序组件提供商阶段,一个enterprise bean软件开发者提交EJB JAR文件。而在应用程序装配者阶段,另一些开发者将这些EJB JAR文件组合到一个J2EE应用程序中并将其保存为一个EAR文件。在应用程序部署者阶段,消费者站点的系统管理员使用EAR文件将J2EE应用程序安装到J2EE服务器中。
  不同的角色不一定总得由不同的人来执行。例如,你是在一个小公司工作或者你是从事一个样本程序的原型开发,你也可能需要执行每一个阶段的任务。

  9、J2EE产品提供者

  J2EE产品提供者是设计并提供实现J2EE规范所定义的J2EE平台、API和其它功能的公司。这一般是指操作系统、数据库系统、应用程序服务器或Web服务器的卖主,它们依照J2EE的规范实现J2EE平台。

  9.1工具提供者
  工具提供者是指建立组件提供者、装配者和部署者所使用的开发、装配和打包的工具的公司或个人。有关J2EE SDK 1.3可用工具的详细信息参见工具。

  9.2 应用程序组件提供者
  应用程序组件提供者是指建立J2EE应用程序所使用的Web组件、enterprise bean、applet或应用程序客户端的公司或个人。

  9.3 Enterprise Bean开发者
  一个enterprise bean开发者执行下面的任务并递交一个包含enterprise bean的EJB JAR文件:

  1、编写并编译源代码
  2、详细说明部署描述
  3、将.class文件和部署描述绑定到一个EJB JAR文件中

  9.4 Web组件开发者
  一个Web组件开发者执行下面的任务并提交一个包含Web组的WAR文件:

  1、编写并编译servlet源代码
  2、编写JSP和HTML文件
  3、详细说明Web组件的部署描述
  4、将.class、.jsp、.html和部署描述绑定到WAR文件中

  9.5 J2EE应用程序客户端开发者
  一个应用程序客户端开发者执行下面的任务并提交一个包含J2EE应用程序客户端的JAR文件:

  1、编写并编译源代码
  2、详细说明客户端的部署描述
  3、将.class文件和部署描述绑定到JAR文件中

  9.6 应用程序装配者
  应用程序装配者是从组件提供者接受应用程序组件JAR文件并将其装配到一个J2EE应用程序EAR文件中的公司或个人。装配者或部署者可以直接编辑部署说明或者使用可以根据交互式的正确地添加XML标志的工具。一个软件开发者执行下列任务并递交一个包含J2EE应用程序的EAR文件:

  1、将上一阶段建立的EJB JAR文件和WAR文件装配到一个J2EE应用程序(EAR)文件中。
  2、详细说明有关J2EE应用程序的部署说明。
  3、检验EAR文件中的组件是否遵守J2EE规范。
9.7 应用程序部署者和系统管理员
  应用程序部署者和系统管理员可以是公司或个人,他们配置和部署J2EE应用程序,管理J2EE应用程序在其中运行的计算机和网络这些低层结构,并对运行环境进行监控。他们的任务可能包括这样的一些事:设置事务控制、安全属性并指定数据库连接。

  在配置时,部署者按照由应用程序组件提供者提供的指示以解决外部的支持、指定安全设置并定义事务属性。在安装时,部署者将应用程序组件装入服务器中并生成特定容器的类和接口。

  一个部署者/系统管理员执行下列任务以安装和配置一个J2EE应用程序:

  1、将上一阶段生成的J2EE应用程序(EAR)文件添加到J2EE服务器中
  2、根据运行环境通过修改J2EE应用程序的部署说明对J2EE应用程序进行配置。
  3、检验EAR文件的内容是否遵守J2EE规范
  4、部署(安装)J2EE应用程序EAR文件到J2EE服务器中

  10、相关执行软件

  J2EE SDK是非经营的J2EE平台的操作定义,它由Sun公司提供可以免费用于示范、试验和教育用途。它包含了J2EE应用程序服务器、Web服务器、关系型数据库、J2EE API和一整套开发和部署工具。你可以从这里下载J2EE SDK:
  http://java.sun.com/j2ee/download.html#sdk

  J2EE SDK的目的是为了允许产品提供者用来测试它们的执行是否需要一系列特定的条件,并可以运行J2EE兼容性测试以确定他们的J2EE产品是否完全遵守J2EE规范。J2EE SDK还可以由应用程序组件开发者用来运行他们的J2EE应用程序以检验它们是否完全适合所有的J2EE产品和工具。

  10.1数据库访问
  关系型数据库为应用程序数据提供了持久稳固的存储。一个J2EE执行并不需要支持特定类型的数据库,这意味着不同的J2EE产品所支持的数据库可以改变。参看下载的J2EE SDK所包含的版本说明可以了解目前所支持的数据库。

  10.2 J2EE API
  J2SE SDK对于J2EE SDK的运行是必需的,它可以为编写J2EE组件提供核心的API、核心的开发工具和Java虚拟机。J2EE SDK为J2EE应用程序提供下面的API以供使用。

  10.3 Enterprise JavaBeans技术2.0
  一个enterprise bean是一个用来执行商业逻辑的模块的具有字段和方法的代码实体。你可以将一个enterprise bean想象成一块积木,它可以在J2EE服务器上单独使用也可以与其它enterprise bean协同工作以执行商业逻辑。
  有三种类型的enterprise bean:session bean、entity bean和message-driven bean。Enterprise bean经常与数据库有一个交互的关系。使用entity bean的一个好处是你不需要编写任何SQL代码或使用JDBC API以直接执行数据库访问操作;EJB容器会为你处理这些事。然而,如果因为某种原因,你重载了默认的容器管理持续化,你需要使用JDBC API。同样的,如果你选择一个session bean来访问database,你也必须使用JDBC API。

  10.4 JDBC API 2.0
  JDBC API让你可以从Java编程语言的方法中调用SQL命令。在一个enterprise bean中,当你重载了默认的容器管理持续化或是在一个session bean中访问数据库时,你必须使用JDBC API。当使用容器管理持续化时,数据库访问的操作是由容器来处理的,你的enterprise bean执行不包含任何JDBC代码或SQL命令。你也可以使用JDBC API从一个servlet或JSP页面中直接访问数据库,而不通过一个enterprise bean来完成。
JDBC API有两个部分:一个由应用程序组件用来访问数据库的应用程序级的接口,一个用来将JDBC驱动整合到J2EE平台中的服务提供商接口。

  10.5 Java Servlet技术2.3
  Java Servlet技术允许你定义一个特定的HTTP的servlet类。一个servlet类为服务器扩展了这样一个性能:可以通过请求-响应这样的程序模式访问主机应用程序。尽管servlet可以响应任何形式的请求,但是它们通常用于Web服务器上的应用程序。

  10.6 JavaServer Pages技术1.2
  JavaServer Pages技术使得你可以将servlet代码嵌入到基于文本的文档中。一个JSP页面就是一个基于文本的文档,它包含两个类型的文本:静态模板的数据,它可以表示为任何基于文本的格式,例如HTML、WML和XML;JSP单元,它决定页面如何建立动态的内容。

  10.7 Java消息服务(JMS)1.0
  JMS是一个消息标准,它允许J2EE应用程序建立、发送、接受和阅读消息。它使得建立连接简单的、可靠的和异步的公布式通信成为可能。有关JMS的更多的介绍,请参看Java消息服务指南:
  http://java.sun.com/products/jms/tutorial/index.html

  10.8 Java命名目录接口(JNDI)1.2
  JNDI提供命名的目录功能。它为应用程序提供标准的目录操作的方法,例如获得对象的关联属性、根据它们的属性搜寻对象等。使用JNDI,一个J2EE应用程序可以存储和重新得到任何类型的命名Java对象。
因为JNDI不依赖于任何特定的执行,应用程序可以使用JNDI访问各种命名目录服务,包括现有的各种诸如LDAP、NDS、DNS和NIS这样的命名目录服务。这使得J2EE应用程序可以和传统的应用程序和系统共存。有关JNDI的更多的信息,请参看JNDI指南:
  http://java.sun.com/products/jndi/tutorial/index.html

  10.9 Java事务API 1.0
  Java事务API(JTA)提供了划分事务的标准接口。J2EE体系结构提供了一个默认的自动提交以处理事务提交和回滚。一个自动提交意味着在每一个数据库读写操作之后任何其它应用程序显示数据时都会看到更新了的数据。然而,如果你的应用程序执行两部分相互依赖的数据库访问操作,你可能会想要用JTA API去确定整个事务,这个事务将包含两个操作的开始、回滚和提交。

  10.10JavaMail API 1.2
  J2EE应用程序可以使用JavaMail API来发送e-mail告示。JavaMail API包含两部分: 一个由应用程序组件用来发送mail的应用程序级的接口和一个服务提供接口。J2EE平台包含作为服务提供的JavaMail,使得应用程序组件可以发送Internet mail。

  10.11 JavaBeans激活架构1.0
  之所以要包含JavaBeans激活架构(JAF)是因为JavaMail要使用到它。它提供标准的服务以确定任意数据段的类型、如何对它访问、找出在其上可应用的操作并建立适当的JavaBeans组件以执行那些操作。
10.12 Java XML处理API1.1
  XML是一种描述基于文本的数据的语言,使用XML使得数据可以被任何程序和工具读取和处理。程序和工具可以生成其它程序和工具可以读取和处理的XML文档。Java XML处理API(JAXP)支持使用DOM、SAX和XSLT对XML文档进行处理。JAXP使得应用程序可以不依赖于特殊的XML处理执行来解析和转换XML文档。
  例如,一个J2EE应用程序可以使用XML来生成报表,而不同的公司都可以获得这个报表并使用各自最适宜的方法来处理它。一个公司可能会通过程序将XML数据导入到HTML中以使得其可以在网站中公布,另一个公司可能会通过工具导出XML数据以制定销售预算,而另一个公司可能会将XML数据导入它的J2EE应用程序中对其进行处理。

  10.13 J2EE连接器体系结构1.0
  J2EE工具提供商和系统综合者使用J2EE连接器体系结构建立可以加入到任何J2EE产品的支持访问企业信息系统的资源适配器。一个资源适配器就是一个使得J2EE应用程序组件可以访问底层的资源管理器并与其实现交互的软件组件。因为一个资源适配器是与它的特定的资源管理器相对应的,典型的情况是不同的数据库或企业信息系统会各自有其不同的资源适配器。

  10.14 Java认证和授权服务1.0
  Java认证和授权服务(JAAS)为J2EE应用程序提供了一个方法以为一个特定的用户或一组用户进行认证和授权。
  JAAS是标准的可插入认证模块(PAM)结构的Java版本,它对Java 2平台的安全认证框架进行了扩展以支持基于用户的安全认证。

  11、简单的系统集成

  J2EE平台具有平台无关性,全部的系统集成解决方案建立了一个开放的市场,在这个市场中,每个提供商都可以向所有的用户出售他们的产品。这样的一个市场鼓励提供商进行竞争,不是试图将通过技术困缚用户,而是通过提供比其它提供商更好的产品和服务,例如是更好的性能、更好的工具或更好的用户支持。
J2EE API使得系统和应用程序集成具有下面的这些特点:

  1、enterprise beans所支持的统一的应用程序多层结构
  2、JSP页面和servlet所支持的简单的请求和响应机制
  3、JAAS所支持的可靠的安全模型
  4、JAXP所支持的基于XML的数据交换集成
  5、J2EE连接器体系结构所支持的简单的协同工作能力
  6、JDBC API所支持的方便的数据库连通性
  7、message-driven beans、JMS、JTA和JNDI所支持的其它特性

  要学习更多的有关使用J2EE平台以建立商业综合系统的知识,你可以参阅J2EE技术实践:http://java.sun.com/j2ee/inpractice/aboutthebook.html

  11.1工具
  J2EE实现标准提供了一个应用程序部署工具和一系列命令以装配、校验和部署J2EE应用程序和管理你的部署和产品环境。

  11.2应用程序部署工具
  J2EE实现标准提供了一个应用程序部署工具(deploytool)以装配、校验和部署J2EE应用程序。这个工具有两个版本:命令行和GUI。

  GUI工具包括下列向导

  1、打包、配置和部署J2EE应用程序
  2、打包和配置enterprise bean
  3、打包和配置Web组件
  4、打包和配置应用程序客户端
  5、打包和配置资源适配置器
  6、此外,还可以在tabbed inspector pane中对每个组件和模块类型的配置信息进行设置。

  11.3命令
  表1-1列出了J2EE实现标准中所包含的命令,你可以使用这些命令在命令行执行操作。



表1-1 J2EE命令

j2ee

启动和终止J2EE服务

cloudscape

启动和终止默认的数据库

j2eeadmin

增加JDBC驱动、JMS目的文件以及不同资源的连接factory

keytool

建立公钥和私钥,并生成X509自签署证书。

realmtool

导入证书文件。为一个J2EE应用程序的认证和授权列表中增加或删除J2EE用户

packager

将J2EE应用程序组件打包到EAR、EJB JAR、应用程序客户端JAR或WAR文件中

verifier

校验EAR、EJB JAR、应用程序客户端JAR和WAR文件是否符合并遵守J2EE规范

runclient

运行一个J2EE应用程序客户端

cleanup

从J2EE服务器中删除所有已部署的应用程序

在小型设备上获取 Java 网络应用程序

Soma Ghosh
应用程序开发人员, Entigo
2002 年 12 月 09 日

如果您一直都在关注无线专区中 Soma Ghosh 的文章,那么您就已经知道了如何使用您的 Java 技能来为手持设备构建简单的应用程序。那您如何将那些设备连接到外部世界呢?在本文中,Ghosh 讨论了 J2ME 联网中至关重要的 javax.microedition.io 类和 java.io 类。您将学习到 J2ME 应用程序如何处理 URL 和接受输入,本文甚至还会带您完成一个样本程序,这个程序把货币兑换信息下载到网络可访问的、遵循 J2ME 的任意设备上。

J2ME I/O 与联网:概览
Java 2 平台,袖珍版(Java 2 Platform,Micro Edition (J2ME))提供了把网络上可用的资源扩展到移动空间中的联网功能。现在,在移动电话或掌上电脑获取实时股票报价或最新贷币汇率是可能的。

javax.microedition.io 中的各个类和接口处理移动信息设备框架(Mobile Information Device Profile,MIDP)的联网功能,MIDP 则是一个开发移动设备应用程序的平台。(想了解更多有关 MIDP 的信息,请访问下面的 参考资料部分,链接到我先前已发表在 developerWorks上的关于这个主题的文章。)

另一方面, java.io 包给 MIDP 提供了输入/输出(input/output(I/O))功能。它的各个类和接口为数据流提供了系统输入和输出。这个 J2ME 包是 Java 2 平台,标准版(Java 2 Platform,Standard Edition(J2SE)) java.io 包的一个子集,它处理低级别的数据 I/O。

J2ME 网络连接性最关键的方面是移动设备与 Web 服务器间的通信。这种通信本质上是客户机/服务器机制,其中移动设备充当 Web 客户机的角色并有能力与企业系统、数据库、公司内部网和因特网建立接口。

J2ME 联网活动可以按照通信协议分为许多种类别。我们将在以下几部分中依次讨论每一种类别。

低级别的 IP 联网
这一类别涉及到套接字、数据报、串口和文件 I/O 通信。基于套接字的通信遵循面向连接的 TCP/IP 协议。另一方面,基于数据报的通信遵循无连接的 UDP/IP 协议。UDP 为应用程序提供了不必建立连接就能发送经过封装的原始 IP 数据报的方法。面向连接的协议需要源地址和目的地址,与此不同,数据报只需要目的地址。下面是数据报连接用来在某端口接受数据报的一个 URI:


datagram://:1234

这里是数据报连接用来在某端口将数据报发送到服务器的一个 URI:


datagram://123.456.789.12:1234

低级别的 IP 联网还可以处理文件 I/O 并且能够允许 MIDlet 注册对本地串口进行网络访问。

安全联网
J2ME 中的安全联网涉及到一些为了与基于 Web 的网络服务进行安全通信而提供的额外接口。这些安全接口由 IP 网络上的 HTTPS 和 SSL/TLS 协议访问提供。

HTTP 联网
移动设备与 Web 服务器之间基于 HTTP(Hypertext Transfer Protocol,超文本传输协议)进行通信。HTTP 是一个面向连接的请求-响应(request-response)协议,在这个协议中,必须在发送请求之前设置请求的各参数。

图 1 说明了移动设备与 Web 服务器间的通信机制。

图 1. 移动设备与 Web 服务器间的连接机制
移动设备与 Web 服务器间的连接机制

连接框架
J2ME 联网旨在处理移动设备的广泛频谱不同的需要。同时,联网系统必须是特定于设备的。为了应付这些要求,J2ME 联网引入了 通用连接框架(generic connection framework)的概念。

通用连接框架的设想是以 Java 接口的形式定义一些能够覆盖联网和文件 I/O 的通用方面的抽象。这个体系结构广泛支持各种手持设备,而将这些接口的实际实现留给了各个设备制造商。设备制造商可以根据其设备的实际功能选择要在它的特定 MIDP 中实现哪个接口。

由 Java 接口定义的通用方面分为以下几种形式的基本通信类型:

  • 基本串行输入(由 javax.microedition.io.InputConnection 定义)
  • 基本串行输出(由 javax.microedition.io.OutputConnection 定义)
  • 数据报通信(由 javax.microedition.io.DatagramConnection 定义)
  • 用于客户机-服务器(client-server)通信的套接字通信通知机制(由 javax.microedition.io.StreamConnectionNotifier 定义)
  • 与 Web 服务器进行的基本 HTTP 通信(由 javax.microedition.io.HttpConnection 定义)

J2ME 中的 URL 处理
J2ME 中的 URL 处理涉及到从移动设备打开一个到 Web 服务器的连接并处理移动设备到 Web 服务器间的数据 I/O。这个过程发生在下面的阶段:

  • 建立(Setup),此时尚未建立到服务器的连接。移动设备准备一堆请求参数并准备接受和解释随后的响应。
  • 已连接(Connected),此时连接已经被建立,请求参数已经被发送并在期待响应。
  • 已关闭(Closed),此时连接已经被关闭。

J2ME 定义了 javax.microedition.io.Connector 类,这个类包含了用于创建所有连接对象的各个静态(static)方法。这一任务是通过根据平台名称和所请求连接的协议动态地查找一个类来完成的。

在 URL 处理中, Connector.open() 用来打开 URL;它返回一个 HttpConnection 对象。 Connector.open() 方法的字符串(string)参数是一个有效的 URL。URL 字符串由于通信协议的不同而不同,下面的清单 1 到清单 5 演示了这一点。

清单 1. 调用基于 HTTP 的通信


Connection conn = Connector.open("http://www.yahoo.com");


清单 2. 调用基于流的套接字通信


Connection conn = Connector.open("socket://localhost:9000");


清单 3. 调用基于数据报的套接字通信


Connection conn = Connector.open("datagram://:9000");


清单 4. 调用串口通信


Connection conn = Connector.open("comm:0;baudrate=9000");


清单 5. 调用文件 I/O 通信


Connection conn = Connector.open("file://myfile.dat");

Connector.open() 方法还可以接受访问模式(值为 READWRITEREAD_WRITE )以及一个用来表示调用者想要得到超时通知的标志。

在安全的联网中,当 https:// 连接字符串被访问时, Connector.open() 就会返回 HttpsConnection 。当 ssl:// 连接字符串被访问时, Connector.open() 就会返回 SecureConnection

无论使用哪一种类型的 URL,调用 Connector.open() 都会打开一个从 Connectionjava.io.InputStream 的字节输入流。这个方法用来读取文件的每一个字符,一直读到文件末尾(以 -1 为标志)。如果抛出一个异常,连接和流就会被关闭。

与此相似,为了进行输出,代表字节输出流的 java.io.OutputStream 将被从 Connection 打开。

InputStreamOutputStream 分别与 java.io.DataInputStreamjava.io.DataOutputStream 相对应。 DataInputStream 让应用程序用与机器无关的方式从底层输入流读取基本的 Java 数据类型。 java.io.DataOutputStream 让应用程序用可移植的方式把基本的 Java 数据类型写到输出流。

清单 6 说明了如何使用 HttpConnection 从 URL 输入数据。

清单 6. 使用 HttpConnection 从 URL 输入数据


String getViaHttpConnection(String url) throws IOException {
         HttpConnection c = null;
         InputStream is = null;
              StringBuffer str = new StringBuffer();

              try {
                  c = (HttpConnection)Connector.open(url);

                // Getting the InputStream will open the connection
                // and read the HTTP headers. They are stored until
                // requested.
                is = c.openInputStream();

                // Get the length and process the data
             int len = (int)c.getLength();
             int ch;
             while ((ch = is.read()) != -1) {
                 str.append((char)ch);
             }

          } finally {
          if (is != null)
              is.close();
          if (c != null)
              c.close();
        }

一个贷币兑换应用程序
我们将通过一个贷币兑换应用程序来说明迄今为止所概述过的概念,这个应用程序将会显示美元(U.S. dollar,USD)和英镑(British pound,GBP)之间最新的汇率。这个应用程序还会显示任意与当前日期和当前时间相关的信息。

这个应用程序的 UI 由一个表单( Form )和一个退出(exit)命令组成,该表单嵌入了代表一个只显示字符串的 StringItem ,退出命令用于完成调用时让应用程序退出。

一旦启动应用程序,URL 请求便已准备就绪。基本的货币符号被提供给请求。接下来,我们需要打开一个移动设备与 Web 服务器间的 URL 连接。打开一个 HttpConnection 并为数据输入建立一个 InputStream 。所获得的数据是一个字符流,这个多字符流附加在 String 中。产生的 String 代表 HTML 输出。由于我们的移动设备上的浏览器不能显示 HTML,因而我们将解析 HTML String 来获取货币值以及任何相关的信息。

我们的程序将在 HTML String 中搜索一个特定模式,即 USDGBP 。一旦确定了这个模式的位置,搜索便查找十进制值。当获得了小数点的位置后,各个数字值便会被检索并以适当的顺序排列,从而获取货币值。清单 7 说明了如何获取货币值。

清单 7. 检索货币值


            String retVal = "";
            int dec = 0;

            int index = str.indexOf("USDGBP");
            if (index != -1)
                   str = str.substring(index, str.length());

            if ( (( dec = str.indexOf(".")) != -1) && (!(str.endsWith(".")))
             && Character.isDigit(str.charAt(dec+1)) )
{
  String front = "";
  int find = dec-1;
  while (Character.isDigit(str.charAt(find)))
  {
    front += str.charAt(find);
    find--;
    }
  retVal += new StringBuffer(front).reverse().toString();
  retVal += ".";

  String back = "";
  int bind = dec+4;
  while (Character.isDigit(str.charAt(bind)))
    {
     back += str.charAt(bind);
     bind--;
    }
  retVal += new StringBuffer(back).reverse().toString();

}

相关信息也是通过查找某些特定的字符串模式来获取的。一旦确定了数据在 HTML String 中的位置,一个偏移量便会被应用来获取日期和时间。这个信息被与原先用于搜索的字符串模式附加在一起。清单 8 说明了如何获取相关信息。

清单 8. 检索相关信息


// Get the time of Currency Exchange and Related information
  int timeIndex = str.indexOf("U.S. Markets Closed.");
  String retTime = "";
  if (timeIndex != -1)
  {
   retTime = str.substring(timeIndex-34, timeIndex);
   retTime += " U.S. Markets Closed ";
  }

清单 9 包括货币兑换应用程序的全部代码。

清单 9. 完整的货币兑换示例


import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.io.*;
import java.io.*;
import java.lang.*;
import java.util.*;

//A first MIDlet with simple text and a few commands.
public class CurrencyExchange extends MIDlet
               implements CommandListener {

  //The exit commands
  private Command exitCommand; 

  //The display for this MIDlet
  private Display display;

  Form displayForm;

  public CurrencyExchange() {
    display = Display.getDisplay(this);
    exitCommand =
    new Command("Exit", Command.SCREEN, 1);

  }

 // Start the MIDlet by creating the Form and
  // associating the exit command and listener.
  public void startApp() {
   displayForm = new Form("Exchange Rate");
   displayForm.addCommand(exitCommand);
   displayForm.setCommandListener(this);

try
{

   String result = getViaHttpConnection
   ("http://finance.yahoo.com/m5?a=1&s=USD&t=GBP");
   displayForm.append(" " + result);

}
catch (Exception exc)
{
   exc.printStackTrace();
}
display.setCurrent(displayForm);

  }

   // Pause is a no-op because there is no   background
  // activities or record stores to be closed.
  public void pauseApp() { }

  // Destroy must cleanup everything not handled
  // by the garbage collector.
  // In this case there is nothing to cleanup.
  public void destroyApp(boolean unconditional) { }

 // Respond to commands. Here we are only  implementing
 // the exit command. In the exit command,  cleanup and
 // notify that the MIDlet has been destroyed.
  public void commandAction(
  Command c, Displayable s) {
    if (c == exitCommand) {
     destroyApp(false);
     notifyDestroyed();
    }

  }

String parse(String str)
{
   // Get the time of Currency Exchange and Related information
   int timeIndex = str.indexOf("U.S. Markets Closed.");
   String retTime = "";
   if (timeIndex != -1)
   {
     retTime = str.substring(timeIndex-34, timeIndex);
     retTime += " U.S. Markets Closed ";
   }

   String retVal = "";
   int dec = 0;

   int index = str.indexOf("USDGBP");
   if (index != -1)
     str = str.substring(index, str.length());

if ( (( dec = str.indexOf(".")) != -1) && (!(str.endsWith(".")))
&& Character.isDigit(str.charAt(dec+1)) )
{
   String front = "";
   int find = dec-1;
   while (Character.isDigit(str.charAt(find)))
   {
      front += str.charAt(find);
      find--;
   }
   retVal += new StringBuffer(front).reverse().toString();
   retVal += ".";

   String back = "";
   int bind = dec+4;
   while (Character.isDigit(str.charAt(bind)))
   {
     back += str.charAt(bind);
     bind--;
   }
   retVal += new StringBuffer(back).reverse().toString();

}
System.out.println(retVal);

 return "USD/GBP " + retVal + " at " + retTime ;

}

String getViaHttpConnection(String url) throws IOException {
         HttpConnection c = null;
         InputStream is = null;
         StringBuffer str = new StringBuffer();

         try {
             c = (HttpConnection)Connector.open(url);

             // Get the ContentType
             String type = c.getType();

             // Getting the InputStream will open the connection
             // and read the HTTP headers. They are stored until
             // requested.
             is = c.openInputStream();

             // Get the length and process the data
             int len = (int)c.getLength();
             int ch;
             while ((ch = is.read()) != -1) {
                    str.append((char)ch);
                 }

         } finally {
             if (is != null)
                 is.close();
             if (c != null)
                 c.close();
         }

// Before returning, the resultant String should be parsed to get Exchange Rate

String val = parse(str.toString());
System.out.println(val);
return val;
     }

 }

货币值和其它相关信息不久便出现在移动设备的用户界面上。图 2 说明了显示的结果。

图 2. 在手持设备上运行的货币兑换应用程序
在手持设备上运行的货币兑换应用程序

结束语
J2ME 有一种独特的在受约束环境中处理数据输入/输出的精简方式。有了这种功能,支持 Java 的移动配件就不再是简单的通信设备了,而是能够充当微型浏览器的设备,它可以在移动中提供重要的信息。

参考资料

关于作者
Soma Ghosh 是一位计算机科学与工程的毕业生,在过去七年里,她开发了涉及电子交易和联网领域的不计其数的 Java 应用程序。她相信无线交易是业界近年的前途所在,最近她已经投身到了现有台式机组件和模型的无线倡议中。Soma 现在是 Entigo 的一名应用程序开发人员,Entigo 是实时电子商务解决方案和 B2B 销售和服务端电子交易产品的先锋和业界领头羊。您可以通过 sghosh@entigo.com与 Soma 联系。

2001 年 10 月 16 日

网络通信是当今信息社会网络化一个必不可少的环节。各种为Internet量身定做的网络通信信息技术层出不穷。如早期的CGI,ISAPI等,现在非常流行的JSP,Servlet,ASP等,特别是Sun MicroSystem公司的J2EE和Microsoft的。NET方案,为企业快速高效的实现Internet应用提供了强大支持。而对于一些基于Internet的即时通信,一般是采用C/S模式。即客户端安装并执行客户程序,通过某种通信协议与服务器端的服务器程序或者是直接与另外的客户程序进行通信。本文介绍的是怎样采用Java技术,用B/S模式来实现基于Internet的即时通信,对学习Java Socket编程很有帮助。

为什么选择Java 和 B/S
Sun MicroSystem 的Java技术以其明显的优势得到了广泛应用。如美国华尔街的高盛,美国Amazon.com以及美国通用等的电子商务网站都是采用Java技术。Java语言具有面向对象,面向网络,可移植,与平台无关,多线程,安全等特点。基于网络带宽限制和网络安全等原因,本即时通信系统的客户端用Java小程序(applet)来实现。即通过支持Java的浏览器(如Microsoft Internet Explorer 和 Netscape Navigator等)下载并执行Java Applet 来完成客户端操作。Java Applet 具有体积小,安全等特点。通常,基于C/S模式的通信程序都要求用户先下载一个客户端程序安装在本地机上,而且这个客户程序相对比较大(所谓“胖客户”)。而且,对于一些不可信站点的程序还要考虑到安全因素,因为大多数后门工具就是利用这个缺陷侵入用户计算机的。而使用Java Applet,用户就不必为这些而烦恼。首先,Applet通常很小,用户不必先安装就可立即执行。其次,由于Applet的安全性,用户可以放心使用来自Internet的哪怕是不可信站点的Applet程序。这样,客户端只要拥有支持Java的浏览器就可实现。根据现在的情况来看是不难办到的。而在服务器端我们采用Java Application。这样可以充分发挥Java技术的多线程及平台无关等优点,并且在后方可以借助JDBC与DBMS进行通信,存储和更新数据,以及采用J2EE等技术进行扩展。本文重点放在即时通信,因此服务器端与DBMS的连接技术将不作介绍。

系统设计与实现
开发平台及工具:Windows2000,Jbuilder4(J2SDK1.3),Together4.2。
客户端程序运行环境:拥有支持Java的浏览器的任何平台。
服务器程序运行环境:拥有JVM的任何平台。

1. 需求分析
图2。1。1为系统的Use Case图。

图2。1。1
图2。1。1

用例(Use Case): 远程会话

系统范围(Scope):服务器端系统

级别(Level):顶层(Summary)

外界系统描述(Context of Use):为了与该系统交互,外界系统为远程客户,而每一客户对应于一个客户端程序,客户通过客户端程序与系统交互

主要执行者(Primary Actor):客户

典型成功场景(Main Success Scenario):

  1. 客户通过客户端程序发出"申请新账号"的请求,并提供登录账号和登录密码等申请信息;
  2. 服务器端程序对该客户提交的信息进行验证,并发送"账号申请成功"信息;
  3. 客户接收"申请账号成功"信息后,继续发送请求;
  4. 服务器端程序接收该请求并进行相应处理,然后将执行结果返回给客户;
  5. 重复执行步骤3和步骤4,直到客户发送"会话结束"信息。这时服务器程序完成结束前的处理工作后,断开与客户的连接;

扩展(Extensions):

  • 1a.:系统发现该账号已经存在

    1a.1.:系统返回"该账号已存在"信息,客户可以选择另选账号或者退出

  • 1b:客户提交"登录"信息:
    • 1b.1.:系统对客户身份进行验证:
      • 1b.1a.:验证通过,返回"登录成功"信息
      • 1b.1b:验证不能通过,返回"登录失败"信息,客户可以再尝试登录或者退出
  • 说明:典型成功场景的第1步可以用1a代替,接下来是1a.1;或者用1b代替,后接1b.1,再接1b.1a或者1b.1b。

2. 概要设计

(图2。2。1)
2. 概要设计(图2。2。1)

该系统分为两大部份:客户端程序和服务器端程序。客户端程序采用Java 小程序,通过socket与服务器端程序通信;服务器端程序采用Java Application,同样采用socket与客户端程序进行交互。考虑到即时通信的准确性要求,通信协议采用TCP。

3. 详细设计

  1. 服务器端程序设计:

    服务器端完成的功能是:对服务器的某一可用端口进行监听,以获得客户端请求,从而对客户端请求进行处理。因为是多客户同时请求,所以要采用多线程,为每一个在线用户分配一个线程,实时处理每个客户端的请求。因此,

    对服务器端程序抽象如下:(图2。3。1)

    • a.公共数据处理(Common Data Processing)

      处理公共数据。如在线人数统计,客户的公共数据(如通知等),客户数据资料的存储与读取等(与数据库交互);

    • b. 端口监听器(Port Listener)

      监听服务器某一端口,为每一在线客户建立一个会话线程;

    • 客户请求处理(Client Request Processing)

      处理客户的请求。根据客户的请求执行相应的操作。

    • 服务器管理器


    服务器端的管理工具,如对数据进行统计,紧急情况的处理等。

    服务器端类的设计(图2。3。2和图2。3。3):

    公共数据处理类CmDataProcessor(Common Data Processor):该类包含客户所共有的数据,以及如何对这些数据进行处理。

    端口监听类PortListener(Port Listener):该类实现了java. lang. Runnable接口,从服务器程序初始化完成后一直运行。由于目前JDK只支持同步通信,在没有客户请求时,该线程处于等待状态;一旦有客户请求到来,便继续执行。这时服务器程序可以通过java. net. ServerSocket. accept()方法获得客户端请求的java. net. Socket对象。然后用这个Socket对象为参数构造一个新的线程:ClientSession的实例(类ClientSession以下将作介绍)。然后在ClientSession实例中用该Socket对象构造一个输出流java. io. PrintStream和一个输入流java. io. BufferedReader,以后,每个客户就可以通过这一对输入输出流与服务器交互了。应该注意的是,ServerSocket 对象并不是在该对象内创建的,而是在服务器程序初始化时创建的。因为socket是进程间的通信,在线程中创建将会失败。客户端程序也是如此。

    客户会话类ClientSession(Client Session):该类继承自java. lang. Thread类,由PortListener创建。一般的,每一个在线客户都对应一个ClientSession的实例。该类用parseRequest()方法解析客户发来的请求,进行相应处理。该线程在客户会话期间一直运行,通过I/O流读取和发送数据(I/O流即从PortListener监听线程获得的java. net. Socket对象而创建的java. io. PrintStream和java. io. BufferedReader实例),直到客户退出才撤销。该类和类ClientSession一样都实现了java. lang. Runnable接口,故都有一个run()方法。该方法的结束标志着该线程将结束。

    服务器管理类 ServerManager(Server Manager):管理服务器。拥有管理权限的客户(管理员)可以远程操作服务器程序,包括运行、停止服务器,广播通知,给指定客户发送消息等特权操作。

    图2。3。2
    图2。3。2

    图2。3。3

  2. 客户端程序设计(图2。3。4)

    客户端完成的功能是:建立与服务器的连接;向服务器发送功能请求,接收来自服务器的信息,完成与主机或其他客户交互;断开与服务器的连接。客户端程序相对服务器端程序来说属于LightWeight(轻量级)。这是由本系统的自身特点决定的。所以,对客户端程序抽象如下:

    1. 客户请求发送器:负责功能请求的发送。如登录请求等。
    2. 服务器信息接收器:负责接收来自服务器端的信息。如请求处理结果等。


    客户端类的设计:
    请求发送器(RequestSender):该类发送客户端的功能请求。客户通过客户端用户界面提交要执行的操作,然后由该类将客户提交的信息封装成服务器端程序可以理解的功能请求发送出去。

    信息接收器(Receiver):该类接收来服务器端的信息。这些信息可以是客户请求的处理结果,也可以是服务器端的广播通知。为保证实时性,该类实现了java. lang. Runnable接口。在客户会话期间,该类将一直运行,实时的将来自服务器端的信息反馈给客户。该类接收信息后,应该对该信息做相应处理。如通知客户已登录成功等。这些操作都将在run()方法中实现。

    图2。3。4
    图2。3。4

    4. 实现
    以上的系统设计是一个即时通信系统的总体框架,根据实际情况,可以添加或者修改。下文就以“远程会议系统“为例来实例化这样一个通信系统。

    我们知道,远程会议系统有几个方面的特点:实时交互;准确传输信息;多客户等。所以,完全可以用该系统框架来实现(这里只给出核心代码)。

    首先我们来实现服务器端程序。为了便于对服务器程序的管理,服务器端程序采用了GUI界面。在该程序初始化时应该实现对可用端口的监听(程序清单1。1)。

    程序清单1。1。1

        try {
              socket = new ServerSocket(NetUtil.SLISTENPORT);
            }
    catch (Exception se)
        {
                statusBar.setText(se.getMessage());
            }

    其中,NetUtil.SLISTENPORT是服务器的一个可用端口,可以根据实际情况确定。NetUtil是一个接口,其中包含了该系统用到的各类常数。NetUtil.SLISTENPORT就是NetUtil中的一个整型常数。如果端口监听抛出异常,GUI中的statusBar将给出提示。监听成功后可以启动监听线程了(程序清单1。1。2)。

    程序清单1。1。2

    listenThread=new Thread (new ListenThread ());
    listenThread.start();
    

    以上程序中listenThread是一个Thread实例,用来操作监听线程。监听线程实现如下(程序清单1。2。1):

    程序清单1。2。1

       public class PortListener implements Runnable{
        ServerSocket socket; //服务器监听端口
        FrmServer frm; //FrmServer为GUI窗口类
        ClientSession cs; //客户端会话线程
        PortListener (FrmServer frm,ServerSocket socket) {
          this.frm=frm;
          this.socket=socket;
        }
          public void run () {
            int count = 0;
            if (socket == null) {
                frm.statusBar.setText("socket failed!");
                return;
            }
            while (true) {
                try {
                    Socket clientSocket;
                    clientSocket = socket.accept(); //取得客户请求Socket
                    //用取得的Socket构造输入输出流
                    PrintStream os = new PrintStream(new
                    BufferedOutputStream(clientSocket.getOutputStream(),
                    1024), false);
                    BufferedReader is = new BufferedReader(new
                    InputStreamReader(clientSocket.getInputStream()));
                    //创建客户会话线程
                    cs = new ClientSession(this.frm);
                    cs.is = is;
                    cs.os = os;
                    cs.clientSock = clientSocket;
                    cs.start();
                    } catch (Exception e_socket) {
                    frm.statusBar.setText(e_socket.getMessage());
                }
            }
        }
    }

    监听线程一直在后台运行。当有客户请求到来时,监听线程创建与该客户进行会话的ClientSession实例。这时,监听线程会等待另外客户请求的到来,然后又创建会话线程,如此循环下去。客户会话则通过会话线程进行。客户会话线程主要的工作就是怎样处理客户请求。ClientSession. parseRequest ()就是处理客户请求的。这个方法的内容应该根据实际应用的需要来确定。这里只实现了一些很简单的功能。如会议大厅发言,私下交谈等。ClientSession的实现代码见程序清单1。3。1。

    程序清单1。3。1

    public class ClientSession extends Thread {
        FrmServer frm; //GUI窗口类
        BufferedReader is = null; //输入流
        PrintStream os = null; //输出流
        Socket clientSock = null; //客户请求Socket
        int curContent = 0;
        public ClientSession (FrmServer frm) {
          this.frm=frm;
        }
        public void run () {
            parseRequest(); //处理客户请求
        }
        public void destroy () {
            try {
                clientSock.close();
                is.close();
                os.close();
            } catch (Exception cs_e) {}
        }
        //客户请求处理方法,只实现了大厅发言及私下交谈
        private void parseRequest () {
            String strTalking;
            String strUserID;
            String strMain = "";
            String strPerson = NetUtil.PERSONINFO + "\n";
            boolean flagEndProc = false;
            while (true) {
                try {
                    while ((strTalking = new String(is.readLine())) != null) {
                        if(strTalking.equals(NetUtil.CHATSTART)){ //客户会话开始
                            strUserID = new String(is.readLine());
                            // 将所有谈话内容发送给刚登录的客户
                            for (int i = 0; i < frm.dataProcessor.vecMainTalk.size(); i++) {
                                strMain += (String)frm.dataProcessor.vecMainTalk.get(i);
                                strMain += "\n";
                            }
                            curContent = frm.dataProcessor.vecMainTalk.size();
                            os.println(strMain);
                            os.flush();
                            for (int j = 0; j < frm.dataProcessor.vecP2PInfo.size(); j++) {
                                strPerson += (String)frm.dataProcessor.vecP2PInfo.get(j);
                                strPerson += "\n";
                            }
                            os.println(strPerson);
    os.flush(); //将所有在线用户名单发给新加入的客户
    os.println(NetUtil.DIVIDEDLINE);
    os.flush();
    while (true) {
    this.sleep(1000);
    String strContent = "";
    //如果有人发言,则把发言发给在线客户
    if (frm.dataProcessor.vecMainTalk.size() > curContent) {
    for (int ci = curContent; ci <frm.dataProcessor.vecMainTalk.size(); ci++) {
    strContent +=(String)frm.dataProcessor.vecMainTalk.get(ci);
    strContent += "\n";
    }
    curContent = frm.dataProcessor.vecMainTalk.size();
    os.println(strContent);
    os.flush();
    }
    //如果有人私下交谈,则把交谈的内容发给交谈的另一方
    if (strUserID != null) {
    int nvi = 0;
    for (nvi = 0; nvi <
    frm.dataProcessor.vecSelfInfo.size()&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get
    (nvi)).get(0)).equals(strUserID); nvi++);
    if (nvi < frm.dataProcessor.vecSelfInfo.size()) {
    Vector vecTalk =(Vector)frm.dataProcessor.vecSelfInfo.get(nvi);
    if ((String)vecTalk.get(1)).equals(NetUtil.CALLED)){
    String strCallRes = NetUtil.ISCALLED+ "\n";
    String strCallTemp = (String)vecTalk.get(2);
    strCallRes += strCallTemp;
    os.println(strCallRes);
    os.flush();
    }
    else if(((String)vecTalk.get(1)).equals(NetUtil.CALLING)){
    if(((String)vecTalk.get(3)).equals(NetUtil.RESPONSING)){ //一客户呼叫另一客户并有了回应
    String strResponsing =NetUtil.RESPONSE + "\n";
    String strResponsingT =(String)vecTalk.get(2);
    strResponsing += strResponsingT;
    os.println(strResponsing);
    os.flush();
    //设置客户“正在私下交谈”状态
    vecTalk.setElementAt(NetUtil.CHATTING, 1);
    String strOther = (String)vecTalk.get(2);
    int setvi = 0;
    for (setvi = 0; setvi <frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(setvi)).get(0)).equals(strOther); setvi++);
    Vector vecOther = (Vector)frm.dataProcessor.vecSelfInfo.get(setvi);
    vecOther.setElementAt(NetUtil.CHATTING,1);
    }}
    else if (((String)vecTalk.get(1)).equals(NetUtil.CHATTING)) {
    String strCurContent = (String)vecTalk.get(4);
    if (!strCurContent.equals(NetUtil.NONCONTENT)) {
    String strToWho = vecTalk.get(2)+ "\n";
    String strPerRes = NetUtil.PERSONALRECEIVE+ "\n";
    strPerRes += strToWho;
    strPerRes += strCurContent;
    os.println(strPerRes);
    os.flush();
    vecTalk.setElementAt(NetUtil.NONCONTENT,4);
    }}}}}}
    // 处理客户发来与另一客户私下交谈的请求
    else if (strTalking.equals(NetUtil.PERSONALTALK)) {
    strTalking = new String(is.readLine());
    int vi = 0;
    for (vi = 0; vi < frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi)).get(0)).equals(strTalking); vi++);
    if (vi == frm.dataProcessor.vecSelfInfo.size()) {
    os.println(NetUtil.NOTEXIST);
    os.flush();
    }
    else {
    Vector vec = (Vector)frm.dataProcessor.vecSelfInfo.get(vi);
    String strCall = new String(is.readLine());
    vec.setElementAt(NetUtil.CALLED, 1);
    vec.setElementAt(strCall, 2);
    int vi_c = 0;
    for (vi_c = 0; vi_c < frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi_c)).get(0)).equals(strCall); vi_c++);
    if (vi_c == frm.dataProcessor.vecSelfInfo.size()) {
    os.println(NetUtil.NOTEXIST);
    os.flush();
    }
    Vector vec_c = (Vector)frm.dataProcessor.vecSelfInfo.get(vi_c);
    vec_c.setElementAt(NetUtil.CALLING, 1);
    vec_c.setElementAt(strTalking, 2);
    os.println(NetUtil.RESPONSE);
    os.flush();
    }
    flagEndProc = true;
    }
    // 存储新客户的信息
    else if (strTalking.equals(NetUtil.PERSONNAME)) {
    String strName = "";
    frm.isStart = true;
    strName = new String(is.readLine());
    if (strName != "\n") {
    frm.dataProcessor.vecP2PInfo.addElement(strName);
    Vector vec = new Vector();
    vec.addElement(strName);
    vec.addElement(NetUtil.IDLE);
    vec.addElement(NetUtil.NONE);
    vec.addElement(NetUtil.NORESPONSE);
    vec.addElement(NetUtil.NONCONTENT);
    frm.dataProcessor.vecSelfInfo.addElement(vec);
    }
    flagEndProc = true;
    }
    //私下交谈时,处理被叫方发送的应答请求
    else if (strTalking.equals(NetUtil.SETRESPONSE)) {
    String strResName = new String(is.readLine());
    int res = 0;
    for (res = 0; res < frm.dataProcessor.vecSelfInfo.size()
    &&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(res)).get(0)).equals(strResName); res++);
    Vector vecRes = (Vector)frm.dataProcessor.vecSelfInfo.get(res);
    vecRes.setElementAt(NetUtil.RESPONSING, 3);
    }
    //私下交谈时,处理主叫方发送的“开始交谈“请求
    else if (strTalking.equals(NetUtil.PERSONALTALKSTART)) {
    String strPerCallName = new String(is.readLine());
    String strPerTalkContent = new String(is.readLine());
    int pres = 0;
    for (pres = 0; pres < frm.dataProcessor.vecSelfInfo.size()
    &&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(pres)).get(0)).equals(strPerCallName);
    pres++);
    Vector vecPer = (Vector)frm.dataProcessor.vecSelfInfo.get(pres);
    vecPer.setElementAt(strPerTalkContent, 4);
    }
    else {
    if (!strTalking.equals("\n") && !strTalking.equals("")) {
    frm.dataProcessor.vecMainTalk.addElement(strTalking);
    strTalking += "\n";
    }
    flagEndProc = true;
    }}
    } catch (Exception io_e) {
    frm.statusBar.setText(io_e.getMessage());
    }
    if (flagEndProc)
    break;
    }}}

    以上程序实现了对客户请求的处理。客户登录后,就可以发言了(简化了客户身份验证)。客户可以在大厅发言(每位在线客户都能接收到该发言),也可以选择某一位客户进行私下交谈(只有被选择的客户能收到该信息)。限于篇幅的原因,只实现了这几个简单功能。其它功能可以参考着实现。

    客户信息,客户谈话内容等公共数据存放在CmDataProcessor的实例中。该实例是GUI窗口类的一个成员变量,在初始化时创建。CmDataProcessor中简化了对公共数据的处理,没有与后方的DBMS交互。因为本文重点是即时通信,所以没有实现与数据库交互。CmDataProcessor类中有三个主要成员变量.vecMainTalk用来存放客户的大厅谈话内容,vecSelfInfo存放每个在线客户的状态信息,vecP2Pinfo存放在线客户列表。需要说明的是,客户会话(ClientSession)这个类的parseRequest ()方法的实现细节不是本文的重点,因为其实现是根据应用的不同而不同的,尽管程序清单中给出了比较详细的注释。以下是类CmDataProcessor的实现代码(程序清单1。4。1):

    程序清单1。4。1

    public class CmDataProcessor {
      Vector vecMainTalk = new Vector();
      Vector vecSelfInfo = new Vector();
      Vector vecP2PInfo = new Vector();
      public CmDataProcessor() {
        vecMainTalk.addElement(NetUtil.WELCOME); //登录时的欢迎界面
      }
    }

    接口NetUtil和GUI用到的类的实现这里从略。到此,服务器端程序已实现。下面是客户端程序的实现。

    客户端程序的实现:客户端程序首先实现的是类Reciever。代码如下(程序清单2。1。1):

    程序清单2。1。1

    class Receiver extends Thread {
    PrintStream os;
    BufferedReader is;
    Socket clientSocket = null;
    public Receiver (Socket socket) {
    clientSocket = socket; //Socket实例是在applet初始化时创建的,而不能在线程内创建
    }
    public void run () {
    if (clientSocket == null) {
    return;
    }
    try {
    os = new PrintStream(clientSocket.getOutputStream());
    is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    } catch (Exception se) {
    String err = se.getMessage() + "\n";
    }
    String strTalking;
    String strStart = "";
    strStart = NetUtil.CHATSTART + "\n";
    strStart += strUserID;
    os.println(strStart);
    os.flush();
    while (true) {
    try {
    while ((strTalking = new String(is.readLine())) != null) {
    if (strTalking.equals(NetUtil.PERSONINFO)) { //显示在线客户信息
    while ((strTalking = new String(is.readLine()))!= null) {
    if (strTalking.equals(NetUtil.DIVIDEDLINE)) {
    break;
    }
    if (!strTalking.equals("\n") && !strTalking.equals("")) {
    addUser(strTalking);
    }}}
    else if (strTalking.equals(NetUtil.ISCALLED)) {
    String strCallName = (String)is.readLine();
    strCallingName = strCallName;
    textNotify.setEnabled(true);
    textNotify.setText(strCallName + " is calling you!");
    }
    else if (strTalking.equals(NetUtil.RESPONSE)) {
    String strResponseName = (String)is.readLine();
    textNotify.setText("You are chatting with " + strResponseName);
    strCallingName = strResponseName;
    isTalkWith = true;
    choiceUser.addItem(strResponseName);
    }
    else if (strTalking.equals(NetUtil.PERSONALRECEIVE)) {
    String strWhoIs = (String)is.readLine();
    String strContent = (String)is.readLine();
    textTalkContent.append(strWhoIs + " speak to you: "+ strContent + "\n");
    }
    else {
    strTalking += "\n";
    textTalkContent.append(strTalking);
    }}
    } catch (Exception io_e) {
    System.out.println(io_e.getMessage());
    }}}}

    该类用来实现与服务器端数据同步。当有客户发言或者有客户和另一客户私下交谈时,该类将立即更新这些数据,在客户端显示出来。所以,该类在客户会话期间一直在后台运行。负责发送客户请求的类是RequestSender。RequestSender用成员方法chatAll ()和chatOne ()分别实现大厅发言和私下交谈,其实现可以参照以上程序。客户端applet的实现代码这里从略。至此,客户端程序也已完成。以下是客户端程序运行时的快照(图2。4。1和图2。4。2)。

    图2。4。1
    图2。4。1

    图2。4。2
    图2。4。2

    5.系统扩展及补充说明
    以上实例由于篇幅原因只实现很少一部分功能,但能体现该即时通信系统的总体设计思想,而且很容易实现功能扩展。例如,可以实现公共数据处理类(CmDataProcessor)与DBMS的交互,实现数据的持久化(persistence);可以对applet实现数字签名来加大它对客户机的访问权限,从而实现文件的传输,实现多媒体交互;可以实现applet与servlet的对话,将比较复杂的业务逻辑交给应用服务器中的EJB来处理等。由于本人水平有限,难免有错误之处,欢迎批评指正。

    关于作者
    杨健,中南工业大学计算机科学与技术专业硕士研究生,参与过大型MIS系统开发,网站建设等,现在正致力于Java技术的研究。Email: ytjcopy@263.net


    服务器端的管理工具,如对数据进行统计,紧急情况的处理等。

    服务器端类的设计(图2。3。2和图2。3。3):

    公共数据处理类CmDataProcessor(Common Data Processor):该类包含客户所共有的数据,以及如何对这些数据进行处理。

    端口监听类PortListener(Port Listener):该类实现了java. lang. Runnable接口,从服务器程序初始化完成后一直运行。由于目前JDK只支持同步通信,在没有客户请求时,该线程处于等待状态;一旦有客户请求到来,便继续执行。这时服务器程序可以通过java. net. ServerSocket. accept()方法获得客户端请求的java. net. Socket对象。然后用这个Socket对象为参数构造一个新的线程:ClientSession的实例(类ClientSession以下将作介绍)。然后在ClientSession实例中用该Socket对象构造一个输出流java. io. PrintStream和一个输入流java. io. BufferedReader,以后,每个客户就可以通过这一对输入输出流与服务器交互了。应该注意的是,ServerSocket 对象并不是在该对象内创建的,而是在服务器程序初始化时创建的。因为socket是进程间的通信,在线程中创建将会失败。客户端程序也是如此。

    客户会话类ClientSession(Client Session):该类继承自java. lang. Thread类,由PortListener创建。一般的,每一个在线客户都对应一个ClientSession的实例。该类用parseRequest()方法解析客户发来的请求,进行相应处理。该线程在客户会话期间一直运行,通过I/O流读取和发送数据(I/O流即从PortListener监听线程获得的java. net. Socket对象而创建的java. io. PrintStream和java. io. BufferedReader实例),直到客户退出才撤销。该类和类ClientSession一样都实现了java. lang. Runnable接口,故都有一个run()方法。该方法的结束标志着该线程将结束。

    服务器管理类 ServerManager(Server Manager):管理服务器。拥有管理权限的客户(管理员)可以远程操作服务器程序,包括运行、停止服务器,广播通知,给指定客户发送消息等特权操作。

    图2。3。2
    图2。3。2

    图2。3。3

  3. 服务器端类的设计(图2。3。2和图2。3。3):

    公共数据处理类CmDataProcessor(Common Data Processor):该类包含客户所共有的数据,以及如何对这些数据进行处理。

    端口监听类PortListener(Port Listener):该类实现了java. lang. Runnable接口,从服务器程序初始化完成后一直运行。由于目前JDK只支持同步通信,在没有客户请求时,该线程处于等待状态;一旦有客户请求到来,便继续执行。这时服务器程序可以通过java. net. ServerSocket. accept()方法获得客户端请求的java. net. Socket对象。然后用这个Socket对象为参数构造一个新的线程:ClientSession的实例(类ClientSession以下将作介绍)。然后在ClientSession实例中用该Socket对象构造一个输出流java. io. PrintStream和一个输入流java. io. BufferedReader,以后,每个客户就可以通过这一对输入输出流与服务器交互了。应该注意的是,ServerSocket 对象并不是在该对象内创建的,而是在服务器程序初始化时创建的。因为socket是进程间的通信,在线程中创建将会失败。客户端程序也是如此。

    客户会话类ClientSession(Client Session):该类继承自java. lang. Thread类,由PortListener创建。一般的,每一个在线客户都对应一个ClientSession的实例。该类用parseRequest()方法解析客户发来的请求,进行相应处理。该线程在客户会话期间一直运行,通过I/O流读取和发送数据(I/O流即从PortListener监听线程获得的java. net. Socket对象而创建的java. io. PrintStream和java. io. BufferedReader实例),直到客户退出才撤销。该类和类ClientSession一样都实现了java. lang. Runnable接口,故都有一个run()方法。该方法的结束标志着该线程将结束。

    服务器管理类 ServerManager(Server Manager):管理服务器。拥有管理权限的客户(管理员)可以远程操作服务器程序,包括运行、停止服务器,广播通知,给指定客户发送消息等特权操作。

    图2。3。2
    图2。3。2

    图2。3。3

  4. 客户端程序设计(图2。3。4)

    客户端完成的功能是:建立与服务器的连接;向服务器发送功能请求,接收来自服务器的信息,完成与主机或其他客户交互;断开与服务器的连接。客户端程序相对服务器端程序来说属于LightWeight(轻量级)。这是由本系统的自身特点决定的。所以,对客户端程序抽象如下:

    1. 客户请求发送器:负责功能请求的发送。如登录请求等。
    2. 服务器信息接收器:负责接收来自服务器端的信息。如请求处理结果等。


    客户端类的设计:
    请求发送器(RequestSender):该类发送客户端的功能请求。客户通过客户端用户界面提交要执行的操作,然后由该类将客户提交的信息封装成服务器端程序可以理解的功能请求发送出去。

    信息接收器(Receiver):该类接收来服务器端的信息。这些信息可以是客户请求的处理结果,也可以是服务器端的广播通知。为保证实时性,该类实现了java. lang. Runnable接口。在客户会话期间,该类将一直运行,实时的将来自服务器端的信息反馈给客户。该类接收信息后,应该对该信息做相应处理。如通知客户已登录成功等。这些操作都将在run()方法中实现。

    图2。3。4
    图2。3。4

    4. 实现
    以上的系统设计是一个即时通信系统的总体框架,根据实际情况,可以添加或者修改。下文就以“远程会议系统“为例来实例化这样一个通信系统。

    我们知道,远程会议系统有几个方面的特点:实时交互;准确传输信息;多客户等。所以,完全可以用该系统框架来实现(这里只给出核心代码)。

    首先我们来实现服务器端程序。为了便于对服务器程序的管理,服务器端程序采用了GUI界面。在该程序初始化时应该实现对可用端口的监听(程序清单1。1)。

    程序清单1。1。1

        try {
              socket = new ServerSocket(NetUtil.SLISTENPORT);
            }
    catch (Exception se)
        {
                statusBar.setText(se.getMessage());
            }

    其中,NetUtil.SLISTENPORT是服务器的一个可用端口,可以根据实际情况确定。NetUtil是一个接口,其中包含了该系统用到的各类常数。NetUtil.SLISTENPORT就是NetUtil中的一个整型常数。如果端口监听抛出异常,GUI中的statusBar将给出提示。监听成功后可以启动监听线程了(程序清单1。1。2)。

    程序清单1。1。2

    listenThread=new Thread (new ListenThread ());
    listenThread.start();
    

    以上程序中listenThread是一个Thread实例,用来操作监听线程。监听线程实现如下(程序清单1。2。1):

    程序清单1。2。1

       public class PortListener implements Runnable{
        ServerSocket socket; //服务器监听端口
        FrmServer frm; //FrmServer为GUI窗口类
        ClientSession cs; //客户端会话线程
        PortListener (FrmServer frm,ServerSocket socket) {
          this.frm=frm;
          this.socket=socket;
        }
          public void run () {
            int count = 0;
            if (socket == null) {
                frm.statusBar.setText("socket failed!");
                return;
            }
            while (true) {
                try {
                    Socket clientSocket;
                    clientSocket = socket.accept(); //取得客户请求Socket
                    //用取得的Socket构造输入输出流
                    PrintStream os = new PrintStream(new
                    BufferedOutputStream(clientSocket.getOutputStream(),
                    1024), false);
                    BufferedReader is = new BufferedReader(new
                    InputStreamReader(clientSocket.getInputStream()));
                    //创建客户会话线程
                    cs = new ClientSession(this.frm);
                    cs.is = is;
                    cs.os = os;
                    cs.clientSock = clientSocket;
                    cs.start();
                    } catch (Exception e_socket) {
                    frm.statusBar.setText(e_socket.getMessage());
                }
            }
        }
    }

    监听线程一直在后台运行。当有客户请求到来时,监听线程创建与该客户进行会话的ClientSession实例。这时,监听线程会等待另外客户请求的到来,然后又创建会话线程,如此循环下去。客户会话则通过会话线程进行。客户会话线程主要的工作就是怎样处理客户请求。ClientSession. parseRequest ()就是处理客户请求的。这个方法的内容应该根据实际应用的需要来确定。这里只实现了一些很简单的功能。如会议大厅发言,私下交谈等。ClientSession的实现代码见程序清单1。3。1。

    程序清单1。3。1

    public class ClientSession extends Thread {
        FrmServer frm; //GUI窗口类
        BufferedReader is = null; //输入流
        PrintStream os = null; //输出流
        Socket clientSock = null; //客户请求Socket
        int curContent = 0;
        public ClientSession (FrmServer frm) {
          this.frm=frm;
        }
        public void run () {
            parseRequest(); //处理客户请求
        }
        public void destroy () {
            try {
                clientSock.close();
                is.close();
                os.close();
            } catch (Exception cs_e) {}
        }
        //客户请求处理方法,只实现了大厅发言及私下交谈
        private void parseRequest () {
            String strTalking;
            String strUserID;
            String strMain = "";
            String strPerson = NetUtil.PERSONINFO + "\n";
            boolean flagEndProc = false;
            while (true) {
                try {
                    while ((strTalking = new String(is.readLine())) != null) {
                        if(strTalking.equals(NetUtil.CHATSTART)){ //客户会话开始
                            strUserID = new String(is.readLine());
                            // 将所有谈话内容发送给刚登录的客户
                            for (int i = 0; i < frm.dataProcessor.vecMainTalk.size(); i++) {
                                strMain += (String)frm.dataProcessor.vecMainTalk.get(i);
                                strMain += "\n";
                            }
                            curContent = frm.dataProcessor.vecMainTalk.size();
                            os.println(strMain);
                            os.flush();
                            for (int j = 0; j < frm.dataProcessor.vecP2PInfo.size(); j++) {
                                strPerson += (String)frm.dataProcessor.vecP2PInfo.get(j);
                                strPerson += "\n";
                            }
                            os.println(strPerson);
    os.flush(); //将所有在线用户名单发给新加入的客户
    os.println(NetUtil.DIVIDEDLINE);
    os.flush();
    while (true) {
    this.sleep(1000);
    String strContent = "";
    //如果有人发言,则把发言发给在线客户
    if (frm.dataProcessor.vecMainTalk.size() > curContent) {
    for (int ci = curContent; ci <frm.dataProcessor.vecMainTalk.size(); ci++) {
    strContent +=(String)frm.dataProcessor.vecMainTalk.get(ci);
    strContent += "\n";
    }
    curContent = frm.dataProcessor.vecMainTalk.size();
    os.println(strContent);
    os.flush();
    }
    //如果有人私下交谈,则把交谈的内容发给交谈的另一方
    if (strUserID != null) {
    int nvi = 0;
    for (nvi = 0; nvi <
    frm.dataProcessor.vecSelfInfo.size()&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get
    (nvi)).get(0)).equals(strUserID); nvi++);
    if (nvi < frm.dataProcessor.vecSelfInfo.size()) {
    Vector vecTalk =(Vector)frm.dataProcessor.vecSelfInfo.get(nvi);
    if ((String)vecTalk.get(1)).equals(NetUtil.CALLED)){
    String strCallRes = NetUtil.ISCALLED+ "\n";
    String strCallTemp = (String)vecTalk.get(2);
    strCallRes += strCallTemp;
    os.println(strCallRes);
    os.flush();
    }
    else if(((String)vecTalk.get(1)).equals(NetUtil.CALLING)){
    if(((String)vecTalk.get(3)).equals(NetUtil.RESPONSING)){ //一客户呼叫另一客户并有了回应
    String strResponsing =NetUtil.RESPONSE + "\n";
    String strResponsingT =(String)vecTalk.get(2);
    strResponsing += strResponsingT;
    os.println(strResponsing);
    os.flush();
    //设置客户“正在私下交谈”状态
    vecTalk.setElementAt(NetUtil.CHATTING, 1);
    String strOther = (String)vecTalk.get(2);
    int setvi = 0;
    for (setvi = 0; setvi <frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(setvi)).get(0)).equals(strOther); setvi++);
    Vector vecOther = (Vector)frm.dataProcessor.vecSelfInfo.get(setvi);
    vecOther.setElementAt(NetUtil.CHATTING,1);
    }}
    else if (((String)vecTalk.get(1)).equals(NetUtil.CHATTING)) {
    String strCurContent = (String)vecTalk.get(4);
    if (!strCurContent.equals(NetUtil.NONCONTENT)) {
    String strToWho = vecTalk.get(2)+ "\n";
    String strPerRes = NetUtil.PERSONALRECEIVE+ "\n";
    strPerRes += strToWho;
    strPerRes += strCurContent;
    os.println(strPerRes);
    os.flush();
    vecTalk.setElementAt(NetUtil.NONCONTENT,4);
    }}}}}}
    // 处理客户发来与另一客户私下交谈的请求
    else if (strTalking.equals(NetUtil.PERSONALTALK)) {
    strTalking = new String(is.readLine());
    int vi = 0;
    for (vi = 0; vi < frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi)).get(0)).equals(strTalking); vi++);
    if (vi == frm.dataProcessor.vecSelfInfo.size()) {
    os.println(NetUtil.NOTEXIST);
    os.flush();
    }
    else {
    Vector vec = (Vector)frm.dataProcessor.vecSelfInfo.get(vi);
    String strCall = new String(is.readLine());
    vec.setElementAt(NetUtil.CALLED, 1);
    vec.setElementAt(strCall, 2);
    int vi_c = 0;
    for (vi_c = 0; vi_c < frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi_c)).get(0)).equals(strCall); vi_c++);
    if (vi_c == frm.dataProcessor.vecSelfInfo.size()) {
    os.println(NetUtil.NOTEXIST);
    os.flush();
    }
    Vector vec_c = (Vector)frm.dataProcessor.vecSelfInfo.get(vi_c);
    vec_c.setElementAt(NetUtil.CALLING, 1);
    vec_c.setElementAt(strTalking, 2);
    os.println(NetUtil.RESPONSE);
    os.flush();
    }
    flagEndProc = true;
    }
    // 存储新客户的信息
    else if (strTalking.equals(NetUtil.PERSONNAME)) {
    String strName = "";
    frm.isStart = true;
    strName = new String(is.readLine());
    if (strName != "\n") {
    frm.dataProcessor.vecP2PInfo.addElement(strName);
    Vector vec = new Vector();
    vec.addElement(strName);
    vec.addElement(NetUtil.IDLE);
    vec.addElement(NetUtil.NONE);
    vec.addElement(NetUtil.NORESPONSE);
    vec.addElement(NetUtil.NONCONTENT);
    frm.dataProcessor.vecSelfInfo.addElement(vec);
    }
    flagEndProc = true;
    }
    //私下交谈时,处理被叫方发送的应答请求
    else if (strTalking.equals(NetUtil.SETRESPONSE)) {
    String strResName = new String(is.readLine());
    int res = 0;
    for (res = 0; res < frm.dataProcessor.vecSelfInfo.size()
    &&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(res)).get(0)).equals(strResName); res++);
    Vector vecRes = (Vector)frm.dataProcessor.vecSelfInfo.get(res);
    vecRes.setElementAt(NetUtil.RESPONSING, 3);
    }
    //私下交谈时,处理主叫方发送的“开始交谈“请求
    else if (strTalking.equals(NetUtil.PERSONALTALKSTART)) {
    String strPerCallName = new String(is.readLine());
    String strPerTalkContent = new String(is.readLine());
    int pres = 0;
    for (pres = 0; pres < frm.dataProcessor.vecSelfInfo.size()
    &&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(pres)).get(0)).equals(strPerCallName);
    pres++);
    Vector vecPer = (Vector)frm.dataProcessor.vecSelfInfo.get(pres);
    vecPer.setElementAt(strPerTalkContent, 4);
    }
    else {
    if (!strTalking.equals("\n") && !strTalking.equals("")) {
    frm.dataProcessor.vecMainTalk.addElement(strTalking);
    strTalking += "\n";
    }
    flagEndProc = true;
    }}
    } catch (Exception io_e) {
    frm.statusBar.setText(io_e.getMessage());
    }
    if (flagEndProc)
    break;
    }}}

    以上程序实现了对客户请求的处理。客户登录后,就可以发言了(简化了客户身份验证)。客户可以在大厅发言(每位在线客户都能接收到该发言),也可以选择某一位客户进行私下交谈(只有被选择的客户能收到该信息)。限于篇幅的原因,只实现了这几个简单功能。其它功能可以参考着实现。

    客户信息,客户谈话内容等公共数据存放在CmDataProcessor的实例中。该实例是GUI窗口类的一个成员变量,在初始化时创建。CmDataProcessor中简化了对公共数据的处理,没有与后方的DBMS交互。因为本文重点是即时通信,所以没有实现与数据库交互。CmDataProcessor类中有三个主要成员变量.vecMainTalk用来存放客户的大厅谈话内容,vecSelfInfo存放每个在线客户的状态信息,vecP2Pinfo存放在线客户列表。需要说明的是,客户会话(ClientSession)这个类的parseRequest ()方法的实现细节不是本文的重点,因为其实现是根据应用的不同而不同的,尽管程序清单中给出了比较详细的注释。以下是类CmDataProcessor的实现代码(程序清单1。4。1):

    程序清单1。4。1

    public class CmDataProcessor {
      Vector vecMainTalk = new Vector();
      Vector vecSelfInfo = new Vector();
      Vector vecP2PInfo = new Vector();
      public CmDataProcessor() {
        vecMainTalk.addElement(NetUtil.WELCOME); //登录时的欢迎界面
      }
    }

    接口NetUtil和GUI用到的类的实现这里从略。到此,服务器端程序已实现。下面是客户端程序的实现。

    客户端程序的实现:客户端程序首先实现的是类Reciever。代码如下(程序清单2。1。1):

    程序清单2。1。1

    class Receiver extends Thread {
    PrintStream os;
    BufferedReader is;
    Socket clientSocket = null;
    public Receiver (Socket socket) {
    clientSocket = socket; //Socket实例是在applet初始化时创建的,而不能在线程内创建
    }
    public void run () {
    if (clientSocket == null) {
    return;
    }
    try {
    os = new PrintStream(clientSocket.getOutputStream());
    is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    } catch (Exception se) {
    String err = se.getMessage() + "\n";
    }
    String strTalking;
    String strStart = "";
    strStart = NetUtil.CHATSTART + "\n";
    strStart += strUserID;
    os.println(strStart);
    os.flush();
    while (true) {
    try {
    while ((strTalking = new String(is.readLine())) != null) {
    if (strTalking.equals(NetUtil.PERSONINFO)) { //显示在线客户信息
    while ((strTalking = new String(is.readLine()))!= null) {
    if (strTalking.equals(NetUtil.DIVIDEDLINE)) {
    break;
    }
    if (!strTalking.equals("\n") && !strTalking.equals("")) {
    addUser(strTalking);
    }}}
    else if (strTalking.equals(NetUtil.ISCALLED)) {
    String strCallName = (String)is.readLine();
    strCallingName = strCallName;
    textNotify.setEnabled(true);
    textNotify.setText(strCallName + " is calling you!");
    }
    else if (strTalking.equals(NetUtil.RESPONSE)) {
    String strResponseName = (String)is.readLine();
    textNotify.setText("You are chatting with " + strResponseName);
    strCallingName = strResponseName;
    isTalkWith = true;
    choiceUser.addItem(strResponseName);
    }
    else if (strTalking.equals(NetUtil.PERSONALRECEIVE)) {
    String strWhoIs = (String)is.readLine();
    String strContent = (String)is.readLine();
    textTalkContent.append(strWhoIs + " speak to you: "+ strContent + "\n");
    }
    else {
    strTalking += "\n";
    textTalkContent.append(strTalking);
    }}
    } catch (Exception io_e) {
    System.out.println(io_e.getMessage());
    }}}}

    该类用来实现与服务器端数据同步。当有客户发言或者有客户和另一客户私下交谈时,该类将立即更新这些数据,在客户端显示出来。所以,该类在客户会话期间一直在后台运行。负责发送客户请求的类是RequestSender。RequestSender用成员方法chatAll ()和chatOne ()分别实现大厅发言和私下交谈,其实现可以参照以上程序。客户端applet的实现代码这里从略。至此,客户端程序也已完成。以下是客户端程序运行时的快照(图2。4。1和图2。4。2)。

    图2。4。1
    图2。4。1

    图2。4。2
    图2。4。2

    5.系统扩展及补充说明
    以上实例由于篇幅原因只实现很少一部分功能,但能体现该即时通信系统的总体设计思想,而且很容易实现功能扩展。例如,可以实现公共数据处理类(CmDataProcessor)与DBMS的交互,实现数据的持久化(persistence);可以对applet实现数字签名来加大它对客户机的访问权限,从而实现文件的传输,实现多媒体交互;可以实现applet与servlet的对话,将比较复杂的业务逻辑交给应用服务器中的EJB来处理等。由于本人水平有限,难免有错误之处,欢迎批评指正。

    关于作者
    杨健,中南工业大学计算机科学与技术专业硕士研究生,参与过大型MIS系统开发,网站建设等,现在正致力于Java技术的研究。Email: ytjcopy@263.net


    服务器端的管理工具,如对数据进行统计,紧急情况的处理等。

    服务器端类的设计(图2。3。2和图2。3。3):

    公共数据处理类CmDataProcessor(Common Data Processor):该类包含客户所共有的数据,以及如何对这些数据进行处理。

    端口监听类PortListener(Port Listener):该类实现了java. lang. Runnable接口,从服务器程序初始化完成后一直运行。由于目前JDK只支持同步通信,在没有客户请求时,该线程处于等待状态;一旦有客户请求到来,便继续执行。这时服务器程序可以通过java. net. ServerSocket. accept()方法获得客户端请求的java. net. Socket对象。然后用这个Socket对象为参数构造一个新的线程:ClientSession的实例(类ClientSession以下将作介绍)。然后在ClientSession实例中用该Socket对象构造一个输出流java. io. PrintStream和一个输入流java. io. BufferedReader,以后,每个客户就可以通过这一对输入输出流与服务器交互了。应该注意的是,ServerSocket 对象并不是在该对象内创建的,而是在服务器程序初始化时创建的。因为socket是进程间的通信,在线程中创建将会失败。客户端程序也是如此。

    客户会话类ClientSession(Client Session):该类继承自java. lang. Thread类,由PortListener创建。一般的,每一个在线客户都对应一个ClientSession的实例。该类用parseRequest()方法解析客户发来的请求,进行相应处理。该线程在客户会话期间一直运行,通过I/O流读取和发送数据(I/O流即从PortListener监听线程获得的java. net. Socket对象而创建的java. io. PrintStream和java. io. BufferedReader实例),直到客户退出才撤销。该类和类ClientSession一样都实现了java. lang. Runnable接口,故都有一个run()方法。该方法的结束标志着该线程将结束。

    服务器管理类 ServerManager(Server Manager):管理服务器。拥有管理权限的客户(管理员)可以远程操作服务器程序,包括运行、停止服务器,广播通知,给指定客户发送消息等特权操作。

    图2。3。2
    图2。3。2

    图2。3。3

  5. 客户端程序设计(图2。3。4)

    客户端完成的功能是:建立与服务器的连接;向服务器发送功能请求,接收来自服务器的信息,完成与主机或其他客户交互;断开与服务器的连接。客户端程序相对服务器端程序来说属于LightWeight(轻量级)。这是由本系统的自身特点决定的。所以,对客户端程序抽象如下:

    1. 客户请求发送器:负责功能请求的发送。如登录请求等。
    2. 服务器信息接收器:负责接收来自服务器端的信息。如请求处理结果等。


    客户端类的设计:
    请求发送器(RequestSender):该类发送客户端的功能请求。客户通过客户端用户界面提交要执行的操作,然后由该类将客户提交的信息封装成服务器端程序可以理解的功能请求发送出去。

    信息接收器(Receiver):该类接收来服务器端的信息。这些信息可以是客户请求的处理结果,也可以是服务器端的广播通知。为保证实时性,该类实现了java. lang. Runnable接口。在客户会话期间,该类将一直运行,实时的将来自服务器端的信息反馈给客户。该类接收信息后,应该对该信息做相应处理。如通知客户已登录成功等。这些操作都将在run()方法中实现。

    图2。3。4
    图2。3。4

4. 实现
以上的系统设计是一个即时通信系统的总体框架,根据实际情况,可以添加或者修改。下文就以“远程会议系统“为例来实例化这样一个通信系统。

我们知道,远程会议系统有几个方面的特点:实时交互;准确传输信息;多客户等。所以,完全可以用该系统框架来实现(这里只给出核心代码)。

首先我们来实现服务器端程序。为了便于对服务器程序的管理,服务器端程序采用了GUI界面。在该程序初始化时应该实现对可用端口的监听(程序清单1。1)。

程序清单1。1。1

    try {
          socket = new ServerSocket(NetUtil.SLISTENPORT);
        }
catch (Exception se)
    {
            statusBar.setText(se.getMessage());
        }

其中,NetUtil.SLISTENPORT是服务器的一个可用端口,可以根据实际情况确定。NetUtil是一个接口,其中包含了该系统用到的各类常数。NetUtil.SLISTENPORT就是NetUtil中的一个整型常数。如果端口监听抛出异常,GUI中的statusBar将给出提示。监听成功后可以启动监听线程了(程序清单1。1。2)。

程序清单1。1。2

listenThread=new Thread (new ListenThread ());
listenThread.start();

以上程序中listenThread是一个Thread实例,用来操作监听线程。监听线程实现如下(程序清单1。2。1):

程序清单1。2。1

   public class PortListener implements Runnable{
    ServerSocket socket; //服务器监听端口
    FrmServer frm; //FrmServer为GUI窗口类
    ClientSession cs; //客户端会话线程
    PortListener (FrmServer frm,ServerSocket socket) {
      this.frm=frm;
      this.socket=socket;
    }
      public void run () {
        int count = 0;
        if (socket == null) {
            frm.statusBar.setText("socket failed!");
            return;
        }
        while (true) {
            try {
                Socket clientSocket;
                clientSocket = socket.accept(); //取得客户请求Socket
                //用取得的Socket构造输入输出流
                PrintStream os = new PrintStream(new
                BufferedOutputStream(clientSocket.getOutputStream(),
                1024), false);
                BufferedReader is = new BufferedReader(new
                InputStreamReader(clientSocket.getInputStream()));
                //创建客户会话线程
                cs = new ClientSession(this.frm);
                cs.is = is;
                cs.os = os;
                cs.clientSock = clientSocket;
                cs.start();
                } catch (Exception e_socket) {
                frm.statusBar.setText(e_socket.getMessage());
            }
        }
    }
}

监听线程一直在后台运行。当有客户请求到来时,监听线程创建与该客户进行会话的ClientSession实例。这时,监听线程会等待另外客户请求的到来,然后又创建会话线程,如此循环下去。客户会话则通过会话线程进行。客户会话线程主要的工作就是怎样处理客户请求。ClientSession. parseRequest ()就是处理客户请求的。这个方法的内容应该根据实际应用的需要来确定。这里只实现了一些很简单的功能。如会议大厅发言,私下交谈等。ClientSession的实现代码见程序清单1。3。1。

程序清单1。3。1

public class ClientSession extends Thread {
    FrmServer frm; //GUI窗口类
    BufferedReader is = null; //输入流
    PrintStream os = null; //输出流
    Socket clientSock = null; //客户请求Socket
    int curContent = 0;
    public ClientSession (FrmServer frm) {
      this.frm=frm;
    }
    public void run () {
        parseRequest(); //处理客户请求
    }
    public void destroy () {
        try {
            clientSock.close();
            is.close();
            os.close();
        } catch (Exception cs_e) {}
    }
    //客户请求处理方法,只实现了大厅发言及私下交谈
    private void parseRequest () {
        String strTalking;
        String strUserID;
        String strMain = "";
        String strPerson = NetUtil.PERSONINFO + "\n";
        boolean flagEndProc = false;
        while (true) {
            try {
                while ((strTalking = new String(is.readLine())) != null) {
                    if(strTalking.equals(NetUtil.CHATSTART)){ //客户会话开始
                        strUserID = new String(is.readLine());
                        // 将所有谈话内容发送给刚登录的客户
                        for (int i = 0; i < frm.dataProcessor.vecMainTalk.size(); i++) {
                            strMain += (String)frm.dataProcessor.vecMainTalk.get(i);
                            strMain += "\n";
                        }
                        curContent = frm.dataProcessor.vecMainTalk.size();
                        os.println(strMain);
                        os.flush();
                        for (int j = 0; j < frm.dataProcessor.vecP2PInfo.size(); j++) {
                            strPerson += (String)frm.dataProcessor.vecP2PInfo.get(j);
                            strPerson += "\n";
                        }
                        os.println(strPerson);
os.flush(); //将所有在线用户名单发给新加入的客户
os.println(NetUtil.DIVIDEDLINE);
os.flush();
while (true) {
this.sleep(1000);
String strContent = "";
//如果有人发言,则把发言发给在线客户
if (frm.dataProcessor.vecMainTalk.size() > curContent) {
for (int ci = curContent; ci <frm.dataProcessor.vecMainTalk.size(); ci++) {
strContent +=(String)frm.dataProcessor.vecMainTalk.get(ci);
strContent += "\n";
}
curContent = frm.dataProcessor.vecMainTalk.size();
os.println(strContent);
os.flush();
}
//如果有人私下交谈,则把交谈的内容发给交谈的另一方
if (strUserID != null) {
int nvi = 0;
for (nvi = 0; nvi <
frm.dataProcessor.vecSelfInfo.size()&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get
(nvi)).get(0)).equals(strUserID); nvi++);
if (nvi < frm.dataProcessor.vecSelfInfo.size()) {
Vector vecTalk =(Vector)frm.dataProcessor.vecSelfInfo.get(nvi);
if ((String)vecTalk.get(1)).equals(NetUtil.CALLED)){
String strCallRes = NetUtil.ISCALLED+ "\n";
String strCallTemp = (String)vecTalk.get(2);
strCallRes += strCallTemp;
os.println(strCallRes);
os.flush();
}
else if(((String)vecTalk.get(1)).equals(NetUtil.CALLING)){
if(((String)vecTalk.get(3)).equals(NetUtil.RESPONSING)){ //一客户呼叫另一客户并有了回应
String strResponsing =NetUtil.RESPONSE + "\n";
String strResponsingT =(String)vecTalk.get(2);
strResponsing += strResponsingT;
os.println(strResponsing);
os.flush();
//设置客户“正在私下交谈”状态
vecTalk.setElementAt(NetUtil.CHATTING, 1);
String strOther = (String)vecTalk.get(2);
int setvi = 0;
for (setvi = 0; setvi <frm.dataProcessor.vecSelfInfo.size()
&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(setvi)).get(0)).equals(strOther); setvi++);
Vector vecOther = (Vector)frm.dataProcessor.vecSelfInfo.get(setvi);
vecOther.setElementAt(NetUtil.CHATTING,1);
}}
else if (((String)vecTalk.get(1)).equals(NetUtil.CHATTING)) {
String strCurContent = (String)vecTalk.get(4);
if (!strCurContent.equals(NetUtil.NONCONTENT)) {
String strToWho = vecTalk.get(2)+ "\n";
String strPerRes = NetUtil.PERSONALRECEIVE+ "\n";
strPerRes += strToWho;
strPerRes += strCurContent;
os.println(strPerRes);
os.flush();
vecTalk.setElementAt(NetUtil.NONCONTENT,4);
}}}}}}
// 处理客户发来与另一客户私下交谈的请求
else if (strTalking.equals(NetUtil.PERSONALTALK)) {
strTalking = new String(is.readLine());
int vi = 0;
for (vi = 0; vi < frm.dataProcessor.vecSelfInfo.size()
&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi)).get(0)).equals(strTalking); vi++);
if (vi == frm.dataProcessor.vecSelfInfo.size()) {
os.println(NetUtil.NOTEXIST);
os.flush();
}
else {
Vector vec = (Vector)frm.dataProcessor.vecSelfInfo.get(vi);
String strCall = new String(is.readLine());
vec.setElementAt(NetUtil.CALLED, 1);
vec.setElementAt(strCall, 2);
int vi_c = 0;
for (vi_c = 0; vi_c < frm.dataProcessor.vecSelfInfo.size()
&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi_c)).get(0)).equals(strCall); vi_c++);
if (vi_c == frm.dataProcessor.vecSelfInfo.size()) {
os.println(NetUtil.NOTEXIST);
os.flush();
}
Vector vec_c = (Vector)frm.dataProcessor.vecSelfInfo.get(vi_c);
vec_c.setElementAt(NetUtil.CALLING, 1);
vec_c.setElementAt(strTalking, 2);
os.println(NetUtil.RESPONSE);
os.flush();
}
flagEndProc = true;
}
// 存储新客户的信息
else if (strTalking.equals(NetUtil.PERSONNAME)) {
String strName = "";
frm.isStart = true;
strName = new String(is.readLine());
if (strName != "\n") {
frm.dataProcessor.vecP2PInfo.addElement(strName);
Vector vec = new Vector();
vec.addElement(strName);
vec.addElement(NetUtil.IDLE);
vec.addElement(NetUtil.NONE);
vec.addElement(NetUtil.NORESPONSE);
vec.addElement(NetUtil.NONCONTENT);
frm.dataProcessor.vecSelfInfo.addElement(vec);
}
flagEndProc = true;
}
//私下交谈时,处理被叫方发送的应答请求
else if (strTalking.equals(NetUtil.SETRESPONSE)) {
String strResName = new String(is.readLine());
int res = 0;
for (res = 0; res < frm.dataProcessor.vecSelfInfo.size()
&&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(res)).get(0)).equals(strResName); res++);
Vector vecRes = (Vector)frm.dataProcessor.vecSelfInfo.get(res);
vecRes.setElementAt(NetUtil.RESPONSING, 3);
}
//私下交谈时,处理主叫方发送的“开始交谈“请求
else if (strTalking.equals(NetUtil.PERSONALTALKSTART)) {
String strPerCallName = new String(is.readLine());
String strPerTalkContent = new String(is.readLine());
int pres = 0;
for (pres = 0; pres < frm.dataProcessor.vecSelfInfo.size()
&&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(pres)).get(0)).equals(strPerCallName);
pres++);
Vector vecPer = (Vector)frm.dataProcessor.vecSelfInfo.get(pres);
vecPer.setElementAt(strPerTalkContent, 4);
}
else {
if (!strTalking.equals("\n") && !strTalking.equals("")) {
frm.dataProcessor.vecMainTalk.addElement(strTalking);
strTalking += "\n";
}
flagEndProc = true;
}}
} catch (Exception io_e) {
frm.statusBar.setText(io_e.getMessage());
}
if (flagEndProc)
break;
}}}

以上程序实现了对客户请求的处理。客户登录后,就可以发言了(简化了客户身份验证)。客户可以在大厅发言(每位在线客户都能接收到该发言),也可以选择某一位客户进行私下交谈(只有被选择的客户能收到该信息)。限于篇幅的原因,只实现了这几个简单功能。其它功能可以参考着实现。

客户信息,客户谈话内容等公共数据存放在CmDataProcessor的实例中。该实例是GUI窗口类的一个成员变量,在初始化时创建。CmDataProcessor中简化了对公共数据的处理,没有与后方的DBMS交互。因为本文重点是即时通信,所以没有实现与数据库交互。CmDataProcessor类中有三个主要成员变量.vecMainTalk用来存放客户的大厅谈话内容,vecSelfInfo存放每个在线客户的状态信息,vecP2Pinfo存放在线客户列表。需要说明的是,客户会话(ClientSession)这个类的parseRequest ()方法的实现细节不是本文的重点,因为其实现是根据应用的不同而不同的,尽管程序清单中给出了比较详细的注释。以下是类CmDataProcessor的实现代码(程序清单1。4。1):

程序清单1。4。1

public class CmDataProcessor {
  Vector vecMainTalk = new Vector();
  Vector vecSelfInfo = new Vector();
  Vector vecP2PInfo = new Vector();
  public CmDataProcessor() {
    vecMainTalk.addElement(NetUtil.WELCOME); //登录时的欢迎界面
  }
}

接口NetUtil和GUI用到的类的实现这里从略。到此,服务器端程序已实现。下面是客户端程序的实现。

客户端程序的实现:客户端程序首先实现的是类Reciever。代码如下(程序清单2。1。1):

程序清单2。1。1

class Receiver extends Thread {
PrintStream os;
BufferedReader is;
Socket clientSocket = null;
public Receiver (Socket socket) {
clientSocket = socket; //Socket实例是在applet初始化时创建的,而不能在线程内创建
}
public void run () {
if (clientSocket == null) {
return;
}
try {
os = new PrintStream(clientSocket.getOutputStream());
is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
} catch (Exception se) {
String err = se.getMessage() + "\n";
}
String strTalking;
String strStart = "";
strStart = NetUtil.CHATSTART + "\n";
strStart += strUserID;
os.println(strStart);
os.flush();
while (true) {
try {
while ((strTalking = new String(is.readLine())) != null) {
if (strTalking.equals(NetUtil.PERSONINFO)) { //显示在线客户信息
while ((strTalking = new String(is.readLine()))!= null) {
if (strTalking.equals(NetUtil.DIVIDEDLINE)) {
break;
}
if (!strTalking.equals("\n") && !strTalking.equals("")) {
addUser(strTalking);
}}}
else if (strTalking.equals(NetUtil.ISCALLED)) {
String strCallName = (String)is.readLine();
strCallingName = strCallName;
textNotify.setEnabled(true);
textNotify.setText(strCallName + " is calling you!");
}
else if (strTalking.equals(NetUtil.RESPONSE)) {
String strResponseName = (String)is.readLine();
textNotify.setText("You are chatting with " + strResponseName);
strCallingName = strResponseName;
isTalkWith = true;
choiceUser.addItem(strResponseName);
}
else if (strTalking.equals(NetUtil.PERSONALRECEIVE)) {
String strWhoIs = (String)is.readLine();
String strContent = (String)is.readLine();
textTalkContent.append(strWhoIs + " speak to you: "+ strContent + "\n");
}
else {
strTalking += "\n";
textTalkContent.append(strTalking);
}}
} catch (Exception io_e) {
System.out.println(io_e.getMessage());
}}}}

该类用来实现与服务器端数据同步。当有客户发言或者有客户和另一客户私下交谈时,该类将立即更新这些数据,在客户端显示出来。所以,该类在客户会话期间一直在后台运行。负责发送客户请求的类是RequestSender。RequestSender用成员方法chatAll ()和chatOne ()分别实现大厅发言和私下交谈,其实现可以参照以上程序。客户端applet的实现代码这里从略。至此,客户端程序也已完成。以下是客户端程序运行时的快照(图2。4。1和图2。4。2)。

图2。4。1
图2。4。1

图2。4。2
图2。4。2

5.系统扩展及补充说明
以上实例由于篇幅原因只实现很少一部分功能,但能体现该即时通信系统的总体设计思想,而且很容易实现功能扩展。例如,可以实现公共数据处理类(CmDataProcessor)与DBMS的交互,实现数据的持久化(persistence);可以对applet实现数字签名来加大它对客户机的访问权限,从而实现文件的传输,实现多媒体交互;可以实现applet与servlet的对话,将比较复杂的业务逻辑交给应用服务器中的EJB来处理等。由于本人水平有限,难免有错误之处,欢迎批评指正。

关于作者
杨健,中南工业大学计算机科学与技术专业硕士研究生,参与过大型MIS系统开发,网站建设等,现在正致力于Java技术的研究。Email: ytjcopy@263.net

(来源:转载自umlchina,Hans-Erik Erikkson, Magnus Penker著,刘忠(caulzhong@sina.com) 译 )

文的案例学习提供了一个例子,说明如何将UML用在现实中。一个处理图书馆借阅和预定图书和杂志的应用程序,可以大到足够检验UML解决现实问题能力的程度。但是如果太大的话,则不适合在杂志上发表。

在分析模型中,用用例和域分析描述了应用程序。我们进一步把它扩展成设计模型。在设计模型中,我们描述了典型的技术解决方案细节。最后,我们编写了一段Java代码(代码连同完整的分析和设计模型放在网上,以一种包括评估版在内的Rational Rose能够识别的格式在线提供。)

必须注意,这里只是一个可行的解决方案。可能会有许多其他的解决方案。没有绝对正确的方案。当然,有的方案更好一些,但只有不断的实践和努力的工作才能掌握相应的技能。

1.需求(Requirements)

典型地,由系统最终用户的代表写出文本形式的需求规范文档。对于该图书馆应用程序来说,需求规范文档应该类似于这样:

1.       这是一个图书馆支持系统;

2.       图书馆将图书和杂志借给借书者。借书者已经预先注册,图书和杂志也预先注册;

3.       图书馆负责新书的购买。每一本图书都购进多本书。当旧书超期或破旧不堪时,从图书馆中去掉。

4.       图书管理员是图书馆的员工。他们的工作就是和读者打交道并在软件系统的支持下工作。

5.       借阅人可以预定当前没有的图书和杂志。这样,当他所预定的图书和杂志归还回来或购进时,就通知预定人。当预定了某书的借书者借阅了该书后,预定就取消。或者通过显式的取消过程强行取消预定。

6.       图书馆能够容易地建立、修改和删除标题、借书者、借阅信息和预定信息。

7.       系统能够运行在所有流行的技术环境中,包括Unix, WindowsOS/2,并应有一个现代的图形用户界面 (GUI)

8.       系统容易扩展新功能。

系统的第一版不必考虑预定的图书到达后通知预定人的功能,也不必检查借书过期的情况。

2.分析(Analysis)

系统分析的目的是捕获和描述所有的系统需求,并且建立一个模型来定义系统中主要的域类。通过系统分析达到开发者和需求者的理解和沟通。因此,分析一般都是分析员和用户协作的产物。

在这个阶段,程序开发者不应该考虑代码或程序的问题;它只是理解需求和实现系统的第一步。

2.1需求分析(Requirements Analysis)

分析的第一步是确定系统能够做什么?谁来使用这个系统?这些分别叫角色(actors)和用例(use cases)。用例描述了系统提供什么样的功能。通过阅读和分析文档,以及和潜在的用户讨论系统来分析用例。

图书馆的角色定为图书管理员和借书人。图书管理员是软件系统的用户;而借书者则是来借阅或预定图书杂志的客户。偶尔,图书管理员或图书馆的其他工作人员也可能是一个借书者。借书者不直接和系统交互,借书人的功能由图书管理员代为执行。

图书馆系统中的用例有:

1.         借书

2.         还书

3.         预定

4.         取消预定

5.         增加标题

6.         修改或删除标题

7.         增加书目

8.         删除书目

9.         增加借书者

10.      修改或删除借书者

由于一本书通常有多个备份,因此系统必须将书的标题和书目的概念区分开。

图书馆系统分析的结果写在UML 用例图中,如图1所示。每一个用例都附带有文本文档,描述用例和客户交互的细节。文本是通过与客户讨论得到的。用例“借书”描述如下:

1.如果借阅者没有预定:

h 确定标题

h 确定该标题下有效的书目

h 确定借书者

h 图书馆将书借出

h 登记一个新的借阅

2.如果借阅者有预定:

h 确定借书人

h 确定标题

h 确定该标题下有效的书目

h 图书馆将相应的书目借出

h 登记一个新的借阅

h 取消预定

除了定义系统的功能需求之外,在分析过程中用例用于检查是否有相应的域类已经被定义,然后他们可以被用在设计阶段,确保解决方案可以有效地处理系统功能。可以在顺序图中可视化实现细节。



 

1:角色和用例。分析中的第一步就是指出系统能被用来做什么,谁将去使用它。它们分别就是用例和角色。所有的用例必须始于角色,而且有些用例也结束于角色。角色是位于你所工作的系统外部的人或其他系统。一台打印机或一个数据库都可能是一个角色。本系统有两个角色:借阅者和图书管理员。通过与用户或客户的讨论,可以将每一个用例用文字进行说明。

 

2.2域分析(Domain Analysis)

系统分析也详细地列出了域(系统中的关键类)。为了导出一个域分析,可以阅读规范文档(specifications)和用例,查找哪一些概念应该被系统处理。或者组织一个集体讨论,在用户及领域专家共同的参与下指出系统中必须处理的关键概念,以及它们之间的关系。

图书馆系统中的域类如下:borrowerinformation(如此命名是为了与用例图中的角色borrower区分开来),titlebook title, magazine title, item, reservationloan。这些类以及它们之间的关系记录在类图文档中,如图2所示。域类定义为Business object版型,Business object版型是一个用户自定义的版型,指定该类的对象是关键域的一部分,并且应该在系统中持久存储。

其中有些类有UML状态图,用来显示这些类的对象可能具有的不同状态,以及触发他们的状态发生改变的事件。该例子中有状态图的类是item title类。

用例lend item(借阅者没有预定的情况)的顺序图显示在图3中。所有用例的顺序图都可从在线模型中查到。

图2:域类结构。域分析详细说明了系统中的关键类。对每一个对象而言,如果它调用了其他对象的方法,那么在他们之间就用一条直线连结起来,以显示他们之间的关系。每一个代表类的四边形被分成了三部分,最顶层包括类的名称,中间一层是类的属性,最底层是类的方法。类之间的直线是关联,用来指出一个对象调用另一个对象的方法。如果再仔细看,将会发现在Loan和Item之间的关联关系中靠近Loan的一端有“0..1”,这代表关联的重数。重数“0..1表示Item可以感知0个到1个loan。其他可能出现的重数还有:“0..*”表示0或多;“1”表示就是1;“0”表示就是0,“1..*”表示1或多。

当对顺序图建模时,必须提供窗体和对话框作为人机交互的界面。在本分析当中,只要知道借书、预定和还书需要窗体就可以了。在此,详细的界面不必考虑。

为了把系统中的窗体类和域类分开,所有的窗体类组织在一起放在GUI Package包中。域类组织在一起放在Business Package包中。



 

图3:Lend item场景的顺序图。场景是从头到尾实现一个用例的一次特定的过程。场景总是始于角色,而角色是属于系统外部的。场景描绘了从所有角色的观点出发,完成一次系统动作的完整过程。UML在用顺序图来图示场景。本用例图显示了在借阅者没有预定图书的情况下的Lend用例。横在图的顶部的是参与交互的对象。自上而下表示时间的流逝。首先,图书管理员尝试去查找标题。标有“Lending Window”的是用户界面,在分析阶段作为一个粗略的对象。横在顺序图中的每一个箭头都是一次方法的调用,箭头的首端是调用的对象,箭头的末端是被调用的对象。

3.设计(Design)

设计阶段对分析模型进行扩展并将模型进一步细化,并考虑技术细节和限制条件。设计的目的是指定一个可行的解决方案,以便能很容易地转变成为编程代码。

设计可以分成两个阶段:

体系结构设计阶段(Architecture Design)。这是一个从较高层次的进行的设计,用来定义包(子系统),描述包之间的依赖性及通信机制。很自然,目的是要设计一个清晰简单的体系结构,有很少的依赖性,而且尽可能避免双向依赖。详细设计阶段(Detailed Design)。在此阶段,所有的类都详尽地进行描述,给编写代码的程序员一个清晰的规范说明。UML中的动态模型用来说明类的对象如何在特定的情况下做出相应的表现。

3.1体系结构设计

一个良好的体系结构设计是一个可扩展的和可改变的系统的基础。包可能关注特定的功能领域或关注特定的技术领域。把应用程序逻辑(域类)和技术逻辑分开是至关重要的,这样不管哪一部分的改变都不会影响其他的部分。

本案例的包或叫子系统如下:

User-Interface Package包。该包中的类基于Java AWT包,java AWT是一个用来书写用户界面应用程序的Java的标准库。该包和Business-objects Package包协作。Business-objects Package包包含那些实际存储数据的类。UI包调用Business 对象的操作,对他们进行取出或插入数据操作。

Business-object Package。该包包括域类,这些域类(如borrowerinformationtitleitemloan等)来自于分析模型。设计阶段完整地定义了这些类的操作,并增加了一些其他细节来支持持续存储。Business-object包与Database Package进行协作。所有的Business-object类必须继承Database Package中的persistent类。

Database PackageDatabase PackageBusiness-object包中的类提供服务,以便他们能够持续地存储。在当前版本中,persistent类将把它的子类的对象存储到文件系统的文件中。

Utility PackageUtility Package包含了一些服务,用来被系统中其他包调用。当前,ObjId类是该包中的唯一的类。用来被整个系统包括User-InterfaceBusiness-ObjectDatabase package使用。

这些包的内部设计如图4所示.

 

4:图书馆应用程序体系结构设计总览。本类图显示了应用程序包以及它们之间的依赖性。Database包提供了persistence类。Utility包提供了Object ID类。Business-Object包包含了域类(详细情况参见图5)最后,UI包(在本例中它是基于标准Jaa AWT库)调用business对象中的操作来实现对他们的数据存取操作。,

3.2详细设计

细节设计描述了新的技术性的类,如User-InterfaceDatabase 包中的类,并且丰富了分析阶段所形成的Business-Object类。类图、状态图和动态图用的还是分析阶段所形成的图,但对他们定义的更加详细,具有了更高的技术水平。在分析阶段对用例进行的文字性描述在此用来证明用例在设计阶段也能被处理。顺序图就是用来说明用例如何在系统中被实现的。

Database Package。应用程序必须有持续存储的对象。因此,必须增加数据层来提供这样的服务。为简单起见,我们将对象以文件的形式保存在磁盘上。存储的细节被应用程序隐藏起来,只需调用诸如store(), update(),delete()和find()这样的公共操作即可。这些都是persistent类的一部分,所有需要持续对象的类必须继承它。

对类进行持续处理的一个重要因子就是ObjId类。它的对象用来引用系统中的任何持续对象(不管这个对象是在磁盘上还是已经被读进了应用程序之中)。ObjIdObject Identity的简写,它是一个广为应用的技术,用来有效地处理应用程序中的对象引用。通过使用object identifiers,一个对象ID能被传递到普通的persistent.getobject()操作中,进而该对象将被从持续的存储体中取出或存储。通常情况下,这些都是通过每个持续类的一个getobject操作完成的。当然,持续类同时也作一些检查或格式转换的操作。一个对象标识符也能作为一个参数很容易地在两个操作之间传递(例如,一个查找特定对象的查询窗口可以将它的查询结果通过object id传递给另一个窗口 )。

ObjId是一个系统中所有的包(User Interface , Business ObjectDatabase)都能使用的通用类,所以在设计阶段它被放置在Utility包中,而不是放在Database包中。

当前对persistent类的实现还能改进。为此,定义persistent类的接口,方便持续存储的改变。一些备选的方案可能是:将对象存储在一个关系数据库中或存储在面向对象的数据库中,或使用Java 1.1所支持的持续对象来存储他们。

Business-Object Package。设计阶段的Business-Object包基于相应的分析阶段的放置域类的包。类和类之间的关系以及他们的行为继续保留,只是被描述的更为详细,包括他们的关系和行为如何被实现。

分析模型中的一些操作中被翻译成设计模型的操作,另一些改了名字。这是很正常的事,因为分析阶段得到的是每一个类的草图,而设计阶段是对系统的详细描述。因此,设计模型的操作必须有设计良好的特征值和返回值(由于空间限制,图5没有显示,但他们在在线模型中都有)。注意以下所列的设计和分析阶段的变化:

1.         系统的当前版本不要求检查书目是否按时归还,也不要求处理预定的次序。因此没有在loan reservation类中设置日期属性。

2.         除了最长借阅期外,对杂志和书标题的处理方式是一样的。因此分析阶段的子类magazine titlebook title被认为在设计阶段是不必要的,而是在title类中增加type属性来指出该标题引用的是一本书还是一本杂志。在面向对象的设计中不存在设计不能简化分析的说法。

如果认为有必要的话,在将来的版本中这些简化都可以很容易地被取消。

分析阶段的状态图也在设计阶段细化了。状态图显示了如何表示状态及如何在系统中处理状态。设计阶段的title 类的状态图如图6所示。其他的对象可以通过调用如图所示的操作addreservation()和removereservation()来改变title对象的状态。

User-Interface PackageUser-Interface Package位于其他包的“上面。在系统中它为用户提供输出信息和服务。正如上面曾经提到的,该包是基于标准Java AWTabstract windows toolkit)类的。

设计模型中的动态模型放置在GUI包中,因为所有和用户的交互都从用户界面开始。在此声明,顺序图用来显示动态模型。用例在设计模型中的实现通过顺序图被详细地显示出来,包括类中的实际操作。

顺序图由一系列的交互构成。在实现阶段(编码),考虑到具体情况,可能会有更多的交互。图7显示了add title用例的顺序图。实际的操作和特征值从在线模型代码中可以看到。

 


图5:商业对象设计(Business-Object design)。本图描述了在Business-Object包中的不同类的设计。设计包括定型模型,更完全地定制界面,给属性选择数据类型等等。

6:Title的状态图。Title具有预定和非预定状态,在设计中,通过称为“reservations”的矢量来实现。

7:Add Title的顺序图。本图中所涉及到的用户界面问题的详细情况已经超出了本文的讨论范围。



协作图可以作为顺序图的替代,如图8所示:

8:Add Title的协作图。本图中涉及到的用户界面问题的详细情况已经超出了本文讨论的范围

3.3 用户界面 设计(User-lnterface Design)

设计阶段的一个特定的活动是创建用户界面。

图书馆系统的用户界面基于用例,分为以下几部分,每一部分都在主窗体菜单上给出一个单独的菜单项。

Functions:实现系统基本功能的窗体,通过它可以实现借阅、归还和对图书的预定。

Information:查看系统信息的窗体,收集了借阅者和图书的信息。

Maintenance:维护系统的窗体,添加、修改和删除标题、借阅者和书目。

9显示了一个User-Interface Package中类图的例子。其中包含了典型的AWT事件句柄。按钮(button)、标签(label)和编辑(edit)等的属性没有显示。

典型地,每一个窗体都给出了系统的一个服务,并且映射一个最初的用例(尽管并非所有的用户界面都必须从用例中映射)。创建成功的用户界面已经超出了本文所讨论的范围。在此邀请读者来考虑用symantec visual cafe 环境开发的本应用程序的UI代码(已经放在网上

图9:功能类图模型。功能菜单中的用户界面类一般都有1对1的关联关系,表示需要建立关联的窗口类,或者需要访问关联的商业对象类。

4.实现(Implementation)



在构造或称实现阶段进行程序编写。该应用程序要求能运行在几个不同的处理器和不同的操作系统上,因此选择Java来实现系统。Java可以轻松地将逻辑类映射为代码组件,因为在类和Java代码文件之间有11的映射。

10:组件图显示了依赖性。源代码组件实现了域类,他们之间的关联显示为双向依赖性

10显示,设计模型的组件视图简单地将逻辑视图中的类映射为组件视图中的组件。每个逻辑视图包含了一个指向逻辑视图中类的连接,因此可以很容易地在不同的视图之间导航(即便象本例只是简单地使用了文件名)。由于依赖性可以从逻辑视图的类图中得到,因此组件图中没有显示组件之间的依赖性。

编写代码时,从下面的设计模型的图中取出规范说明:

1.类规范:每一个类的规范详细地显示了必要的属性和操作。

2.类图:类图由类构成,显示了类的结构以及类之间的关系。

3.状态图:类的状态图显示了类可能具有的状态以及需要处理的状态转移(以及触发转移的操作)。

4.动态图(顺序图、协作图和活动图):涉及到类的对象,显示了类中的特定方法如何实现,或对象之间如何使用其它类的对象进行交互。

5.用例图和规范:当开发者需要了解更多的关于系统如何被使用的信息时(当开发者感到他或她已经迷失在一片细节中),他们显示了系统被使用的结果。

很自然,设计中的某些缺陷可以在编码阶段发现。可能需要一些新的操作或修改某些操作,这意味着开发者必须改变设计模型。这在所有的工程中都会发生。重要的是将设计模型和代码同步,以便使模型能够成为系统的最终文档。

这里给出了loan 类和部分titleframe类的Java代码。整个应用程序的Java代码可以从网上查到。当学习这些代码时,注意结合UML模型,考虑UML结构是如何被转变为代码的。注意以下几点:

1.Java包规范是与类所属的逻辑视图或组件视图中相应的包等值的代码。

2.私有属性对应于模型中指定的属性;并且,很自然Java 方法对应于模型中的操作。

3.ObjId类(对象标识符)被调用来实现关联。也就是说,关联通常和类一起保存(因为ObjId类是持续的)。

程序清单1的代码示例来自于loan类,loan类是一个Business-Object类,用来存放借阅信息。由于该类主要是信息的存放处,因此程序的实现是直接的,代码也简单。大多数的功能继承了Database 包中的Persistent类。该类的唯一属性是对象标识符,将itemborrowerinformation类关联起来。并且这些关联属性也在Write()和Read()操作中被保存。

试着在Add Title顺序图(图7)上下文中检验一下程序清单2中显示的Addbutton_Clicked()操作。结合顺序图阅读代码,可以看出:它就是另一个对顺序图所表达的协作关系的更为详细的描述。

所有设计模型中的顺序图的编码都包含在源代码当中(操作名和类名显示在顺序图中)

5.测试和部署(Test and Deployment)

编码结束后,UML的使用还没有停止。例如,可以检验用例能否在已完成的应用程序中得到很好的支持。对于系统的部署来说,利用模型和本文可以做一份得心应手的文档。

6.总结

本学习案例的不同部分由一组人分别来设计完成。他们以做实际工程的方式努力完成该工作。尽管不同的阶段和活动似乎是独立的,而且以严格的顺序来管理,但在实际工作中仍然有很多反复。设计中的经验和教训反馈到分析模型,实现阶段所发现的新情况在设计模型中更改或更新。这就是建立面向对象系统的一般方法。

本文摘录自UML ToolkitNew YorkWiley & Sons1998.  Hans-Erik Erikkson是一个有名的C++OO技术方面的作者。Magnus PenkerAstrakan培训副主席,Astrankan是一个瑞典专攻OO建模和设计的公司。

原文链接:http://www.umlchina.com/Indepth/DesignJava.htm
模型及源码:http://www.umlchina.com/zippdf/libraryjava.zip

附:主要术语中英文对照

actor:角色

use case:用例

domain:域

domain analysis:域分析

specification:规范文档

sequence diagram:顺序图

collaboration diagram:协作图

component diagram:组件图

state diagram:状态图

dependency:依赖性

attribute:属性

method:方法

operation:操作

association:关联

multiplicity:重数

class:类

object:对象

package:包

implementation:实现

deployment:部署

 

其实,就算用Java建造一个不是很烦琐的web应用,也不是件轻松的事情。 在构架的一开始就有很多事情要考虑。 从高处看,摆在开发者面前有很多问题:要考虑是怎样建立用户接口?在哪里处理业务逻辑? 怎样持久化的数据。 而这三层构架中,每一层都有他们要仔细考虑的。 各个层该使用什么技术? 怎样的设计能松散耦合还能灵活改变? 怎样替换某个层而不影响整体构架?应用程序如何做各种级别的业务处理(比如事务处理)?

构架一个Web应用需要弄明白好多问题。 幸运的是,已经有不少开发者已经遇到过这类问题,并且建立了处理这类问题的框架。 一个好框架具备以下几点: 减轻开发者处理复杂的问题的负担(“不重复发明轮子”); 内部有良好的扩展; 并且有一个支持它的强大的用户团体。 好的构架一般有针对性的处理某一类问题,并且能将它做好(Do One Thing well)。 然而,你的程序中有几个层可能需要使用特定的框架,已经完成的UI(用户接口) 并不代表你也可以把你的业务逻辑和持久逻辑偶合到你的UI部分。 举个例子, 你不该在一个Controller(控制器)里面写JDBC代码作为你的业务逻辑, 这不是控制器应该提供的。 一个UI 控制器应该委派给其它给在UI范围之外的轻量级组件。 好的框架应该能指导代码如何分布。 更重要的是,框架能把开发者从编码中解放出来,使他们能专心于应用程序的逻辑(这对客户来说很重要)。

这篇文章将讨论怎样结合几种著名的框架来使得你的应用程序做到松弛耦合。

如何建立你的架构,并且怎样让你的各个应用层保持一致。?如何整合框架以便让每个层在以一种松散偶合的方式彼此作用而不用管低层的技术细节?这对我们来说真是一种挑战。 这里讨论一个整合框架的策略( 使用3 种受欢迎的开源框架) :表示层我们用Struts; 业务层我们用Spring;而持久层则用Hibernate。 你也可以用其他FrameWork替换只要能得到同样的效果。 见图1 (框架组合示意图)


应用程序的分层

大部分的Web应用在职责上至少能被分成4层。 这四层是:presentation(描述),persistence(持久),business(业务)和domain model(域模块)。每个层在处理程序上都应该有一项明确的责任, 而不应该在功能上与其它层混合,并且每个层要与其它层分开的,但要给他们之间放一个通信接口。 我们就从介绍各个层开始,讨论一下这些层应该提供什么,不应该提供什么。

表示层(The Presentation Layer)

一般来讲,一个典型的Web应用的的末端应该是表示层。 很多Java发者也理解Struts所提供的。 象业务逻辑之类的被打包到org.apache.struts.Action., 因此,我们很赞成使用Struts这样的框架。

下面是Struts所负责的:

* 管理用户的请求,做出相应的响应。
* 提供一个Controller ,委派调用业务逻辑和其它上层处理。
* 处理异常,抛给Struts Action
* 为显示提供一个模型
* UI验证。

以下条款,不该在Struts显示层的编码中经常出现。 它们与显示层无关的。

* 直接的与数据库通信,例如JDBC调用。
* 与你应用程序相关联的业务逻辑以及校验。
* 事物管理。

在表示层引入这些代码,则会带来高偶合和麻烦的维护。

持久层(The Persistence Layer)

典型的Web应用的另一个末端是持久层。这里通常是程序最容易失控的地方。开发者总是低估构建他们自己的持久框架的挑战性。系统内部的持续层不但需要大量调试时间,而且还经常缺少功能使之变得难以控制,这是持久层的通病。 还好有几个ORM开源框架很好的解决了这类问题。尤其是Hibernate。 Hibernate为java提供了OR持久化机制和查询服务, 它还给已经熟悉SQL和JDBC API 的Java开发者一个学习桥梁,他们学习起来很方便。 Hibernate的持久对象是基于POJO和Java collections。此外,使用Hibernate并不妨碍你正在使用的IDE。

请看下面的条目,你在持久层编码中需要了解的。

* 查询对象的相关信息的语句。 Hibernate通过一个OO查询语言(HQL)或者正则表达的API来完成查询。 HQL非常类似于SQL– 只是把SQL里的table和columns用Object和它的fields代替。 你需要学习一些新的HQL语言; 不管怎样,他们容易理解而文档也做的很好。 HQL是一种对象查询的自然语言,花很小的代价就能学习它。
* 如何存储,更新,删除数据库记录。
* 象Hibernate这类的高级ORM框架支持大部分主流数据库,并且他们支持 Parent/child关系,事物处理,继承和多态。

业务层(The Business Layer)

一个典型Web应用的中间部分是业务层或者服务层。 从编码的视角来看,这层是最容易被忽视的一层。 而我们却往往在UI层或持久层周围看到这些业务处理的代码,这其实是不正确的,因为它导致了程序代码的紧密偶合,这样一来,随着时间推移这些代码很难维护。幸好,针对这一问题有好几种Frameworks存在。 最受欢迎的两个框架是Spring和PicoContainer。 这些为也被称为microcontainers,他们能让你很好的把对象搭配起来。 这两个框架都着手于‘依赖注射’(dependency injection)(还有我们知道的‘控制反转’Inversion of Control=IoC)这样的简单概念。 这篇文章将关注于Spring的注射(译注:通过一个给定参数的Setter方法来构造Bean,有所不同于Factory), Spring还提供了Setter Injection(type2),Constructor Injection(type3)等方式供我们选择。 Spring把程序中所涉及到包含业务逻辑和Dao的Objects——例如transaction management handler(事物管理控制)、Object Factoris(对象工厂)、service objects(服务组件)——都通过XML来配置联系起来。

后面我们会举个例子来揭示一下Spring 是怎样运用这些概念。

业务层所负责的如下:

* 处理应用程序的 业务逻辑和业务校验
* 管理事物
* 允许与其它层相互作用的接口
* 管理业务层级别的对象的依赖。
* 在显示层和持久层之间增加了一个灵活的机制,使得他们不直接的联系在一起。
* 通过揭示 从显示层到业务层之间的Context来得到business services。
* 管理程序的执行(从业务层到持久层)。

域模块层(The Domain Model Layer )

既然我们致力于的是一个不是很复杂的Web的应用, 我们需要一个对象集合,让它在不同层之间移动的。 域模块层由实际需求中的业务对象组成 比如, OrderLineItem , Product等等。 开发者在这层 不用管那些DTOs,仅关注domain object即可。 例如,Hibernate允许你将数据库中的信息存放入对象(domain objects),这样你可以在连接断开的情况下把这些数据显示到UI层。 而那些对象也可以返回给持续层,从而在数据库里更新。 而且,你不必把对象转化成DTOs(这可能似的它在不同层之间的在传输过程中丢失),这个模型使得Java开发者能很自然运用OO,而不需要附加的编码。

一个简单例子

既然我们已经从全局上理解这些组件。 现在就让我们开始实践吧。 我们还是用 Struts,Spring 和Hibernate。这三个框架已经被描述够多了,这里就不重复介绍了。 这篇文章举例指导你如何使用这三个框架整合开发, 并向你揭示 一个请求是如何贯穿于各个层的。(从用户的加入一个Order到数据库,显示;进而更新、删除)。

从这里可以下载到程序程序原代码(download)

既然每个层是互相作用的,我们就先来创建domain objects。首先,我们要在这些Object中要确定那些是需要持久化的,哪些是提供给business logic,那些是显示接口的设计。 下一步,我们将配置我们的持久层并且定义好Hibernate的OR mappings。然后定义好Business Objects。有了这些组成部分之后,我们将 使用Spring把这些连接起来。 最后,我们提供给Spring一个持久层,从这个持久层里我们可以知道它是如何与业务逻辑层(business service layer)通信的,以及它是怎样处理其他层抛出的异常的。。

域对象层(Domain Object Layer)

这层是编码的着手点,我们的编码就从这层开始。 例子中Order 与OrderItem 是一个One—To—Many的关系。 下面就是Domain Object Layer的两个对象:

· com.meagle.bo.Order.java: 包含了一个Order的概要信息
· com.meagle.bo.OrderLineItem.java: 包含了Order的详细信息

好好考虑怎你的package命名,这反应出了你是怎样分层的。 例如 domain objects在程序中可能打包在com.meagle.bo内。 更详细一点将打包在com. meagle.bo的子目录下面。business logic应该从com.meagle.serice开始打包,而DAO 对象应该位于com.meagle.service.dao.hibernate。反应Forms和Actions的 持久对象(presentation classes) 应该分别放在 com.meagle.action和com.meagle.forms包。 准确的给包命名使得你的classes很好分割并且易于维护,并且在你添加新的classes时,能使得程序结构上保持上下一致。

持久层的配置(Persistence Layer Configuration)
建立Hibernate的持久层 需要好几个步骤。 第一步让我们把BO持久化。 既然Hibernate是通过POJO工作的, 因此Order和 OrderLineItem对象需要给所有的fileds 加上getter,setter方法。 Hibernate通过XML文件来映射(OR)对象,以下两个xml文件分别映射了Order 和OrderItem对象。(这里有个叫XDoclet工具可以自动生成你的XML影射文件)

Order.hbm.xml
OrderLineItem.hbm.xml

你可以在WebContent/WEB-INF/classes/com/meagle/bo目录下找到这些xml文件。Hibernate的 SessionFactory 是用来告诉程序 应该与哪个数据库通信,该使用哪个连接池或使用了DataSource, 应该加载哪些持久对象。而Session接口是用来完成Selecting,Saving,Delete和Updating这些操作。 后面的我们将讲述SessionFactory和Session是怎样设置的。

业务层的配置(Business Layer Configuration)

既然我们已经有了domain objects,接下来我们就要business service objects了,用他们来执行程序的logic,调用持久层,得到UI层的requests,处理transactions,并且控制exceptions。 为了将这些连接起来并且易于管理,我们将使用面向方面的 SpringFramework。 Spring 提供了 控制倒置(inversion of control 0==IoC)和注射依赖设置(setter dependency injection)这些方式(可供选择),用XML文件将对象连接起来。 IoC是一个简单概念(它允许一个对象在上层接受其他对象的创建),用IoC这种方式让你的对象从创建中释放了出来,降低了偶合度。

这里是一个没有使用IoC的对象创建的例子,它有很高偶合度。

图 2.没有使用 IoC. A 创建了 B 和 C

而这里是一个使用IoC的例子,这种方式允许对象在高层可以创建并进入另外一个对象,所以这样可以直接被执行。

图 3. 对象使用了 IoC。 A 包含了接受B,C的 setter方法 , 这同样达到了 由A创建B,C的目的。

建立我们的业务服务对象(Building Our Business Service Objects)

Business Object中的Setter方法接受的是接口,这样我们可以很松散的定义对象实现,然后注入。 在我们的案例中,我们将用一个business service object接收一个DAO,用它来控制domain objects的持久化。 由于在这个例子中使用了Hibernate,我们可以很方便的用其他持久框架实现 同时通知Spring 有新的DAO可以使用了。

在面向接口的编程中,你会明白 “注射依赖”模式是怎样松散耦合你的业务逻辑和持久机制的:)。

下面是一个接口business service object,DAO代码片段:


public interface IOrderService {
public abstract Order saveNewOrder(Order order)
throws OrderException, OrderMinimumAmountException;

public abstract List findOrderByUser( String user) throws OrderException;

public abstract Order findOrderById(int id) throws OrderException;

public abstract void setOrderDAO( IOrderDAO orderDAO);
}


注意到这段代码里有一个 setOrderDao(),它就是一个DAO Object设置方法(注射器)。 但这里并没有一个getOrderDao的方法,这不必要,因为你并不会在外部访问这个orderDao。这个DAO Objecte将被调用,和我们的persistence layer 通信。我们将用Spring把DAO Object 和 business service object搭配起来的。因为我们是面向接口编程的,所以并不需要将实现类紧密的耦合在一起。

接下去我们开始我们的DAO的实现类进行编码。 既然Spring已经有对Hibernate的支持,那这个例子就直接继承HibernateDaoSupport类了,这个类很有用,我们可以参考HibernateTemplate(它主要是针对HibernateDaoSupport的一个用法,译注:具体可以查看Srping 的API)。 下面是这个DAO接口代码:


public interface IOrderDAO {
public abstract Order findOrderById(
final int id);

public abstract List findOrdersPlaceByUser(
final String placedBy);
public abstract Order saveOrder(
final Order order);
}


我们仍然要给我们持久层组装很多关联的对象,这里包含了HibernateSessionFactory 和TransactionManager。 Spring 提供了一个 HibernateTransactionManager,他用线程捆绑了一个Hibernate Session,用它来支持transactions(请查看ThreadLocal) 。

下面是HibernateSessionFactory 和 HibernateTransactionManager:的配置:


<bean id="mySessionFactory"
class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="mappingResources">
<list>
<value>
com/meagle/bo/Order.hbm.xml
</value>
<value>
com/meagle/bo/OrderLineItem.hbm.xml
</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
net.sf.hibernate.dialect.MySQLDialect
</prop>
<prop key="hibernate.show_sql">
false
</prop>
<prop key="hibernate.proxool.xml">
C:/MyWebApps/.../WEB-INF/proxool.xml
</prop>
<prop key="hibernate.proxool.pool_alias">
spring
</prop>
</props>
</property>
</bean>

<!-- Transaction manager for a single Hibernate
SessionFactory (alternative to JTA) -->
<bean id="myTransactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionManager">
<property name="sessionFactory">
<ref local="mySessionFactory"/>
</property>
</bean>


可以看出:每个对象都可以在Spring 配置信息中用<bean>标签引用。在这里,mySessionFactory引用了HibernateSessionFactory,而myTransactionManager引用了HibernateTransactionManage。 注意代码中myTransactionManger Bean有个sessionFactory属性。 HibernateTransactionManager有个sessionFactory setter 和 getter方法,这是用来在Spring启动的时候实现“依赖注入” (dependency injection)的。 在sessionFactory 属性里 引用mySessionFactory。这两个对象在Spring容器初始化后就被组装了起来了。 这样的搭配让你从 单例(singleton objects)和工厂(factories)中解放了出来,降低了代码的维护代价。 mySessionFactory.的两个属性,分别是用来注入mappingResources 和 hibernatePropertes的。通常,如果你在Spring之外使用Hibernate,这样的设置应该放在hibernate.cfg.xml中的。 不管怎样,Spring提供了一个便捷的方式—–在Spring内部配置中并入了Hibernate的配置。 如果要得到更多的信息,可以查阅Spring API。

既然我们已经组装配置好了Service Beans,就需要把Business Service Object和 DAO也组装起来,并把这些对象配到一个事务管理器(transaction manager)里。

在Spring中的配置信息:


<!-- ORDER SERVICE -->
<bean id="orderService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="myTransactionManager"/>
</property>
<property name="target">
<ref local="orderTarget"/>
</property>
<property name="transactionAttributes">
<props>
<prop key="find*">
PROPAGATION_REQUIRED,readOnly,-OrderException
</prop>
<prop key="save*">
PROPAGATION_REQUIRED,-OrderException
</prop>
</props>
</property>
</bean>

<!-- ORDER TARGET PRIMARY BUSINESS OBJECT:
Hibernate implementation -->
<bean id="orderTarget"
class="com.meagle.service.spring.OrderServiceSpringImpl">
<property name="orderDAO">
<ref local="orderDAO"/>
</property>
</bean>

<!-- ORDER DAO OBJECT -->
<bean id="orderDAO"
class="com.meagle.service.dao.hibernate.OrderHibernateDAO">
<property name="sessionFactory">
<ref local="mySessionFactory"/>
</property>
</bean>


图4 是我们对象搭建的一个提纲。 从中可以看出,每个对象都联系着Spring,并且能通过Spring注入到其他对象。把它与Spring的配置文件比较,观察他们之间的关系

图 4. Spring就是这样基于配置文件,将各个Bean搭建在一起。

这个例子使用一个TransactionProxyFactoryBean,它定义了一个setTransactionManager()。 这对象很有用,他能很方便的处理你申明的事物还有Service Object。 你可以通过transactionAttributes属性来定义怎样处理。 想知道更多还是参考TransactionAttributeEditor吧。

TransactionProxyFactoryBean 还有个setter. 这会被我们 Business service object(orderTarget)引用, orderTarget定义了 业务服务层,并且它还有个属性,由setOrderDAO()引用。这个属性

Spring 和Bean 的还有一点要注意的: bean可以以用两种方式创造。 这些都在单例模式(Sington)和原型模式(propotype)中定义了。 默认的方式是singleton,这意味着共享的实例将被束缚。 而原形模式是在Spring用到bean的时候允许新建实例的。当每个用户需要得到他们自己Bean的Copy时,你应该仅使用prototype模式。(更多的请参考设计模式中的单例模式和原形模式)

提供一个服务定位器(Providing a Service Locator)

既然我们已经将我们的Serices和DAO搭配起来了。我们需要把我们的Service显示到其他层。 这个通常是在Struts或者Swing这层里编码。一个简单方法就是用 服务定位器返回给Spring context 。当然,可以通过直接调用Spring中的Bean来做。

下面是一个Struts Actin 中的服务定位器的一个例子。

 
public abstract class BaseAction extends Action {

private IOrderService orderService;

public void setServlet(ActionServlet
actionServlet) {
super.setServlet(actionServlet);
ServletContext servletContext =
actionServlet.getServletContext();

WebApplicationContext wac =
WebApplicationContextUtils.
getRequiredWebApplicationContext(
servletContext);

this.orderService = (IOrderService)
wac.getBean("orderService");
}

protected IOrderService getOrderService() {
return orderService;
}
}


UI 层配置 (UI Layer Configuration)
这个例子里UI层 使用了Struts framework. 这里我们要讲述一下在给程序分层的时候, 哪些是和Struts部分的。我们就从一个Struts-config.xml文件中的Action的配置信息开始吧。

struts-config.xml file.


<action path="/SaveNewOrder"
type="com.meagle.action.SaveOrderAction"
name="OrderForm"
scope="request"
validate="true"
input="/NewOrder.jsp">
<display-name>Save New Order</display-name>
<exception key="error.order.save"
path="/NewOrder.jsp"
scope="request"
type="com.meagle.exception.OrderException"/>
<exception key="error.order.not.enough.money"
path="/NewOrder.jsp"
scope="request"
type="com.meagle.exception.OrderMinimumAmountException"/>
<forward name="success" path="/ViewOrder.jsp"/>
<forward name="failure" path="/NewOrder.jsp"/>
</action>


SaveNewOrder 这个Action是用来持久化UI层里的表单提交过来Order的。这是Struts中一个很典型的Action; 注意观察这个Action中exception配置,这些Exceptions也在Spring 配置文件(applicationContext-hibernate.xml)中配置了(就在 business service object 的transactionAttributes属性里)。 当异常在业务层被被抛出时,我们可以控制他们,并适当的显示给UI层。

第一个异常,OrderException,在持久层保存order对象失败的时候被触发。这将导致事物回滚并且通过BO把异常回传到Struts这一层。

第二个异常,OrderMinimumAmountException也同第一个一样。

搭配整和的最后一步 通过是让你显示层和业务层相结合。这个已经被服务定位器(service locator)实现了(前面讨论过了), 这里服务层作为一个接口提供给我们的业务逻辑和持久层。

SaveNewOrder Action 在Struts中用一个服务定位器(service locator)来调用执行业务方法的。 方法代码如下:


public ActionForward execute(
ActionMapping mapping,
ActionForm form,
javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws java.lang.Exception {
OrderForm oForm = (OrderForm) form;

// Use the form to build an Order object that
// can be saved in the persistence layer.
// See the full source code in the sample app.

// Obtain the wired business service object
// from the service locator configuration
// in BaseAction.
// Delegate the save to the service layer and
// further upstream to save the Order object.

getOrderService().saveNewOrder(order);
oForm.setOrder(order);
ActionMessages messages = new ActionMessages();
messages.add(
ActionMessages.GLOBAL_MESSAGE,
new ActionMessage(
"message.order.saved.successfully"));

saveMessages(request, messages);
return mapping.findForward("success");
}


总结

这篇文章在技术和构架方面掩盖了很多低层的基础信息, 文章的主要的意图在于让你意识到如何给你应用程序分层。 分层可以“解耦”你的代码——允许新的组件被添加进来,而且让你的代码易于维护。 这里用到的技术只是专注于把“解偶”做好。 不管怎样,使用这样的构架可以让你用其他技术代替现在的层。 例如,你可能不使用Hibernate实现持久化。既然你在DAO中面向接口的编程的,所以你完全可以用iBATIS来代替。或者,你也可能想用Struts外的其他的技术或者框架替换现在的UI层(转换久层,实现层并不应该直接影响到你的业务逻辑和业务服务层)。 用适当的框架搭建你的Web应用,其实也不是一件烦琐的工作,更主要的是它“解耦”了你程序中的各个层。

后记

看完这篇文章后,只是觉得很喜欢,于是就翻译了,当然同时也准备着挨大家扔来的鸡蛋:)。

这篇文章里并没有太多的技术细节,和详细的步骤。如果你从未使用过这些框架而在运行实例程序遇上困难的话,可以到CSDN论坛Java Open Source版发贴,我一定会详细解答的(啊哦,这不算做广告吧?),

文章是从一个构架的角度讲述了如何搭配现有的开源框架进行分层, 有太多的术语我都不知道怎么表达,而且可能有很多语句存在错误。如果影响了你的阅读,请你直接点原文地址,我同时也象你说声抱歉。

作者简介:Mark Eagle 高级软件工程师,亚特兰大。
翻 译:Totodo(zhangli@telecomjs.com) 软件工程师

参考:
Struts:http://jakarta.apache.org/struts/index.html
Spring: http://www.springframework.org/
Hibernate: http://www.hibernate.org/
http://www.hibernate.org.cn/
关于控制反转IOC和依赖注射:http://www.martinfowler.com/articles/injection.html

转自:http://forum.javaeye.com/viewtopic.php?t=164&postdays=0&postorder=asc&start=0

制。了解目标组织中当前存在的问题并确定改进的可能性。确保客户、最终用户和开发人员就目标组织达成共识。导出支持目标组织所需的系统需求。 (来自RUPCN)

以上大家可以理解,我们有没有更深的理解呢?我先从业务主角和业务角色说起业务建模,在业务建模中主要有业务主角(BUSINESS ACTOR)和业务角色(BUSINESS WORKER),对此我们有什么了解呢。我先做出定义:业务主角是服务对象,如对商店进行业务建模, 业务主角是顾客。业务角色是服务人员或系统,如对商店进行业务建模,业务角色是售货员。业务主角是为谁提供服务,产生了用例。业务角色是提供服务,完成了用例。可以仔细看看RUPCN。这两个东西它们在ROSE中图形的表示也不一样。

但大家有没有想过,业务建模更深的意义,我们在传统的软件工程来说并没有类似业务建模这个概念,而RATIONAL公司又要将此放入,在软件工程中,它又到底起了什么作用呢?

一般说来,我们做一个软件项目基本上都是从需求调研后就到需求建模和需求分析了,没怎么去想业务建模。而大家有没有想过,我们对需求的处理比较表象,如界面、功能和数据,对业务处理过程缺少规范的说明,而这正是开发必须的。你有没有觉得你以前很多开发没有准确反应需求,是否有一个原因,不是需求不清,而是对需求的实现过程不清,即没有了解好业务,从RUP的角度来说,也就是缺少了业务建模这一关键环节。我曾经思索过业务建模最主要解决什么问题,我的看法就是业务建模就是创建业务处理模型,是进行需求分析的依据。而需求分析的结果,将要确定一个开发规范,正确的实现业务处理过程应当是它的一个重要内容,而我们在需求调研时,往往忽视了这一点。

那么,在需求调研中,需求建模与业务建模谁先谁后呢?个人认为,业务是本来就存在的,不管有没有这个软件或项目,它都存在,它都在按一定的模式运作,因此业务建模与需求调研、需求建模是无关的,立项之前业务模型就可以存在. 或是立项以前,业务建模就可以完成的。 然而,实际并非如此,用户只知道如何处理业务,却很少有一个完整的业务模型,当立项时,就需求承建方邦助客户创建它。因为承建方不了解客户的业务过程是不能建模的, 又必须了解客户的业务。

对于软件开发过程,从软件工程的角度来说大多数人都清楚从需求调研,到需求建模,需求分析,系统分析,系统结构设计,系统代码设计,系统测试,系统维护这一过程,我参与过的项目,让我感到,有些项目失败或是非常难做,不在于以上这些环节没做好,而在于少了业务的环节。

很多人也许会说,业务不就是需求吗,没错,但更多的时候我们关注的是界面、功能、和数据,却很少注意功能之间的联系即业务过程。

有没有想过,我们将软件所有的功能做成菜单,让用户自己选择他们要做什么,而没有一个业务过程的概念,用户要自己知道用了某个功能后下一个该用哪个。这样的系统其实不是应用系统,只是一个数据处理机或者说是一个比较复杂的计算机器。它把业务过程分解为一些独立/分散的功能,而没有把这些功能组织起来。因此,有必要将业务从需求中分离而进行强调。我想,这也就是RATIONAL 公司引入业务建模这个重要的概念吧,

无论是从我的经验、观点和教训来说,还是看了RUP的观点来年,我个人认为新的软件开发过程是:业务调研,业务建模(业务分析),(业务模型分析)需求调研(这时,已经有一部分需求可从来务模型中获得), 需求建模,需求分析……因为建模的过程也是分析的过程,所以业务建模和业务分析可能交叉在一起。如果遇到一个客户,规范到已经有了现成的业务模型,那么直接就拿来就可以了。如果业务建模与需求调研是同一班人,那么需求调研中的业务模型分析工作就比较容易了。


业务建模当中又要注意什么呢?我个人的看法是:千万别把业务建模和需示分析混在一起啦。如网上订单系统,一个人下了订单,当然此订单是通过系统自动完成,并没有后台人员的支持,此时,业务主角当然是下订单的人,那业务角色呢 是否是没有,还是下订单的人? 对于系统来说,业务主角当然是下订单的人,而业务角色呢?不太可能是系统自已吧? 而我的看法是业务角色就是系统自已,那不是很矛盾吗?其实,关键一点是业务建模你不能有需求分析的概念,在这里,系统并不是软件系统,而是完成业务主角的订单,即一个用例的东西,根据业务建模的概念,完成了订单这个用例只能是订单系统,那么业务角色也就是订单系统。因为它是下订单这一业务中。

无论业务角色还是业务主角,都是对角色的抽象,不能具体到某个人的。而一个具体的事务,可能会兼任两个不同的角色,但此时,大部分发生了身份或岗位的转移。

  虽然许多文章曾经讨论过J2EE最佳实践。那么,为什么我还要再写一篇文章呢?本文究竟与以前的文章有何不同或者说比其他文章好在哪呢?
  首先,本文的目标读者是正在从事技术工作的架构师。为了避免浪费大家的才智,我会避免讲述一些陈腐的最佳实践,例如"日常构建(build daily)"、"测试一切(test everything)"和"经常集成( integrate often)。 任何具有称职架构师的项目都有分工明确的、定义良好的团队结构。他们还为进行编码检查、构建代码(每日或在需要时)、进行测试(单元、集成和系统的)、部署和配置/释放管理而具备已记录的过程。
  其次,我将跳过通常吹捧的最佳实践,例如"基于接口的设计"、"使用著名的设计模型"以及"使用面向服务的架构"等。相反,我将集中讲述我曾学过并且使用了若干年的6(不是很多)个方面的in-the-trench课程。最后,本文的目的是让您思考一下自己的架构,提供工作代码示例或者解决方案超出了本文的范围。下面就让我介绍一下这6课:


第1课:切勿绕过服务器端验证
  作为一位软件顾问,我曾有机会不但设计并实现了Web应用程序,而且还评估/审核了许多Web应用程序。在复杂的、并且用JavaScript客户端封装的应用程序内,我经常遇到对用户输入信息执行大量检查的Web页面。即使HTML元素具有数据有效性的属性也如此,例如MAXLENGTH。只有在成功验证所有输入信息后,才能提交HTML表单。结果,一旦服务器端收到通知表单(请求),便恰当地执行业务逻辑。
  在此,您发现问题了么?开发人员已经做了许多重要的假设。例如,他们假设所有的Web应用程序用户都同样诚实。开发人员还假设所有用户将总是使用他们测试过的浏览器访问Web应用程序。还有很多其他的假设。这些开发人员忘记了利用可以免费得到的工具,通过命令行很容易地模拟类似浏览器的行为。事实上,通过在浏览器窗口中键入适当的URL,您可以发送任何"posted"表单,尽管如此,通过禁用这些页面的GET请求,您很容易地阻止这样的"表单发送"。但是,您不能阻止人们模拟甚至创建他们自己的浏览器来入侵您的系统。
根本的问题在于开发人员不能确定客户端验证与服务器端验证的主要差别。两者的主要差别不在于验证究竟发生在哪里,例如在客户端或者在服务器端。主要的差别在于验证背后的目的不同。
  客户端验证仅仅是方便。执行它可为用户提供快速反馈??使应用程序似乎做出响应,给人一种运行桌面应用程序的错觉。
  另一方面,服务器端验证是构建安全Web应用程序必需的。不管在客户端一侧输入的是什么,它可以确保客户端送往服务器的所有数据都是有效的。
  因而,只有服务器端验证才可以提供真正应用程序级的安全。许多开发人员陷入了错误感觉的圈套:只有在客户端进行所有数据的验证才能确保安全。下面是说明此观点的一个常见的示例:
  一个典型的登录页面拥有一个用来输入用户名的文本框和一个输入密码的文本框。在服务器端,某人在接收servlet中可能遇到一些代码,这些代码构成了下面形式的SQL查询:
"SELECT * FROM SecurityTable WHERE username = ’" + form.getParameter("username") + "’ AND password = ’" + form.getParameter("password") + "’;",并执行这些代码。如果查询在结果集的某一行返回,则用户登录成功,否则用户登录失败。
  第一个问题是构造SQL的方式,但现在让我们暂时忽略它。如果用户在用户名中输入"Alice’–"会怎样呢?假设名为"Alice"的用户已经在SecurityTable中,这时此用户(更恰当的说法是黑客)成功地登录。我将把找出为什么会出现这种情况的原因做为留给您的一道习题。
  许多创造性的客户端验证可以阻止一般的用户从浏览器中这样登录。但对于已经禁用了JavaScript的客户端,或者那些能够使用其他类似浏览器程序直接发送命令(HTTP POST和GET命令)的高级用户(或者说黑客)来说,我们又有什么办法呢?服务器端验证是防止这种漏洞类型所必须的。这时,SSL、防火墙等都派不上用场了。

第2课:安全并非是附加物
  如第1课所述,我曾有幸研究过许多Web应用程序。我发现所有的JavaServer Page(JSP)都有一个共同的主题,那就是具有类似下面伪代码的布局:

<%
User user = 
session.getAttribute("User");
if(user == null)
{
// redirect to 
// the logon page…

if(!user.role.equals("manager"))
{
// redirect to the
// "unauthorized" page…
}
%>

<!-
HTML, JavaScript, and JSP
code to display data and
allow user interaction –>

  如果项目使用诸如Struts这样的MVC框架,所有的Action Bean都会具有类似的代码。尽管最后这些代码可能运行得很好,但如果您发现一个bug,或者您必须添加一个新的角色(例如,"guest"或者"admin"),这就会代表一场维护恶梦。
  此外,所有的开发人员,不管您多年轻,都需要熟悉这种编码模式。当然,您可以用一些JSP标签来整理JSP代码,可以创建一个清除派生Action Bean的基本Action Bean。尽管如此,由于与安全相关的代码会分布到多个地方,所以维护时的恶梦仍旧存在。由于Web应用程序的安全是强迫建立在应用程序代码的级别上(由多个开发人员),而不是建立在架构级别上,所以Web应用程序还是很可能存在弱点。
  很可能,根本的问题是在项目接近完成时才处理安全性问题。最近作为一名架构师,我曾在一年多的时间里亲历了某一要实现项目的6个版本,而直到第四版时我们才提到了安全性??即使该项目会将高度敏感的个人数据暴露于Web上,我们也没有注意到安全性。为了更改发布计划,我们卷入了与项目资助人及其管理人员的争斗中,以便在第一版中包含所有与安全相关的功能,并将一些"业务"功能放在后续的版本中。最终,我们赢得了胜利。而且由于应用程序的安全性相当高,能够保护客户的私有数据,这一点我们引以为荣,我们的客户也非常高兴。
  遗憾的是,在大多数应用程序中,安全性看起来并未增加任何实际的商业价值,所以直到最后才解决。发生这种情况时,人们才匆忙开发与安全相关的代码,而丝毫没有考虑解决方案的长期可维护性或者健壮性。忽视该安全性的另一个征兆是缺乏全面的服务器端验证,如我在第1课中所述,这一点是安全Web应用程序的一个重要组成部分。
  记住:J2EE Web应用程序的安全性并非仅仅是在Web.xml 和ejb-jar.xml文件中使用合适的声明,也不是使用J2EE技术,如Java 认证和授权服务(Java Authentication and Authorization Service,JAAS)。而是经过深思熟虑后的设计,且实现一个支持它的架构。

第3课:国际化(I18N)不再是纸上谈兵 
  当今世界的事实是许多英语非母语的人们将访问您的公共Web应用程序。随着电子政务的实行,由于它允许人们(某个国家的居民)在线与政府机构交互,所以这一点特别真实。这样的例子包括换发驾照或者车辆登记证。许多第一语言不是英语的人们很可能将访问这样的应用程序。国际化(即:"i18n",因为在"internationalization"这个单词中,字母i和字母n之间一共有18个字母)使得您的应用程序能够支持多种语言。
  显然,如果您的JSP 页面中有硬编码的文本,或者您的Java代码返回硬编码的错误消息,那么您要花费很多时间开发此Web应用程序的西班牙语版本。然而,在Web应用程序中,为了支持多种语言,文本不是惟一必须"具体化"的部分。因为许多图像中嵌有文字,所以图形和图像也应该是可配置的。在极端的情况下,图像(或者颜色)在不同的文化背景中可能有完全不同的意思。类似地,任何格式化数字和日期的Java代码也必须本地化。但问题是:您的页面布局可能也需要更改。
  例如,如果您使用HTML表格来格式化和显示菜单选项、应用程序题头或注脚,则您可能必须为每一种支持的语言更改每一栏的最小宽度和表格其他可能的方面。为了适应不同的字体和颜色,您可能必须为每一种语言使用单独的样式表。
  显然,现在创建一个国际化的Web应用程序面临的是架构挑战而不是应用程序方面的挑战。一个架构良好的Web应用程序意味着您的JSP页面和所有与业务相关的(应用程序特有的)Java代码都不知不觉地选择了本地化。要记住的教训是:不要因为Java、J2EE支持国际化而不考虑国际化。您必须从第一天起就记住设计具有国际化的解决方案。

第4课:在MVC表示中避免共同的错误 
  J2EE开发已经足够成熟,在表示层,大多数项目使用MVC架构的某些形式,例如Struts。在这样的项目中,我常见到的现象是对MVC模式的误用。下面是几个示例。
  常见的误用是在模型层(例如,在Struts的Action Bean中)实现了所有的业务逻辑。不要忘了,表示层的模型层仍然是表示层的一部分。使用该模型层的正确方法是调用适当的业务层服务(或对象)并将结果发送到视图层(view layer)。用设计模式术语来说,MVC表示层的模型应该作为业务层的外观(Fa?ade)来实现。更好的方法是,使用核心J2EE模式(Core J2EE Patterns)中论述到的Business Delegate模式。这段自书中摘录的内容精彩地概述了将您的模型作为Business Delegate来实现的要点和优点:
  Business Delegate起到客户端业务抽象化的作用。它抽象化,进而隐藏业务服务的实现。使用Business Delegate,可以降低表示层客户端和系统的业务服务.之间的耦合程度。根据实现策略不同,Business Delegate可以在业务服务API的实现中,保护客户端不受可能的变动性影响。这样,在业务服务API或其底层实现变化时,可以潜在地减少必须修改表示层客户端代码的次数。
  另一个常见的错误是在模型层中放置许多表示类型的逻辑。例如,如果JSP页面需要以指定方式格式化的日期或者以指定方式排序的数据,某些人可能将该逻辑放置在模型层,对该逻辑来说,这是错误的地方。实际上,它应该在JSP页面使用的一组helper类中。当业务层返回数据时,Action Bean应该将数据转发给视图层。这样,无需创建模型和视图之间多余的耦合,就能够灵活支持多个视图层(JSP、Velocity、XML等)。也使视图能够确定向用户显示数据的最佳方式。
  最后,我见过的大多数MVC应用程序都有未充分应用的控制器。例如,绝大多数的Struts应用程序将创建一个基本的Action类,并完成所有与安全相关的功能。其他所有的Action Bean都是此基类的派生类。这种功能应该是控制器的一部分,因为如果没有满足安全条件,则首先调用不应该到达Action Bean(即:模型)。记住,一个设计良好的MVC架构的最强大功能之一是存在一个健壮的、可扩展的控制器。您应该利用该能力以加强自己的优势。

第5课:不要被JOPO束缚住手脚
  我曾目睹许多项目为了使用Enterprise JavaBean而使用Enterprise JavaBean。因为EJB似乎给项目带来优越感和妄自尊大的表现,所以有时它是显酷的要素(coolness factor)。而其他时候,它会使J2EE和EJB引起混淆。记住,J2EE和EJB不是同意词。EJB只是J2EE 的一部分,J2EE 是包含JSP、servlet、Java 消息服务(JMS)、Java数据库连接(JDBC)、JAAS、 Java管理扩展(JMX)和EJB在内的一系列技术,同样也是有关如何共同使用这些技术建立解决方案的一组指导原则和模式。
  如果在不需要使用EJB的情况下使用EJB,它们可能会影响程序的性能。与老的Web服务器相比,EJB一般对应用服务器有更多的需求。EJB提供的所有增值服务一般需要消耗更大的内存和更多的CPU时间。许多应用程序不需要这些服务,因此应用服务器要与应用程序争夺资源。
  在某些情况下,不必要地使用EJB可能使应用程序崩溃。例如,最近我遇到了一个在开源应用服务器上开发的应用程序。业务逻辑封装在一系列有状态会话bean(EJB)中。开发人员为了在应用服务器中完全禁用这些bean的"钝化"费了很大的劲。客户端要求应用程序部署在某一商用应用服务器上,而该服务器是客户端技术栈的一部分。该应用服务器却不允许关闭"钝化"功能。事实上,客户端不想改变与其合作的应用服务器的设任何置。结果,开发商碰到了很大的麻烦。(似乎)有趣的事情是开发商自己都不能给出为什么将代码用EJB(而且还是有状态会话bean)实现的好理由。不仅仅是开发商会遇到性能问题,他们的程序在客户那里也无法工作。
  在Web应用程序中,无格式普通Java 对象(POJO)是EJB强有力的竞争者。POJO是轻量级的,不像EJB那样负担额外的负担。在我看来,对许多EJB的优点,例如对象入池,估计过高。POJO是您的朋友,不要被它束缚住手脚。

第6课:数据访问并不能托管O/R映射 
  我曾参与过的所有Web应用程序都向用户提供从其他地方存取的数据,并且因此需要一个数据访问层。这并不是说所有的项目都需要标识并建立这样一个层,这仅仅说明这样层的存在不是隐含的就是明确的。如果是隐含的数据层,数据层是业务对象(即:业务服务)层的一部分。这适用于小型应用程序,但通常与大一些项目所接受的架构指导原则相抵触。
  总之,数据访问层必须满足或超出以下四个标准:
  具有透明性 
  业务对象在不知道数据源实现的具体细节情况下,可以使用数据源。由于实现细节隐藏在数据访问层的内部,所以访问是透明的。
  易于迁移
  数据访问层使应用程序很容易迁移到其他数据库实现。业务对象不了解底层的数据实现,所以迁移仅仅涉及到修改数据访问层。进一步地说,如果您正在部署某种工厂策略,您可以为每个底层的存储实现提供具体的工厂实现。如果是那样的话,迁移到不同的存储实现意味着为应用程序提供一个新的工厂实现。
  尽量减少业务对象中代码复杂性 
  因为数据访问层管理着所有的数据访问复杂性,所以它可以简化业务对象和使用数据访问层的其他数据客户端的代码。数据访问层,而不是业务对象,含有许多与实现相关的代码(例如SQL语句)。这样给开发人员带来了更高的效率、更好的可维护性、提高了代码的可读性等一系列好处。
  把所有的数据访问集中在单独的层上
  由于所有的数据访问操作现在都委托给数据访问层,所以您可以将这个单独的数据访问层看做能够将应用程序的其他部分与数据访问实现相互隔离的层。这种集中化可以使应用程序易于维护和管理。
  注意:这些标准都不能明确地调出对O/R(对象到关系)映射层的需求。O/R映射层一般用O/R映射工具创建,它提供对象对关系数据结构的查看和感知(look-and-feel)。在我看来,在项目中使用O/R映射与使用EJB类似。在大多数情况下,并不要求它。对于包含中等规模的联合以及多对多关系的关系型数据库来说,O/R映射会变得相当复杂。由于增加O/R 映射解决方案本身的内在复杂性,例如延迟加载(lazy loading)、高速缓冲等,您将为您的项目带来更大的复杂性(和风险)。
  为了进一步支持我的观点,我将指出按照Sun Microsystem所普及的实体Bean(O/R映射的一种实现)的许多失败的尝试,这是自1.0版以来一直折磨人的难题。在SUN的防卫措施中,一些早期的问题是有关EJB规范的开发商实现的。这依次证明了实体Bean规范自身的复杂性。结果,大多数J2EE架构师一般认为从实体Bean中脱离出来是一个好主意。
  大多数应用程序在处理他们的数据时,只能进行有限次数的查询。在这样的应用程序中,访问数据的一种有效方法是实现一个数据访问层,该层实现执行这些查询的一系列服务(或对象、或API)。如上所述,在这种情况下,不需要O/R映射。当您要求查询灵活性时,O/R映射正合适,但要记住:这种附加的灵活性并不是没有代价的。
  就像我承诺的那样,在本文中,我尽量避免陈腐的最佳实践。相反,关于J2EE项目中每一位架构师必须做出的最重要的决定,我集中讲解了我的观点。最后,您应该记住:J2EE并非某种具体的技术,也不是强行加入到解决方案中的一些首字母缩写。相反,您应该在适当的时机,恰当的地方,使用合适的技术,并遵循J2EE的指导原则和J2EE中所包含的比技术本身重要得多的实践。

对于这个系列里的问题,每个学Java的人都应该搞懂。当然,如果只是学Java玩玩就无所谓了。如果你认为自己已经超越初学者了,却不很懂这些问题,请将你自己重归初学者行列。内容均来自于CSDN的经典老贴。

问题一:我声明了什么!

String s = "Hello world!";

许多人都做过这样的事情,但是,我们到底声明了什么?回答通常是:一个String,内容是“Hello world!”。这样模糊的回答通常是概念不清的根源。如果要准确的回答,一半的人大概会回答错误。
这个语句声明的是一个指向对象的引用,名为“s”,可以指向类型为String的任何对象,目前指向"Hello world!"这个String类型的对象。这就是真正发生的事情。我们并没有声明一个String对象,我们只是声明了一个只能指向String对象的引用变量。所以,如果在刚才那句语句后面,如果再运行一句:

String string = s;

我们是声明了另外一个只能指向String对象的引用,名为string,并没有第二个对象产生,string还是指向原来那个对象,也就是,和s指向同一个对象。

问题二:"=="和equals方法究竟有什么区别?

==操作符专门用来比较变量的值是否相等。比较好理解的一点是:
int a=10;
int b=10;
则a==b将是true。
但不好理解的地方是:
String a=new String("foo");
String b=new String("foo");
则a==b将返回false。

根据前一帖说过,对象变量其实是一个引用,它们的值是指向对象所在的内存地址,而不是对象本身。a和b都使用了new操作符,意味着将在内存中产生两个内容为"foo"的字符串,既然是“两个”,它们自然位于不同的内存地址。a和b的值其实是两个不同的内存地址的值,所以使用"=="操作符,结果会是false。诚然,a和b所指的对象,它们的内容都是"foo",应该是“相等”,但是==操作符并不涉及到对象内容的比较。
对象内容的比较,正是equals方法做的事。

看一下Object对象的equals方法是如何实现的:
boolean equals(Object o){

return this==o;

}
Object对象默认使用了==操作符。所以如果你自创的类没有覆盖equals方法,那你的类使用equals和使用==会得到同样的结果。同样也可以看出,Object的equals方法没有达到equals方法应该达到的目标:比较两个对象内容是否相等。因为答案应该由类的创建者决定,所以Object把这个任务留给了类的创建者。

看一下一个极端的类:
Class Monster{
private String content;

boolean equals(Object another){ return true;}

}
我覆盖了equals方法。这个实现会导致无论Monster实例内容如何,它们之间的比较永远返回true。

所以当你是用equals方法判断对象的内容是否相等,请不要想当然。因为可能你认为相等,而这个类的作者不这样认为,而类的equals方法的实现是由他掌握的。如果你需要使用equals方法,或者使用任何基于散列码的集合(HashSet,HashMap,HashTable),请察看一下java doc以确认这个类的equals逻辑是如何实现的。

问题三:String到底变了没有?

没有。因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。请看下列代码:

String s = "Hello";
s = s + " world!";

s所指向的对象是否改变了呢?从本系列第一篇的结论很容易导出这个结论。我们来看看发生了什么事情。在这段代码中,s原先指向一个String对象,内容是"Hello",然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个String对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。
通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的内存开销。因为String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用StringBuffer类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。
同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化,把它设置为初始值,应当这样做:
public class Demo {
  private String s;
  …
  public Demo {
    s = "Initial Value";
  }
  …
}
而非
s = new String("Initial Value");
后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。
上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java认为它们代表同一个String对象。而用关键字new调用构造器,总是会创建一个新的对象,无论内容是否相同。
至于为什么要把String类设计成不可变类,是它的用途决定的。其实不只String,很多Java标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以Java标准类库还提供了一个可变版本,即StringBuffer。

问题四:final关键字到底修饰了什么?

final使得被修饰的变量"不变",但是由于对象型变量的本质是“引用”,使得“不变”也有了两种含义:引用本身的不变,和引用指向的对象不变。

引用本身的不变:
final StringBuffer a=new StringBuffer("immutable");
final StringBuffer b=new StringBuffer("not immutable");
a=b;//编译期错误

引用指向的对象不变:
final StringBuffer a=new StringBuffer("immutable");
a.append(" broken!"); //编译通过

可见,final只对引用的“值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。

理解final问题有很重要的含义。许多程序漏洞都基于此—-final只能保证引用永远指向固定对象,不能保证那个对象的状态不变。在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为final,意图使得它“永远不变”。其实那是徒劳的。

问题五:到底要怎么样初始化!

本问题讨论变量的初始化,所以先来看一下Java中有哪些种类的变量。
1. 类的属性,或者叫值域
2. 方法里的局部变量
3. 方法的参数

对于第一种变量,Java虚拟机会自动进行初始化。如果给出了初始值,则初始化为该初始值。如果没有给出,则把它初始化为该类型变量的默认初始值。

int类型变量默认初始值为0
float类型变量默认初始值为0.0f
double类型变量默认初始值为0.0
boolean类型变量默认初始值为false
char类型变量默认初始值为0(ASCII码)
long类型变量默认初始值为0
所有对象引用类型变量默认初始值为null,即不指向任何对象。注意数组本身也是对象,所以没有初始化的数组引用在自动初始化后其值也是null。

对于两种不同的类属性,static属性与instance属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。这个问题会在以后的系列中进行详细讨论。

对于第二种变量,必须明确地进行初始化。如果再没有初始化之前就试图使用它,编译器会抗议。如果初始化的语句在try块中或if块中,也必须要让它在第一次使用前一定能够得到赋值。也就是说,把初始化语句放在只有if块的条件判断语句中编译器也会抗议,因为执行的时候可能不符合if后面的判断条件,如此一来初始化语句就不会被执行了,这就违反了局部变量使用前必须初始化的规定。但如果在else块中也有初始化语句,就可以通过编译,因为无论如何,总有至少一条初始化语句会被执行,不会发生使用前未被初始化的事情。对于try-catch也是一样,如果只有在try块里才有初始化语句,编译部通过。如果在catch或finally里也有,则可以通过编译。总之,要保证局部变量在使用之前一定被初始化了。所以,一个好的做法是在声明他们的时候就初始化他们,如果不知道要出事化成什么值好,就用上面的默认值吧!

其实第三种变量和第二种本质上是一样的,都是方法中的局部变量。只不过作为参数,肯定是被初始化过的,传入的值就是初始值,所以不需要初始化。

问题六:instanceof是什么东东?

instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。举个例子:

String s = "I AM an Object!";
boolean isObject = s instanceof Object;

我们声明了一个String对象引用,指向一个String对象,然后用instancof来测试它所指向的对象是否是Object类的一个实例,显然,这是真的,所以返回true,也就是isObject的值为True。
instanceof有一些用处。比如我们写了一个处理账单的系统,其中有这样三个类:

public class Bill {//省略细节}
public class PhoneBill extends Bill {//省略细节}
public class GasBill extends Bill {//省略细节}

在处理程序里有一个方法,接受一个Bill类型的对象,计算金额。假设两种账单计算方法不同,而传入的Bill对象可能是两种中的任何一种,所以要用instanceof来判断:

public double calculate(Bill bill) {
  if (bill instanceof PhoneBill) {
    //计算电话账单
  }
  if (bill instanceof GasBill) {
    //计算燃气账单
  }
  …
}
这样就可以用一个方法处理两种子类。

然而,这种做法通常被认为是没有好好利用面向对象中的多态性。其实上面的功能要求用方法重载完全可以实现,这是面向对象变成应有的做法,避免回到结构化编程模式。只要提供两个名字和返回值都相同,接受参数类型不同的方法就可以了:

public double calculate(PhoneBill bill) {
  //计算电话账单
}

public double calculate(GasBill bill) {
  //计算燃气账单
}

所以,使用instanceof在绝大多数情况下并不是推荐的做法,应当好好利用多态。

      其实自己是个比较笨的人,碰到事情,也许别人很快就能想通的事情,我可能会不知该如何应对,以致作出些过激的反应,所以,我一直都要求自己,做一件事前,尽可能地想到各种情况,提前预想好该以什么样的心态对待,该以什么样的方式应对,毕竟在心平气和、时间充裕的情况下作出的决定一般都会比较理智。

      最近有所感悟,凡事不能奢求太多,不然,到头来,失望、生气的还是自己。现如今的事,大家为自己考虑大抵多于为别人考虑。自己的付出的努力、心血、感情,能得到半斤八两的回报真的已算是幸运。尤其是在感情上,感情上的投资风险最大,你的一片好心,别人未必真的会领情。反之亦然,别人偶然的举动,也许确会实实在在地伤了你的自尊。所以,凡事想开些,不要以得到回报或善待为标准,不奢求太多,认真做好自己该做的事情就好了。至于别人什么样的反应,并不会影响我做事的评分。这么看来,自己以前的很多烦恼,大抵还是有自身原因的,设错了评判的标准,天真至以为要得到对方的好评、认可才算是做好了事情,的确是幼稚了。处理人际关系不等于讨好别人。所以做好自己份内的事足以,就可以给自己打一个不错的分数,至于对方的反应,那其实是对方份内的事,做法妥善也罢,不对也罢,我只需从中吸取自己可以学习或避免的东西,而并不会影响我行事做人的分数,何苦为了别人的对错影响了自己的心情。

      譬如:朋友困难,向你借钱。你只需想好借给他的理由就好。如果你觉得朋友关系不错,应该帮忙,那你只需估计好风险,将来能够收回本金就好。如果你是想卖份人情给他,以求日后回报,那你要做足分量,让他时时刻刻记得他欠你的这份人情。至于对方的感受,抑或觉得真的感激你,觉得你是个大好人,抑或觉得你我之间不过欠债还钱,将来还了你的钱和人情,就不再相欠。所有这些,还是不要奢求了罢。如果对方感激你,一笑了之,我只是做好我份内的事,抑或我亦有所图。如果对方不这么想,亦无所谓,既然从一开始就未奢望得到别人认可,自然也就不会感到失落。

      至今在想,一个人行事做人的分数,到底是该由别人评判,还是应该由自己评判。

     我越来越意识到,要想得到别人的尊重,得到别人的重视,是不能寄托于别人的素质高尚,自觉自愿的。必须要提高自己。要有自己的经济基础,要有自己的事业,要有自己的生活,自己的发展思路。要独立,绝不能依赖于别人生活,否则你的生活就必须以他为中心而不再能有自己的意志,自己的利益。要变强,最好能够给别人带来利益,这样,别人才会无时无刻不考虑你的存在,考虑你的利益,再不会有人会忽视你的存在,否则,吃亏的只能是他。要懂技巧,要收放自如,给适当的力,让大家都紧紧地围绕在你的周围。

      终于有所感悟:原来,生活是很累的,绝不是少女时所想象的那样自由、美好、充满激情。强大的游戏规则,不容许你不遵从。要想得到你想要过的生活,你必须样样都拿到优秀才能成功。你要专业过硬,又要擅长交际;你要有能力,又要有资本;你要眼光长远,又要脚踏实地;你要待人真诚,同时又要会用手段。与人谈判,既要晓之以理,动之以情,还必须要让对方看到支持我所能得到的好处,反对我所要付出的代价。只善其一,注定会失败。不要以为我善待别人,别人必会善待我,没有了利益的牵制,恐怕对方连一句赞美都会欠奉。这年头,少有人会心甘情愿地为他人做嫁衣的。不晓得保护自己,为自己打算,只傻乎乎地付出,以为理所应当别人会报答自己,等到失望时,只能可怜兮兮,哭哭啼啼,抱怨命苦,抱怨世风日下,人心不古,这种人大抵是不会有人可怜,也不值得可怜地。自己天真幼稚,以为付出必有回报,自己不求上进,到头来不能保护自己。

      这世上的事,事事都是要讲资本。譬如:你想要待人真诚,也是要讲资本的,你有能力保证你的真诚能够对方带来好处或安慰,而不是麻烦或伤害吗?当对方根本对你的真诚不以为然,你有能力让他意识到你的这份真诚的重要吗?当对方想要利用你的真诚,你有能力保证自己的利益吗?如果没有,那么,对不起,你还没有资格,请小心,不要因为自己的天真而害人害己。原来做人如此辛苦。