利用J2EE模式构建网站
作者:李志
 

一、前言

    本文以一个教学网站的建设思路为例,探讨利用J2EE技术和WebSphere产品构建网站的模式和方法。

二、设计网站系统

    我们的样例是一个教学网站系统,它的软件包括WebSphere Application Server应用服务器软件V4.0、WSAD开发工具和DB2数据库(非商业用途),硬件为IBM xSeries服务器。在本文中,主要探讨MVC的开发模型和常用的J2EE模式,关于网站建设的其他细节就略过不提了。

(一)系统用例图

    分析网站的系统目标后,我们首先具体化系统功能,形成一张用例图,定义一系列的可重构组件,以指导随后的开发工作。

 图1 Use Case picture
图1 Use Case picture

 

(二)组件化设计

    在构造网站系统时,我们把每层的系统想象成拥有多个"槽"的装置,开发人员可以向槽中插入组件以扩大其能力,也可以通过继承或其他机制具体化组件系统。这些组件可以是开发人员为该应用系统开发的,也可以是以前开发好的复用组件。在这种分层体系结构中,每个应用系统都表示为一个单独的系统。每个系统都采用组件构造。每个组件系统又可以通过其他下层组件系统构造。重构人员采用一组与特定应用系统领域和业务有关的组件或顶层中的组件系统来构造每个应用系统。

    结合J2EE,让我们首先了解J2EE体系中的组件构成情况,如图2所示。

图2  J2EE组件打包策略
图2 J2EE组件打包策略

 

    在上图中可以看出,不同的组件归档到不同的文件包中,这样就保证了一个组件的"插拔"不会影响到其它的组件。根据应用系统组件的功能,我们可以把它们分为动态组件和静态组件。静态的组件包含网页文件,主要用于放置教学资料和参考文章。动态组件则包括各种功能模块,如论坛系统、模拟测验系统等。

    应用系统组件之下是于特定业务有关的组件。在这里,我们可以添加非Java编写的一些程序,用于处理特定内容下的操作,比如模拟测验系统中的出题模块。当然,要考虑到上层组件调用的正面接口问题。对于这层组件,我们能够随时替换,只要其提供的数据符合上级正面的要求。

    以上两级组件之下是J2EE应用服务器和操作系统,整体如图3所示。

图3 计算机组成原理网站系统组件架构图
图3 计算机组成原理网站系统组件架构图

 

(三)利用J2EE模式开发组件系统

    下面着重介绍开发过程中使用的J2EE模式,这些模式都是通用类型的。

    本系统采用MVC开发模型,即Model-View-Controller。Model是指应用程序的数据,以及对这些数据的操作;View是指用户界面;Controller负责用户界面和程序数据之间的同步。这种模型的好处在于分离不同功能的代码,便于以后的维护,还有利于在项目小组内按照小组成员各自的擅长进行分工,有利于三个部分并行开发、加快项目进度。

    为了使各开发人员协调一致,为其他组件提供一致和标准的正面,增强系统的可维护性和可复用性,我们广泛采用了SUN公司提出的基于MVC的设计模式。

    图4是用户注册模块的UML图,我们将结合这个模块具体阐述各模式的特点和在本系统中的实际应用。

图4 表示层模式
图4 表示层模式

 

    明确了所采用的体系和模式,下面具体设计类的属性和方法,通过设计完善的接口和继承、重载等方法进行重构。模块的UML的类图表示如下:

图5 模块的UML的类图
图5 模块的UML的类图

 

结合上图,让我们看看这个模块中都运用了哪些模式。

1. 表示层模式

    系统的表示层集中了MVC模式中的View与Controller。该系统用JSP代表View,用Servlet代表Controller。在Controller这一模块中,又采用了视图助手、分发者与值对象模式,以增强系统的模块化,提高维护性。

(1)前端控制器

控制器通常表现为Servlet形式,其UML表示如下:

