内容


Struts 2.0 的 OGNL 组件

简介

Struts 2 诞生于 2007 年 2 月初,它拥有很多激动人心的特性。在本文中,我们深入探讨 Struts 2 框架的一个最新组件:Object-Graph Navigation Language (OGNL)。这是一种新型表达式语言,拥有优秀的集合、对象图遍历和索引支持功能。

我们将定义一些基于层级的 web 页面,并展示如何使用最少的 Struts 2 OGNL 框架代码执行数据提取。本文还将讨论如何使用这些构造在 web 上实现重要的业务概念。我们还将演示如何通过此框架的以前版本(Struts 1.x)从这些页面提取数据以及迁移到 Struts 2 的 OGNL 框架如何有助于最小化开发工作量。在这方面,我们将触及 OGNL 和 Struts 2 的基本知识,以及如何利用它们创建一个轻量级方法来处理这些层级页面。我们还对 Struts 1.x 和 Struts 2 的特性进行了比较研究,这将有助于您迁移到一个需要更少编码的环境。

案例 1. 一个现有场景研究

我们仔细检查下面这个场景:一个 web 页面中的层级数据显示和数据更新。一位投资顾问正在为客户准备一份报告。一个屏幕快照显示客户的投资组合的数据更新屏幕。数据输入字段是投资组合的顶级属性。紫色的行是投资组合之下的第一级数据项,字段 Target Investment Amount 和 Target Profit% 是每个第一级数据项的属性。类似地,绿色的行是第二级数据项(之下的字段是该级别的属性)。最后,黄色的行是第三级数据项。本文假定所有字段都可以更新,当用户提交页面时,后端的数据将自动更新。还要注意一点,第一、第二和第三级的行的数量本质上是动态的。

图 1. 图释
图形显示数据库中的各数据行之间的关系
图形显示数据库中的各数据行之间的关系

解决问题的当前实现(使用 Struts 1.x)

清单 1 显示上述页面的数据模型。

清单 1. 早期数据结构
public class ClientPortfolioDetailsBean 
{
	//Domain model for the client portfolio details
	
	//Portfolio level attributes
	private String investorName = null;
	private String emailId = null;
	private String contactNumber = null;
	//..... Other portfolio level attributes
	//List to hold the details of Stocks / Mutual Funds etc investment category 
	private List <InvestmentCategoryDetailsBean> invCategoryList = null;
}
public class InvestmentCategoryDetailsBean {
	
	//Bean to hold the information about investment category details
	//like Stocks, Mutual Funds etc. Represents the first level rows
	private String categoryName = null;
	private double targetInvstAmt  = 0.0;
	private double expectedAnnualReturn   = 0.0;
	//Other attributes follows 
	//List of stocks and fund details bean to hold the 
	//second level items 
	private List <StocksAndFundDetailsBean> itemList = null;
}
public class StocksAndFundDetailsBean {
	
	//Bean to hold the information about investment category details
	//like Stocks, Mutual Funds etc. Represents the second level rows
	
	//Attributes at that level
	private String stockFundCategoryName = null;
	private double targetInvstAmt = 0.0;
	private double targetProfit =0.0;
	//Other attribute follows
	//List to hold the stock details 
	private List <StocksDetailsBean> stockDetailsList = null;
}
public class StocksDetailsBean {
	
	//Represents the third level item details
	//Details of individual stock/fund details
	private String stockFundName = null;
	private double noOfStocks = 0.0;
	private double buyingPrice =0.0;
	private Date buyingDate = null;
}

在 Struts 1.x 中,第一个案例研究的解决方案需要以一种扁平的单级方式定义一个表单 bean。要获取用户输入的更新值,清单 2 提供了必要的表单 bean 结构。

清单 2. Struts 1.x 表单 beans 的结构
public class PortfolioDetailsBean extends ActionForm

{

	//Top level attributes 
	
	private String investorName = null;
	private String emailId = null;
	//........... Other Top level attributes 
	
