利用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應用服務器來組織建設網站的心得和體會。大家的項目類型也許會有不同,但開發的模式總會有相通之處。善于根據實際情況選擇開發模式,可以提高開發效率和代碼質量,但也不要一味死抱著模式不放,量體裁衣才能游刃有余。