图6 前端控制器
图6 前端控制器

 

    根据Model-View-Controller的开发思想,使用控制器作为处理请求的最初联系点。该控制器管理着请求的处理,包括调用安全服务,比如验证和授权、委托业务处理、管理合适的视图选择、处理错误,以及管理内容创建逻辑的选择。也可以把前端控制器看成一个触发器,由它来启动流程。

    下面是功能代码的样本。其中出现的RegisterHelper、Command等类,接下来会有详细介绍。

   public void performTask(javax.servlet.http.HttpServletRequest request,
		javax.servlet.http.HttpServletResponse response)
		throws javax.servlet.ServletException, java.io.IOException 
   {
   	   RegisterHelper rh=new RegisterHelper(request,response);//启动注册视图助手
   	   Command command=rh.getCommand();//由视图助手中获得并初始化Command
   	   CustomerBean cb=rh.getCustomerBean();//由视图助手中获得并初始化值对象
   	   request.setAttribute("customerbean",cb);
   	   String dispatcher=rh.getDispatcher();//由视图助手中获得并初始化分发者
	   request.setAttribute("type",rh.getType());//设置上下文属性
   	   try {
   	    command.execute((Helper)rh);//执行业务代码
   	   } catch(javax.ejb.DuplicateKeyException de) {
		request.setAttribute("errorbean",new ErrorBean("对不起,已经有人注册了该用户名!"));//注册重名处理
		dispatch(request,response,dispatcher);//分发并移交控制权
		return;
   	   } catch(Exception e) {
   		request.setAttribute("errorbean",new ErrorBean("对不起,数据库出错!"));//出错处理
		dispatch(request,response,dispatcher);
		return;
   	}
   	   dispatch(request,response,dispatcher);
    
   }
   

    优点:通过集中化决策点和控制,控制器有助于减少嵌入在JSP中Java代码(Scriptlet)的数量,保持View功能的纯洁性。它的位置如图5中Controller所示。

(2)视图助手

    表示层更改经常发生,而且当业务数据访问逻辑和表示格式化逻辑被混杂时,表示层更改很难开发和维护。这使系统灵活性更差,更缺乏可用性,而且对变化的适应性更弱。

图7 视图助手
图7 视图助手

 

    视图包含格式化代码,把其处理责任委托给其助手类。助手也存储该视图的中间数据,如表单、URL参数等,并且充当业务数据适配器。

下面是功能代码的样本。

public class RegisterHelper implements Helper {

	static String dispatcher = "RegisterDispatcher";
	private CustomerBean customer = null;
	private String type = null;
	
	public RegisterHelper(
		javax.servlet.http.HttpServletRequest request,
		javax.servlet.http.HttpServletResponse response) {
		setType(request);
		setCustomerBean(request);		
	}

	/**
	 * 定义页面类型:HTML or XML
	 */
	public void setType(javax.servlet.http.HttpServletRequest request) {
		type = request.getParameter("type");
	}

	/**
	 * 获取Command
	 */
	public Command getCommand() {
		RegisterCommand rc = new RegisterCommand();
		return rc;
	}

	/**
	 * 向值对象中填充数据
	 */
	public void setCustomerBean(javax.servlet.http.HttpServletRequest request) {
		customer = new CustomerBean();
		customer.setUsername(request.getParameter("username"));
		customer.setPassword(request.getParameter("password"));
		customer.setEmail(request.getParameter("email"));
		customer.setTruename(request.getParameter("truename"));
		customer.setId(request.getParameter("id"));
		customer.setService(this.setService(request));
	}
	/**
	 * 获取值对象
	 */

	public CustomerBean getCustomerBean() {
		return this.customer;
	}

	/**
	 * 获取分发者
	 */
	public String getDispatcher() {
		return this.dispatcher;
	}

	/**
	 * 获取类型
	 */
	public String getType() {
		return type;
	}
}

    优点:在助手中而不是在视图中封装业务逻辑会增强应用程序的模块化,并且更有利于组件重用。助手有大量的责任,包括收集视图和控制需要的数据,以及存储中间模型。它的位置如图5中Helper所示。

(3)Command模式

    Command中包含纯业务代码,如注册、登陆、检验等。在样例模块中,它的职责是将注册信息传递给Entity Bean。

图8 Command
图8 Command

 

功能代码如下所示:

      public void execute(Helper helper) throws Exception 
   {
      RegisterHelper rh = (RegisterHelper) helper;//获取视图助手
		CustomerBean cb = rh.getCustomerBean();//从视图助手中获取值对象
		ServiceLocator sl = ServiceLocator.getInstance();//初始化服务定位器
		CustomersHome ch = (CustomersHome) sl.getHome(ServiceLocator.Services.CUSTOMERS);//从服务定位器中获取Entity Bean本地接口
		try {	
			Customers customers = ch.create(cb);//将注册信息导入数据库
		} catch(javax.ejb.DuplicateKeyException e) {
			throw new javax.ejb.DuplicateKeyException();
		} catch(Exception e) {
			throw e;
		}
	
   }
   