	//Fields to capture Attributes of First level rows 
	private double[] firstLevelTargetAmt = null;
	private double[] firstLevelExpectedAnnlReturn = null;
	//......... Other First Level row attributes

	//Fields to capture Attributes of Second level rows 
	private double[] secondLevelTargetAmt = null;
	private double[] secondLevelTargetInvPeriod = null;
	//......... Other Second level row attributes

	//Fields to capture Attributes of Second level rows 
	private double[] thirdLevelNoOfStocks = null;
	private double[] thirdLevelByingPrice = null;
	//......... Other Third level row attributes
}

要将这些单级表单 bean 元素转换为 清单 1 中显示的多级域模型,则操作类需要额外编码,如清单 3 所示。注意,这里适用的逻辑取决于域模型的结构,随着属性的级别和数量增加,操作类中的转换的复杂性也相应增加。

清单 3. 将单级表单 bean 元素转换为多级域模型的定制代码
public class UpdatePortfolioAction extends Action

{

    public ActionForward execute(ActionMapping mapping, ActionForm form,
	    HttpServletRequest request, HttpServletResponse response)
	    throws Exception {

	ActionErrors errors = new ActionErrors();
	ActionForward forward = new ActionForward(); // return value
	PortfolioDetailsBean portfolioDetailsBean = (PortfolioDetailsBean) form;

	
	try {
		//Counter to track index of 2nd level index
		int counter2ndLevelIndex= 0;
		//Converting the form bean to the domain model 
		ClientPortfolioDetailsBean clientBean = new ClientPortfolioDetailsBean();
		//Setting the top level attributes 
		clientBean.setEmailId(portfolioDetailsBean.getEmailId());
		//.........................................
		//Similarly other top level fields are created 
		//Get the size of first level row sizes from session
		int sizeInvestmentCategory = getInvestmentCategorySize
           (request.getSession());
		clientBean.setInvCategoryList(new ArrayList
           <InvestmentCategoryDetailsBean>(sizeInvestmentCategory));
		for(int firstLevelIndex=0;firstLevelIndex<
           sizeInvestmentCategory;firstLevelIndex++)
		{
			//Create the updated first level bean
			InvestmentCategoryDetailsBean invCatBean = 
               new InvestmentCategoryDetailsBean();
			//Set the investment category Bean in the parent bean
			clientBean.getInvCategoryList().add(invCatBean);
			//set the updated properties 
			invCatBean.setExpectedAnnualReturn(
               portfolioDetailsBean.getFirstLevelExpectedAnnlReturn()[firstLevelIndex]);
			//.........................................
			//Set the other properties at first level 
			//Get the size of 2nd level row sizes from session
	        int sizeStockAndFundDetails = getStockAndFundDetailsSize
               (request.getSession(),firstLevelIndex);
              	invCatBean.setItemList(
                     new ArrayList<StocksAndFundDetailsBean>(sizeStockAndFundDetails));
			//Fill Up the second level objects
	        for(int secondLevelIndex=0;secondLevelIndex<
               sizeStockAndFundDetails;secondLevelIndex++)
			{
	StocksAndFundDetailsBean stkFundDetails = 
	   new StocksAndFundDetailsBean();
    //Set the object to the parent Bean
    invCatBean.getItemList().add(stkFundDetails);
    //Set the individual attributes 
	stkFundDetails.setTargetInvstAmt(
       portfolioDetailsBean.getSecondLevelTargetAmt()[counter2ndLevelIndex++]);
       //............... Other fields follows
	   // Setting up of 3rd level list follows here
	   //......................................
	}			
   }		
} catch (Exception e) {

   // Report the error using the appropriate name and ID.

   }
}

当前场景的缺点

开发 web 应用程序中的一个枯燥乏味的任务是将数据从表单 beans 传输到数据 beans。从 Strings 转换到 Java 类型增加了复杂性:需要将字符串值解析为双精度小树或整数,还需要处理坏数据可能导致的异常。

数据传输和类型转换在请求处理循环的两端发生。显示结果时,需要将数据从 Java 类型再转换回字符串格式。web 应用程序中的几乎每个请求都需要执行这个过程,这是这个域不可或缺的一部分。支持包含动态记录的动态表单需要复杂的代码和逻辑。这样的逻辑代码的开发和维护都很复杂。

Struts 2 和 OGNL

数据传输和数据转换自动化是 Struts 2 最强大的特性之一。借助 OGNL,Struts 2 框架允许将数据传输到更复杂的 Java 端类型,比如 List 和 Map 等。还可以开发自定义转换程序来扩展类型转换机制,处理任何数据类型,甚至包括用户定义的类型。

OGNL 是 Struts 2 框架的基于字符串的 HTTP Input and Output 与基于 Java 的内部处理功能之间的接口。对于熟悉 OGNL 的开发人员而言,OGNL 将极大地提高工作效率并减少维护工作。

图 2. OGNL 栈
显示各种上下文元素映射到 OGNL 结构的示意图
显示各种上下文元素映射到 OGNL 结构的示意图

从开发人员的角度看,OGNL 拥有两个组件:

  • 表达式语言 — 通常用于表单输入字段名称和 JSP 标记。OGNL 表达式用于将 Java 端数据属性绑定到基于文本的视图层中的字符串。
  • 类型转换程序 — 负责数据类型转换。每当数据移向或来自 Java 环境,HTML 中驻留的数据必须从字符串形式转换为适当的 Java 数据类型。此框架提供一些内置转换程序来提供强大的转换功能,开发人员也可以选择创建自定义转换程序。

尽管此框架自动传输数据并转换来自请求参数的数据,但数据将前往何方,OGNL 如何确定目标呢?Struts 2 中的 ValueStack 构造是多个对象的属性的混合体,就像单个虚拟对象的多个属性。如果存在相同的属性(堆栈中的两个对象都拥有 employeeId 属性),将使用堆栈中位置最高的对象的属性。Struts 2 框架内置支持 HTTP 原生字符串和以下 Java 类型之间的转换:

  • Array
  • boolean/Boolean
  • Char/Character
  • int/Integer, float/Float, long/Long, double/Double
  • Date
  • String
  • Map
  • List

使用 OGNL 解决转换难题

Struts 2 OGNL 表达式减小了编写复杂代码、将单级表单 beans 转换为多级域对象的难度。在 Struts 2 中,HTML 输入字段名称可以生成为 OGNL 表达式,后者有助于消除操作类中的复杂编码。举个例子,如果一个 HTML 输入字段名称(比如一个文本框)被设置为 clientPortfolioDetailsBean.investorName,那么将调用操作类的 getClientPortfolioDetailsBean()。这应该会返回 ClientPortfolioDetailsBean,然后使用用户在屏幕中输入的值调用 setInvestorName() 方法。用户更新的 HTML 输入字段的智能命名消除了操作类中的复杂编码。操作类中的域模型将被适当填充。

需要在操作类中添加属性类型 ClientPortfolioDetailsBean 及其 get 方法。在 get 方法中,如果某个 bean 的实例没有在当前操作类实例中设置,那么应该从会话返回那个 bean。在页面加载过程中,包含客户端细节的 bean 实例应该保存在会话对象中。如果遇到更大的对象,getClientDetailsFromSession() 方法可以替换为 getClientDetailsFromDB(),后者将从数据库获取数据。

清单 4. 案例 1 的代码段
public class ClientPortfolioAction extends ActionSupport {

	private ClientPortfolioDetailsBean clientDetailsBean = null;
		
	//Accessor for the clientDetailsBean. 
	public ClientPortfolioDetailsBean getClientDetailsBean() {
		if(clientDetailsBean==null)
		{
			//If the bean is null for this action class instance
			//load the data from the session 
			clientDetailsBean = getClientDetailsFromSession();
		}
		return clientDetailsBean;
	}
}
清单 5. 案例 2 的代码段
<%@ taglib prefix="s" uri="/struts-tags" %>