(4)分发者模式

    如果将表示化逻辑和业务逻辑混合在视图中,会使系统可重用性和灵活性变差,而且一半还会使更改操作难以实施。分发者负责视图管理和导航,选择下一个视图,并且提供分发资源控制的机制。分发者可以提供静态的分发,也能提供更高级的动态分发机制。

    在我们的项目中,由于涉及到PC用户和移动手机用户的访问,我们需要针对不同的用户返回不同的结果页面,因此分发者的存在就非常有必要。分发者表现为Servlet形式,它承接Controller的处理结果,并判断用户的类型,把正确的视图返回给用户。它的位置如图5中Dispatcher所示。

图9 分发者
图9 分发者

 

功能代码如下所示:

		public void performTask(
		javax.servlet.http.HttpServletRequest request,
		javax.servlet.http.HttpServletResponse response)
		throws javax.servlet.ServletException, java.io.IOException {
		String type=(String)request.getAttribute("type");//获取页面类型
		isError=(request.getAttribute("errorbean")!=null)?true:false;//判断是否出错
		String file=selectType(type,isError,response);//根据页面类型和是否出错确定显示页面
		getServletConfig().getServletContext().getRequestDispatcher(file).forward(request,response);//重定向到显示页面
	}
	
	public String selectType(String str,boolean isError,javax.servlet.http.HttpServletResponse response) {
		if (str.equals("html")) {//HTML类型的页面
			if (isError) {//成功
				System.out.println("Some error happens!");
				return "register_error.jsp";
			} else {//出错
				return "register_ok.jsp";
			}
		} else {//WML手机页面
			if (isError) {//成功
				System.out.println("Some error happens!");
				return "wml/register_error.jsp";
			} else {//出错				
				response.setContentType("text/vnd.wap.wml;charSet=gb2312");
				return "wml/register_ok.jsp";
			}
		}
	}
	

(5)复合视图

    复杂的Web页面可以展示来自多个数据源的内容,使用多个包含单显示页面的子视图。同时,具有不同技能的多个开发人员可以参与这些Web页面的开发和维护。

    因此,我们采用有多个原子视图组成的复合视图。模版中每个组件是动态结合在一起的,并且页面的布局是独立于内容进行管理的。在我们的项目中,采用了<jsp:include page="***" flush="true">嵌入页面,使导航栏和标示独立于各个页面,使用户能够及时地看到任何变动,并使系统更改的代价降低到最小。

2. 业务层模式

(1)值对象

    J2EE应用程序把服务器端业务组件实现为Session Bean和Entity Bean。业务组件的一些方法可以向客户端返回数据。通常,客户端需要多次调用业务对象的get/set方法直到获得所有的属性值。由于EJB的调用采用RMI-IIOP方式通过网络进行,这样做大大延缓了业务层的处理速度,降低了效率。

    为了解决这一问题,我们使用值对象封装业务数据。相应的方法调用是设置和检索值对象。当客户端向企业bean请求业务数据时,该企业bean可以构造值对象,用属性值来填充,并且按照值把它传递给客户端,这也符合EJB端粗粒度调用的需要。

    值对象是可串行化的JavaBean对象。值对象类也可以提供接收所有必须的属性以创建该值对象的构造器。通常,值对象中的成员被定义为私有的,而其Get/Set方法则是公有的。

图10值对象
图10值对象

 

如图10所示,作为示例的CutomerBean中包含了对应Entity Bean所需的属性及其访问方法。