	<s:textfield name="clientDetailsBean.investorName" 
value="%{clientDetailsBean.investorName}"/%>
	<s:textfield name="clientDetailsBean.emailId" 
value="%{clientDetailsBean.emailId}"/%>
	<s:textfield name="clientDetailsBean.contactNumber" 
value="%{clientDetailsBean.contactNumber}"/%>
		
	<s:iterator value="clientDetailsBean.invCategoryList" 
var="invCatDetails" status="firstLevelIdx"%>
		 
		<s:property value="%{#invCatDetails.categoryName}"/%>
		 
	<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.targetInvstAmt" 
value="%{invCatDetails.targetInvstAmt}"/%>
								
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.expectedAnnualReturn" 
value="{invCatDetails.expectedAnnualReturn}"/%>
											
			<s:iterator value="#invCatDetails.itemList"
var="stkFundDetails" status="secondLevelIndx"%>
				
			<s:property value="%{#stkFundDetails.
stockFundCategoryName}"/%>
	<s:textfield name="clientDetailsBean
[%{#firstLevelIdx.index}].itemList[%{secondLevelIndx.index}].
targetInvstAmt" value="%{#stkFundDetails.targetInvstAmt}" /%>
	<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].
targetProfit" value="%{#stkFundDetails.targetProfit}" /%>
											
	<s:iterator value="#stkFundDetails.stockDetailsList" 
var="stkDetails" status="thirdLevelIndx"%>
	<s:property value="%{stkDetails.stockFundName}" /%>
	<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].stockDetailsList
[%{thirdLevelIndx.index}].noOfStocks" value="%{#stkDetails.noOfStocks}" /%>
	<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].stockDetailsList
[%{thirdLevelIndx.index}].buyingPrice" value="%{#stkDetails.buyingPrice}"/%>
					
				</s:iterator>				
			</s:iterator>
		
	</s:iterator>

下面我们来看一个 JSP 页面,其中,输入文本框名称根据有效的 OGNL 表达式生成,“ABC Incorporated” 的 “no of Stocks field” 字段通过 OGNL 表达式更新:

                <s:textfield
                name="clientDetailsBean[%{#firstLevelIdx.index}]
                .itemList[%{secondLevelIndx.index}].stockDetailsList
                [%thirdLevelIndx.index}].noOfStocks}'/>

这一行等同于以下 HTML 代码:

<input type="text"
                name="clientDetailsBean[0].itemList[0].stockDetailsList[0].noOfStocks"
                value="<somevalue>" />

OGNL 表达式 clientDetailsBean[0].itemList[0].stockDetails[0].noOfStocks 等同于操作类实例上的以下代码调用: getClientDetailsBean().getItemList().get(0).getStockDetailList(0).get(0).setNoOfStocks(<some value>)。类似地,域模型中的其他字段将相应更新。在操作方法中,如果 getClientDetailsBean() 被调用,所有字段将被 Struts 2 框架更新。这避免了在操作类中进行复杂编码(如 Struts 1 示例所示),将表单 bean 转换为相关域模型对象。

当前场景的优点

Struts 2 框架的优点包括:

  • OGNL 的表达式语言支持将表单字段映射到 Java 端属性。
  • OGNL 的类型转换程序自动将来自请求参数的数据(字符串格式)转换为实际 Java 类型属性。
  • 显示视图时,表达式语言和类型转换程序再次将数据从 Java 类型自动转换回字符串。
  • 支持到(从) Collection 和 Array 进行灵活多样的转换。
  • 允许减小后端代码的复杂性,轻松构建复杂网站。
  • 通过可重用的自动数据传输减少开发时间。
  • 极大地减少用于数据转换和异常处理的繁琐代码,从而专注于核心业务逻辑。

结束语

本文介绍了 Struts 2 的 OGNL 组件,以及如何使用该组件构建更简化的业务应用程序。在后面的文章中,我们将从开发视角审视 Struts 2。之所以要在这里探讨 Struts 2,是因为它正在变得日益稳定,我们希望能帮助您最大限度利用 Struts 2 的增强特性。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=650544
ArticleTitle=Struts 2.0 的 OGNL 组件
publish-date=04252011