(2)服务定位器

    J2EE客户端与EJB组件进行交互,这些组件提供业务服务和持久性能力。为了与它们交互,客户端必须定位(或称为查找)该服务组件,或创建一个新的组件。比如,EJB客户端必须定位EJB的本地对象,然后客户端使用该本地对象来查找某对象,或者创建或删除一个或多个EJB。

    这样,对于所有需要访问JNDI管理的服务对象的客户端而言,都需要进行定位工作。在需要查找服务的客户端中,会导致不必要的代码重复现象。同时,创建最初JNDI环境和在EJB本地对象上执行查找都会占用大量的资源。如果多个客户端反复地请求相同的bean本地对象,这种重复现象会严重影响应用程序性能。

    我们使用服务定位器对象来抽取所有的JNDI应用,并且隐蔽最初环境创建、EJB本地对象查找和EJB对象重创建的复杂性。多个客户端可以重新使用服务定位器对象来降低代码的复杂性,提供单控制点,并且通过提供缓冲机制来提高性能。

    该模式降低了来自于客户端依赖性的客户端复杂性,并且需要执行查找和创建过程,这些都是非常消耗资源的。为了消除这些问题,该模式提供了把所有依赖性和网络细节抽取到服务定位器的一种机制。它的位置如图11中ServiceLocator所示。

图11 服务定位器
图11 服务定位器

 

功能代码如下所示:

public class ServiceLocator 
{
   	private static ServiceLocator me;
	InitialContext context = null;
   /**
    * 初始化上下文
    */
   public ServiceLocator() 
   {
		try {
			context = new InitialContext();
		} catch (NamingException e) {
			e.printStackTrace();
		}    
   }
	public class Services {
		//为EJB设定请求序号
		final public static int CUSTOMERS=0;
		final public static int PARTNERS=1;
		final public static int ADMINISTRATORS=2;
		final public static int PERMITS=3;
		final public static int PAPERBROKER=4;
		final public static int CHECK=5;
	}
	final static Class CUSTOMERS_CLASS=CustomersHome.class;
	final static String CUSTOMERS_NAME="CustomersHome";
	final static Class PARTNERS_CLASS=PartnersHome.class;
	final static String PARTNERS_NAME="PartnersHome";
	final static Class ADMINISTRATORS_CLASS=AdministratorsHome.class;
	final static String ADMINISTRATORS_NAME="AdministratorsHome";
	final static Class PERMITS_CLASS=PermitsHome.class;
	final static String PERMITS_NAME="PermitsHome";
	final static Class PAPERBROKER_CLASS=PaperBrokerHome.class;
	final static String PAPERBROKER_NAME="PaperBrokerHome";
	final static Class CHECK_CLASS=CheckHome.class;
	final static String CHECK_NAME="CheckHome";
	public static ServiceLocator getInstance() {//单线程处理以节省资源
		if (me == null)
			me = new ServiceLocator();
		return me;
	}   
	static private Class getServiceClass(int service) {
		switch(service) {
			case Services.CUSTOMERS:
				return CUSTOMERS_CLASS;
			case Services.PARTNERS:
				return PARTNERS_CLASS;
			case Services.ADMINISTRATORS:
				return ADMINISTRATORS_CLASS;
			case Services.PERMITS:
				return PERMITS_CLASS;
			case Services.PAPERBROKER:
				return PAPERBROKER_CLASS;
			case Services.CHECK:
				return CHECK_CLASS;
		}
		return null;
	}
	static private String getServiceName(int service) {
		switch(service) {
			case Services.CUSTOMERS:
				return CUSTOMERS_NAME;
			case Services.PARTNERS:
				return PARTNERS_NAME;
			case Services.ADMINISTRATORS:
				return ADMINISTRATORS_NAME;
			case Services.PERMITS:
				return PERMITS_NAME;
			case Services.PAPERBROKER:
				return PAPERBROKER_NAME;
			case Services.CHECK:
				return CHECK_NAME;
		}
		return null;
	}
   /**
    * 返回EJB本地接口
    */
   public EJBHome getHome(int s) 
   {
    	EJBHome home = null;
		try {
			Object objref = context.lookup(getServiceName(s));
			home = (EJBHome) PortableRemoteObject.narrow(objref, getServiceClass(s));

		} catch (NamingException e) {
			e.printStackTrace();
		}
		return home;
   }
}

缺点:如果增加新的EJB,需要修改服务定位器的代码。

    以上这些模式都是可以通用的,在实际应用中运用这些模式,不仅加快了开发进度,而且开发人员各司其职,避免了代码的混乱,取得了比较好的效果。

三、总结

    本文记述了根据J2EE模式和MVC开发模型,利用IBM公司的WebSphere应用服务器来组织建设网站的心得和体会。大家的项目类型也许会有不同,但开发的模式总会有相通之处。善于根据实际情况选择开发模式,可以提高开发效率和代码质量,但也不要一味死抱着模式不放,量体裁衣才能游刃有余。