第24章 近距离接触SpringMVC主要角色(一)

第24章 近距离接触SpringMVC主要角色

本章内容

  • 忙碌的协调人HandlerMapping

  • 我们的亲密伙伴Controller

  • ModelAndView

  • 视图定位器ViewResolver

  • 各司其职的View

要恭喜你的是,即使不再继续余下的旅程,你也已经能够处理Spring MVC Web应用开发过程中80%的场景,但却可能不得不重复处理Web开发过程中的某些方面,比如Locale的设置、数据的绑定和验证、form页面的显示和提交等。 为了能够避免每个处理流程都需要实现这些东西,SpringMVC提供了更细粒度的组件支持,而这也将带给我们更加舒适的开发体验。虽然我们同样可以实现这么一套组件体系,但已经有先行者替我们做了这部分工作,岂不是更好?

如果你想进一步避免Web开发过程中的某些重复工作,如果你想“坐享其成”(好像不是个好词儿哦),如果你想了解Spring MVC框架的更多奥秘,让我们继续出发!

24.1 忙碌的协调人HandlerMapping

HandlerMapping帮助DispatcherServlet进行Web请求的URL到具体处理类的匹配。 之所以称为HandlerMapping是因为,在Spring MVC中,并不只局限于使用org.springframework.web.servlet.mvc.Controller作为DispatcherServlet的次级控制器来进行具体的Web请求的处理。实际上,在稍后介绍HandlerAdaptor的时候,你就会了解到,我们也可以使用其他类型的次级控制器,包括SpringMVC提供的除了Controller之外的次级控制器类型,或者第三方Web开发框架中的PageController组件(如Struts的Action),而所有这些次级控制器类型,在Spring MVC中都称作Handler ,我想这就是HandlerMapping这一名称的由来了。

HandlerMapping要处理的也就是Web请求到相应Handler之间的映射关系。 如果你接触过Struts框架的话,可以将HandlerMapping与Struts框架的ActionMapping概念进行类比。只不过HandlerMapping的职贵更加明确,使用上也更加灵活。

HandlerMapping的定义很简单,因为大部分实际工作都是由其相应的子类来完成的,其定义如下所示:

public interface HandlerMapping {
    
    
	String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() +
	  ".pathWithinHandlerMapping";
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

刚看到该接口的定义你或许会说我糊弄人,“HandlerMapping返回给DispacherServlet的,哪里是一个Controller啊?明明是一个HandlerExecutionChain嘛”。各位毋需动怒,HandlerMapping接口定义的确如此,不过我所言也非虚啊,只不过是为了简化概念而已。

我想,各位不想还没了解Spring MVC大体是什么东西的时候,就吃上一顿“概念大餐”吧?即使现在,我也不打算带大家详细了解这个返回的HandlerExecutionChain到底是个什么东西。目前为止,只需要大家知道,HandlerExecutionChain中确实包含了用于处理具体Web请求的Handler ,仅此而已。

24.1.1 可用的HandlerMapping

Spring MVC默认提供了多个HandlerMapping的实现供我们选用,这当然包括我们在最初所接触的BeanNameUrlHandlerMapping,除此之外,还有如下几个。

  • SimpleUrlHandlerMapping。较之BeanNameUrlHandlerMapping,该实现类进一步解除了请求URL与Handler的beanName之间的耦合,并且支持更灵活的映射表达方式。
  • ControllerClassNameHandlerMapping。我们将在第27章中接触到它。
  • DefaultAnnotationHandlerMapping。Spring2.5之后的SpringMVC引入了基于注解的配置方式,我们将在第26章中详细了解它。

我们已经提前接触过BeanNameUrlHandlerMapping了。下面我们详细看一下SimpleUrlHandlerMapping的特性与使用方法。

SimpleUrlHandlerMapping

使用BeanNameUrlHandlerMapping进行Web请求到具体Handler的映射管理,需要我们保证视图模板中的请求路径,必须与容器中对应的Handler的beanName一致, 如图24-1所示。

image-20220628115139107

也就是说,BeanNameUrlHandlerMapping只做了一个传话人,并没有对请求路径与Handler之间的映射做太多的关注(有点儿不动脑子的意思)。从好的方面说,遵循Convention Over Configuration的理念可以减少配置量,提高开发效率。从坏的方面说,BeanNameUrlHandlerMapping强制我们的Handler的bean定义名称,必须去匹配视图中的链接路径。 如果表现层和Web层分别由不同的人员开发,并且人员之间没有合适的交流渠道,那么就有些令双方人员勉为其难了。

SimpleUrlHandlerMapping比BeanNameUrlHandlerMapping做的工作要多一些,可以说尽到了自己作为中间人的那份职责。它允许视图一方和handler一方自由活动,只不过最后由SimpleUrlHandlerMapping进行“统筹”。 如果我们将处理当日评价汇率流程中使用的BeanNameUrlHandlerMapping替换成SimpleUrlHandlerMapping,DispatcherServlet特定的WebApplicationContext中的内容看起来如下方代码清单所示。

<beans xmlns="http://www.springframework.orgschema/beans" xmlns:xsi="http://www.W3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.orgschema/p" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
  
	<bean id="handlerMapping" class="org.springframework.Web.servlet.handler.SimpleUrlHandlerMapping">
		<property name="mappings">
			<props>
        		下面这行是重点
				<prop key="ttmRatelist.do">ttmRateListController</prop>
	      		...
			</props>
		</property>
	</bean>
  
  下面这行是重点
	<bean name="ttmRateLiatController" c1ass="..TTMRatelistController">
		<property name="ttmRateService" ref="ttmRateService"></property>
		<property name="viewName" value="ttmRateList"></property>
	</bean>
  
	<bean id="viewResolver" class="org.springframework.Web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/WEB-INF/jsp/"/>
		<property name="suffix" value=".jsp"/>
	</bean>
</beans>

现在,TTMRateListController可以起任何名字,视图中的链接也可以独立提供,只需要通过SimpleUrlHandlerMapping明确指定一下二者的对应关系就行。SimpleUrlHandlerMapping不对二者做任何限制,如图24-2所示。

image-20220628115749217

使用SimpleUrlHandlerMapping的另一个好处就是,我们可以使用类似于ANT路径形式的模式匹配 ,这样我们就可以通过各种表达式,将一组或者多组拥有某种相似特征的Web处理请求映射给相应的Handler处理,类似于下方代码清单所示的样子。

<bean id="handlerMapping" class="org.springframework.Web.servlet.handler.SimpleUrlHandlerMapping">
	<propert yname="mappings">
		<value>
			ttmRateList.do=ttmRateListController
			/**/*list.do=genericListingController
			/user*.do=userManagerController
			/module/*.do=ModuleInOneController
		</value>
	</property>
</bean>

当然,说了这些不是说SimpleUrlHandlerMapping要比BeanNameUrlHandlerMapping有多好。我们更注重的是,在开发的过程中,可以根据情况,灵活地选用相应的HandlerMapping来使用。

注意:什么?你发现同样是SimpleUrlHandlerMapping的mappings属性,我们用了<props><value>两种不同的元素进行配置,你不知道到底哪一个对?是不是写错了?难道你忘了Spring提供的org.springframework.beans.propertyeditors.PropertiesEditor是用来干什么的了吗?

24.1.2 HandlerMapping执行序列(Chain Of HandlerMapping)

在基于Spring MVC的Web应用程序中,我们可以为DispatcherServlet提供多个HandlerMapping供其使用。

DispatcherServlet在选用HandlerMapping的过程中,将根据我们所指定的一系列HandlerMapping的优先级进行排序,然后优先使用优先级在前的HandlerMapping。如果当前的HandlerMapping能够返回可用的Handler,DispatcherServlet则使用当前返回的Handler进行Web请求的处理,而不再继续询问其他的HandlerMapping。否则,DispatcherServlet将继续按照各个HandlerMapping的优先级进行询问,直到获取一个可用的Handler为止。

HandlerMapping的优先级规定遵循Spring框架内一贯的Ordered接口 所规定的语义。Spring MVC中可用的HandlerMapping实现全都实现了Ordered接口。假设我们优先使用SimpleUrlHandlerMapping进行Handler的映射管理,其次使用BeanNameUrlHandlerMapping,那么就可以在DispatcherServlet特定的WebApplicationContext中增加如下方代码清单所示的配置内容。

<bean id="handlerMapping" c1ass="org.springframework.Web.servlet.handler.SimpleUr1HandlerMapping">
	<property name="order" value="1"/>
	<property name="mappings">
		<value>
      		...
		</value>
	</property>
</bean>

<bean id="defaultHandlerMapping" class="org.springframework.Web.servlet.handler.BeanNameUrlHandlerMapping">
</bean>

如果不为HandlerMapping明确指定order,那么默认值为Integer.MAX_VALUE,对应最低优先级。所以,拥有order值为1的SimpleUrlHandlerMapping较之BeanNameUrlHandlerMapping优先被调用。

24.2 我们的亲密伙伴Controller

Controller是SpringMVC框架支持的用于处理具体Web请求的handler类型之一。 在使用SpringMVC框架开发Web应用程序过程中,它是我们接触最多的角色,几乎每个请求流程的实现都需要与Controller打交道(否则具体的处理逻辑写到哪里去呢?),所以称其为“亲密伙伴”也不为过吧!要实现一个具体的Controller,我们当然可以直接实现Controller接口,其定义如下:

public interface Controller {
    
    
	ModelAnaViewhand1eRequest(HttpServletRequest request,HttpServletResponse response) throws Exception;
}

但更多时候,我们可能会寻求使用Spring MVC提供的更细粒度的Contoller框架类。直接实现Controller接口当然没有问题,这让我们可以随心所欲地实现Web处理过程中的所有关注点,但这通常需要我们关注更多底层的细节,比如请求参数的抽取、请求编码的设定、国际化信息的处理、Session数据的管理等。而实际上,可能这些细节或者关注点是所有的Controller都需要的。我们就应该想办法让这些通用的逻辑可以以某种方式被复用。这也就是Spring MVC提供了一套Controller实现体系的原因。它帮助我们更好地处理了Web请求处理过程中的某些通用关注点。

图24-3给出了Spring MVC中Controller的继承层次体系的概况。

image-20220628132004648

为了便于理解,我们不妨把controller划分为如下两类。

  • 自由挥洒派的Controller。 之所以将AbstractController和MultiActionController归为自由挥洒一派,是因为基本上只要你熟悉ServletAPI,就可以随意处理Web请求了。如果你是从Servlet时代走过来的,那么会发现这一派的做事风格让你觉得很亲切,从HttpServletRequest中获取参数,然后验证,调用业务层逻辑,最终返回一个ModelAndView,甚至,你都可以直接通过HttpServletResponse输出最终的视图。不过自由倒是自由了,杂七杂八要处理的细节也很多,所以,使用AbstractController和MultiActionController之前最好先考察一下当前场景是否合适。如果要处理的底层细节过多,那么不妨求助一下"规范操作派”的各位仁兄!

  • 规范操作派的Controller。 以BaseCommandController为首的规范操作一派,对Web处理过程中的某些通用逻辑做了进一步的规范化封装处理 ,规范化的方面主要包括:

    • 自动抽取请求参数并绑定到指定的Command对象;

    • 提供了统一的数据验证方式,BaseCormandController及其子类可以接收一组org.springframework.validation.Validator以进行数据验证,我们可以根据具体数据提供相应的Validator实现;

    • 规范化了表单(Form)请求的处理流程,并且对简单的多页面表单请求处理提供支持。

虽然这套“制度”对我们的自由挥酒进行了限定,但只要我们熟悉了这套规则,依然可以游刃于其间。各门派我们已经介绍完了,下面是各派中实力比较雄厚的诸位弟子的表演时间…

24.2.1 AbstractController

AbstractController是整个Controller继承层次的起源 (多少有点儿少林正宗弟子的意味),该类通过模板方法模式(TemplateMethodPattern)帮我们解决了如下几个通用关注点:

  • 管理当前Controller所支持的请求方法类型(GET/POST);

  • 管理页面的缓存设置,即是否允许浏览器缓存当前页面;

  • 管理执行流程在会话(Session)上的同步。

而我们所要做的,只不过是在AbstractController所公开的handleRequestInternal(request, response)模板方法中实现具体Web请求处理过程中的其他逻辑。

注意:有关AbstractController所处理的通用关注点的设置参数,可以参考Spring文档或者AbstractController类的Javadoc。

虽然我们已经扩展AbstractController实现了一个TTMRateListController,但多少让人感觉过于简单了。为了进一步向你展示AbstractController的无尽魅力,我们不妨对TTMRateListController进行一下扩展。

TTMRateListController只获取当日评价汇率。为了一般化评价汇率显示功能,我们希望能够检索指定营业日的评价汇率。当用户没有明确指定营业日的时候,返回当日评价汇率,否则返回指定营业日对应的评价汇率。而这也正是大多生产环境下Web应用的需求形式。

为了达到以上扩展要求,我们需要对TTMRateListController及其相关组件进行一系列的重构工作。重构评价汇率请求处理流程的步骤如下所示。

(1)重构业务层接口及实现。 为了能够提供根据营业日进行检索的功能,我们需要一般化ITTMRateService的服务方法。重构后的ITTMRateService接口定义如下所示:

public interface ITTMRateService {
    
    
  List<TTMRate> getTMRatesByTradeDate(TradeDatetradeDate);
}

相应的实现类现在也需要提供进一步的支持,我们依然先采用MockTTMRateService提供模拟数据。现在MockTTMRateService定义如下方代码清单所示。

public class MockTTMRateService implements ITTMRateService {
    
    
	private Map<TradeDate,List> mockData = new HashMap<TradeDate,List>();
  
	public MockTTMRateService() {
    
    
		TradeDate tradeDate20080302 = TradeDate.valueOf("20080302");
		TTMRate USD_JPY = new TTMRate(tradeDate20080302, "USD/JPY", new BigDecimal("121.53"));
		TTMRate EUR_USD = new TTMRate(tradeDate20080302, "EUR/USD", new BigDecimal("1.8950"));
		List<TTMRate> rates0f20080302 = new ArrayList<TTMRate>();
		rates0f20080302.add(USD_JPY);
		rates0f20080302.add(EUR_USD);
		mockData.put(tradeDate20080302, rates0f20080302);
  		// ...添加更多模拟数据
		// mockData.put(TradeDate.today(),...);
  }
  
	public List<TTMRate> getTTMRatesByTradeDate(TradeDatet radeDate) {
    
    
		List<TTMRate> ttmRateList = mockData.get(tradeDate);
		if(ttmRateList == null) {
    
    
			return new ArrayList<TTMRate>();
    	}
		return ttmRateList;
  }
}

当然,只有出于测试目的才像我们这样构造数据。实际系统中通常需要从数据库中抽取相应的评价汇率。但不管怎么样,我们暂且有了一个可用的ITTMRateService实现了。

既然早先我们已经将MockTTMRateService添加到了顶层webApplicationContext中,现在可以免去这一步,直接进入Web层的重构工作。

(2)重构TTMRateListController。 TTMRateListController现在要接收并处理Web请求的查询参数,然后根据参数调用相应服务对象,最终返回处理结果,其具体实现逻辑如下方代码清单所示。

public c1ass TTMRateListController extends AbstractController {
    
    
	private ITTMRateService ttmRateService;
	private String viewName;
  
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
    	ModelAndView mav = new ModelAndView(getViewName());
    
		// 比[request.getParameter("tradeDate")]更好的选择
		String tradeDateStr = ServletRequestUtils.getStringParameter(request, "tradeDate");
		TradeDate tradeDate = TradeDate.today{
    
    );
                                          
		// 可以根据情况在这里添加验证逻辑
		if(StringUtils.isNotEmpty(tradeDateStr)) {
    
    
			tradeDate = TradeDate.valueOf(tradeDateStr);
			mav.addObject("tradeDate", tradeDateStr);
    	}
                                          
		List<TTMRate> ttmRateList = getTtmRateService().getTTMRatesByTradeDate(tradeDate);
		mav.addObject("ttmRates", ttmRateList);
		return mav;
  	}
	// getter和setter方法定义等
	// ...
}

实际上,我们还是对TTMRateListController处理的流程做了简化。实际情况可能是,当检查到获取的营业日字符串不合法的时候,需要将相应的错误信息也添加到ModelAndView,然后在视图中显示。

(3)修改视图模板。 现在我们需要添加一个表单,以便用户可以输入要查询的营业日。修改后的JSP模板文件如下方代码清单所示。

<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>
<$@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-// W3C// DTD HTML 4.01 Transitional// EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/htm1;charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form name="" method="post" action="<c:ur1 value='ttmRateList.do'/>">
	<label>营业日
	<input type="text" name="tradeDate" value="${tradeDate}">
	</label>
	<1abel>
	<input type="submit" name="Submit" va1lue="检索">
	</label>
</form>
  
<table width="517" border="1" cellpadding="0" cellspacing="0" bordercolor="#33FFFF">
	<CAPTION>当日评价汇率</CAPTION>
	<tr bgcolor="#999900"><td width="250"><div align="center">Currency Pair</div></tr
    <td width="261"><div align="center">TTMRATE</div></td>
	</tr>
	<c:forEach items="${ttmRates}" var="ttmRate">
		<tr>
			<td><div align="center">${ttmRate.currencyPair}</div></td>
     		<td><div align="center">${ttmRate.value}</div></td>
		</tr>
	</c:forEach>
</table>
</body>
</html>

相对最初的示例代码来说,我们只是多增加了一个表单的HTML定义。其中<input>所传递的参数名称tradeDate,恰好对应TTMRateListController获取的参数名称,这个是最底层的契约。

整个示例要用于实际生产环境还存在需要改进的地方,比如,更规范的数据合法性验证、错误流程的追加以及视图中错误信息的显示,甚至于视图的美化等。但我们已经通过它构建了通向更加完善的处理流程的基础。不管怎么说,现在已经能够看到重构后的初步成果了,见图24-4。

image-20220628134850128

正如我们所看到的,虽然直接继承AbstractController实现Web处理逻辑没有太多限制,但往往需要关注太多的细节,比如参数抽取、数据验证之类。所以,对于简单的Web请求,可以通过这种方式来处理,如果涉及表单相关的Web请求,最好是使用BaseComnandController一族的相应子类进行扩展,如稍后将介绍的SimpleFormController。

24.2.2 MultiActionController

MultiActionController一旦出马,就身兼数职。 对于一组逻辑上相近的Web请求来说,比如针对同一对象的CRUD(Create-Read-Update-Delete)操作,或者针对同一对象甚至多个对象的一组查询操作,我们可以将这些Web请求交给MultiActionController来统一处理,而不用分别为每个Web请求单独实现一个继承AbstractController的处理类。MultiActionController类似于Struts 1.1框架的DispatchAction,但比DispatchAction要复杂并且灵活得多。

MultiActionController继承了AbstractController,所以也就拥有了AbstractController所处理的那些通用关注点的能力。除此之外,MultiActionController还提供了以下功能。

  • 请求参数到Command对象的绑定。 这看起来多少有点儿多管BaseCommandController闲事的意思。因为正规上说,BaseCommandController才是提供数据绑定功能的基类。不过,BaseCommandController提供的数据绑定和验证更多地服务于其子类的流程控制,而MultiActionController提供的数据绑定能力则比较独立,有点儿你爱用不用的意思。

  • 通过Validator的数据验证。 这个功能与MultiActionController的数据绑定是一起的,一旦启用了到Command对象的数据绑定,那么,绑定过程中MultiActionController会自动调用我们注册的一系列Validator进行数据验证。当然,即使不用数据绑定功能,也同样可以自行决定是否调用相应的validator。

  • 细化的异常处理方法。 可以定义一系列的异常处理方法,用于处理Web请求处理过程中所抛出的特定类型的异常。

为了在MultiActionController中处理多个Web请求,我们需要定义多个Web请求处理方法,分别对应每个Web请求的处理。 这些Web请求处理方法可以定义在MultiActionController的子类中,也可以定义在某一个将来可以指定给MultiActionController的委派对象(delegate)内,但Web请求处理方法的签名必须符合一定的要求,如下所示:

(ModelAndView|Map|void) methodName(HttpServletRequest request, HttpServletResponse response[,(HttpSession session |Object command)]);

Web请求处理方法的名称可以取任何有意义的名字,但前两个方法参数是必须的,第三个参数是可选的 ,可以是HttpSession类型也可以是Object类型。如果是Object类型,则表明对应的是Command对象,那么MultiActionController就会帮我们绑定数据并执行数据验证了。方法的返回值有三种类型,分别对应如下语义。

  • 返回ModelAndView表示正常的Web处理方法,后继的ViewResolver和view处理流程,依照之前的DispatcherServlet的流程进行。

  • 返回Map表明只返回了模型数据,而没有返回逻辑视图名。这时,将寻求默认的视图名。这个工作由org.springframework.Web.servlet.RequestToViewNameTranslator负责,该类将在第27章中详细介绍。这里只需要知道,它能按照某种规则提供一个默认的逻辑视图名。

  • 返回void,则表明既没有返回模型数据,也没有返回逻辑视图名。这时,我们认为,当前Web请求处理方法自行处理掉了视图的渲染和输出。

  • 另外,Spring2.5中可以返回String,代表逻辑视图名,没有相关的模型数据。

有了以上的规则限定,通常的MultiActionController定义看起来就如下方代码清单所示的样子了。

public class GenericCRUDMultiActionController extends MultiActionController {
    
    
	public Map 1ist(HttpServletRequest request, HttpServletResponse response) {
    
    
    	...
  	}
	public ModelAndView delete(HttpServletRequest request, HttpServletResponse response) 	{
    
    
    	...
  	}
	public ModelAndView update(HttpServletRequest request, HttpServletResponse response, Object command) {
    
    
    	...
  	}
}

剩下我们所要做的就是,在每个方法内实现当前Controller要完成的功能。不过,我们肯定有一个问题,如何让MultiActionController知道具体哪个Web请求进来之后,将由哪个方法来处理它?

答案是MethodNameResolver。

1. MultiActionControler的助理MethodNameResolver

org.springframework.Web.servlet.mvc.multiaction.MethodNameResolver的主要工作,就是帮助MultiActionController决定当前Web请求应该交给哪个方法来处理 ,其定义如下:

public interface MethodNameResolver {
    
    
	String getHandlerMethodName(HttpServletRequest request) throws NoSuchRequestHandlingMethodException;
}

有了MethodNameResolver这一助手,MultiActionController现在只需要询问一声当前需要调用哪个方法即可,如下代码所示:

protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
	try {
    
    
		String methodName = this.methodNameResolver.getHandlerMethodName(request);
		return invokeNamedMethod(methodName, request, response);
 	}
	catch(NoSuchRequestHandlingMethodException ex) {
    
    
		return handleNoSuchRequestHandlingMethod(ex, request, response);
  	}
}

MethodNameResolver为MultiActionController提供了灵活的Web请求到对应处理方法的映射策略,包括根据Web请求的URL进行映射,或者根据某个参数值进行映射等。在考虑提供自定义的MethodNameResolver之前,我们有必要先了解一下SpringMVC都提供了哪几个可用的MethodNameResolver实现,以免误入“重新发明轮子”之地。

MethodNameResolver本身作为一个策略接口定义,在SpringMVC框架内默认提供了如下三种策略实现。

InternalPathMethodNameResolver。 如果没有为MultiActionController明确指定任何MethodNameResolver,那么InternalPathMethodNameResolver将作为默认的MethodNameResolver实现,以进行Web请求与具体处理方法间的映射解析。InternalPathMethodNameResolver将提取URL最后一个(/)之后的部分并去除扩展名,作为要返回的方法名称 ,比如:

/simplefx/macontroller/1isting.do
/simp1efx/macontroller/create.do
/simplefx/macontroller/update.do
/simplefx/macontroller/delete.do

如果将/simplefx/macontroller/*通过HandlerMapping指定映射到某个MultiActionController实现来处理,那么,对应以上4组URL的Web请求,将分别由这个MultiActionController的listing、create、update以及delete4个方法来处理。

当然,我们也可以通过设定InternalPathMethodNameResolver的prefix和suffix来有限的修饰映射行为,比如:

<bean id="interna1PathMethodNameResolver" class="org.springframework.Web.servlet.mvc.multiaction.InternalPathMethodNameResolver">
	<property name="prefix" value="rate_"/>
</bean>

当使用如上的InternalPathMethodNameResolver定义来映射刚才4组URL的时候,对应的处理方法名应该变成rate_listing、rate_create、rate_update以及rate_delete。

注意:除非需要指定prefix或者suffix这样的额外属性,作为默认MethodNameResolver实现的InternalPathMethodNameResolver,并不需要明确添加到容器中。

PropertiesMethodNameResolver。 PropertiesMethodNameResolverInternalPathMethodNameResolver的唯一相同点在于,它们都是基于请求的URI进行映射(二者有共同的父类AbstractUrlMethodNameResolver),但它比InternalPathMethodNameResolver灵活。

如果从HandlerMapping与MethodNameResolver都是处理映射这一点来看,InternalPathMethodNameResolver相当于BeanNameUrlHandlerMapping,而PropertiesMethodNameResolver则相当于SimpleUrlHandlerMapping。

通过PropertiesMethodNameResolver可以指定完全匹配的映射关系,或者使用ANT形式的路径匹配模式所表达的映射关系 ,例如:

<bean id="propsMethoaNameResolver" class="org.springframework.Web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
	<property name="mappings">
		<value>
			/1isting.do=listingRates
			/update.do=updateRate
			/create*.do=addNewRate
		</value>
	</property>
</bean>

MultiActionController中定义的方法不允许重载,所以,映射中只需要指定方法名即可,不需要任何参数信息。

ParameterMethodNameResolver。 结合ParameterMethodNameResolver使用的MultiActionController,应该是最贴近Struts 1.1的DispatchAction的行为了。不过,ParameterMethodNameResolver赋予了MultiActionController比DispatchAction更灵活多变的请求到处理方法的映射方式。

ParameterMethodNameResolver允许我们根据请求中的某个参数的值作为映射的方法名,也允许我们使用请求中一组参数来映射处理方法名称。

下面是这两种策略的详细情况。

(1)根据请求中某个参数的值作为映射后的方法名。 在Web请求提交之后,我们可以附带一个参数,专门指定由MultiActionController的哪个方法来处理当前请求,如图24-5所示。

image-20220629132941288

ParameterMethodNameResolver默认检测的参数名称为action,恰好如图24-5所示。我们也可以通过ParameterMethodNameResolver的setParamName(String)方法来更改默认的参数名称。如果如下所示,指定paramName为methodName的话:

<bean class="org.springframework.Web.servlet.mvc.multiaction.ParameterMethodNameResolver">
	<property name="paramName" value="methodName"/>
</bean>

那么,HTTP GET形式发送的URL看起来就类似于http://host:port/simplefx/macontroller?methodName=list。当然,以HTTP GET形式演示可以让你有一个直观的认识。你同样可以通过HTTP POST的形式发送该参数(可能需要在客户端根据事件做些手脚,玩过HTML和Javascript的应该都知道)。

(2)根据请求中的一组参数作为映射后的方法名。在某个页面中存在多种行为选择的时候,可以让每一种行为对应一个参数 ,这样,在Web请求提交之后,ParameterMethodNameResolver可以根据提交后的行为参数来调用相应的处理方法,如图24-6所示。

image-20220629133232584

我们通过methodParamNames属性为ParameterMethodNameResolver指定一组要检测的参数名。ParameterMethodNameResolver将以指定的一组参数名作为基准,对Web请求中的参数进行检测。如果发现存在其中某个参数,则将当前Web请求映射到与参数相同名称的处理方法。

假设图24-6中只为methodParamNames指定list和update两个值,那么,在名称为delete的submit按钮提交之后,ParameterMethodNameResolver将无视该参数的存在。这时就可能导致当前请求找不到相应的处理方法进行处理的问题。保险起见,最好通过ParameterMethodNameResolver的defaultMethodName属性指定一个默认处理方法,如下所示:

<bean class="?rg.springframework.Web.servlet.mvc.multiaction.ParameterMethodNameReso1ver">
	<property name="methodParamNames" value="list,update,delete"/>
	<property name="defaultMethodName" value="list"/>
</bean>

如果ParameterMethodNameResolver的methodParamNames和paramName两个属性都设置了的话,那意味着两种策略同时使用。这时,以methodParamNames属性为代表的映射策略将被优先考虑。

基本上,这三种MethodNameResolver就可以满足我们日程开发的需要了。如果实在没有合适的,那再去实现一个也不晚。要做的只不过是替换一下具体的MethodNameResolver策略实现类而已。这也就是MultiActionController比Struts 1.1的DispatchAction更具灵活性之所在了。

2. MultiActionController应用演示

因为评价汇率通常是不能够更新或者删除的,所以,为了演示MultiActionController的使用,我们得重新寻找一个场景。在SimpleFx的后台管理程序中,操作员属于不同的组(Group),每个组的管理权限不同,有的组可以读取或者更新管理信息,而有的组可能只能读取信息。我们可以增加这样的组,更新某个组的权限,甚至删除某个组。这些针对同一实体的操作请求,我们可以将它们映射到同一个MultiActionController进行处理。

我们对整个操作员组的处理场景进行了统一的流程建模,这包括稍后将使用SimpleFormController实现的处理流程部分,图24-7中是对整个流程的简要概括。

image-20220629140039274

当前,我们主要关心左边矩形框中的流程处理部分。在我们点击后台管理画面相应链接发起Web请求之后,我们将操作员组管理相关的Web请求全部交由MultiActionController来处理。 MultiActionController的默认处理方法为list,即罗列当前系统中与组相关的信息列表。这时,我们进入了groupAdmin.jsp渲染的视图画面,该画面有创建、更新和删除三个提交按钮,分别对应操作员组的创建、更新以及删除操作。任何一个提交按钮触发的表单提交,都将再次映射给我们的MultiActionController处理。我们的MultiActionController将根据提交的参数来决定调用哪个方法进行处理工作。总之,到目前为止,不管是最初的list请求还是之后整个表单的提交(存在三种提交分支情况),最终处理都是经由MultiActionController进行的。MultiActionController对逻辑相近的一组Web请求进行管理的特点在这里彰显无遗。

MultiActionController的不同处理方法执行之后,会根据当前场景将处理流程重定向到后继不同的分支流程上。 比如,创建或者更新处理请求最终将被导向相应的表单页面进行后继操作,而这已经是后话了。所以,我们先将这部分流程的实现搁置一边,回头看看对应操作员组权限管理的MultiActionController是如何实现的吧!

操作员组权限管理实现流程概略

(1)构建业务层支持。 有了之前的基础,构建当前流程我们就惜些笔墨,所以,应该如何构建一个服务对象以便对操作员组进行管理等细节就不做详述了,只要我们达成了“需要一个IGroupAdminService和IGroupAdminService的实现类”这样的共识即可。如何将它们添加到相应的项层WebApplicationContext我想不必多提了,毕竟,我们此行的主要目的应该是MultiActionController。

(2)构建MultiActionController。 使用MultiActionController进行一组Web请求的处理有两种实现方式:

  • 继承MultiActionController;

  • 为MultiActionController提供一个委派对象。

为MultiActionController提供一个委派对象的好处在于,委派对象不需要继承任何父类或者接口。 可以说是“干净利落,无牵无挂”,所以,我们就用它了。

我们初步决定使用ParameterMethodNameResolver 对请求方法进行映射,具体来说,就是为methodParamNames指定create、update和delete作为要映射的参数,同时以list作为默认的方法,配置如下所示:

<bean id="methodNameResolver" class="org.springframework.Web.servlet.mvc.multiaction.ParameterMethodNameResolver">
	<property name="methodParamNames" value="create,update,delete"/>
	<property name="defaultMethodName" value="list"></property>
</bean>

这样我们所提供的委派对象就需要实现list、create、update和delete对应的Web请求处理方法。实现后的委派对象定义如下方代码清单所示。

public class GroupAdminDelegate {
    
    
	private IGroupAdminService groupAaminService;
	private String 1istViewName;
	private String createViewName;
	private String updateViewName;
	private String deleteSuccessViewName;
  
	public ModelAndView list(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
		List<AdminGroup> groups = getGroupAdminService().getAvailableGroups();
		ModelAndView mav = newModelAndView(getListViewName());
		mav.addObject("groups", groups);
		return mav;
  	}
  
	public ModelAnaView create(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
		return new ModelAndView(getCreateViewName());
  	}
  
  	public ModelAndView delete(HttpServletRequest request, HttpServletResponse response, AdminGroup group) throws Exception {
    
    
		getGroupAdminService().deleteGroup(group);
		ModelAndView mav = new ModelAndView(getDeleteSuccessViewName());
		return mav;
  	}
  
  	public ModelAndView update(HttpServletRequest request, HttpServletResponse response, AdminGroup group) throws Exception {
    
    
		return new ModelAn?View(getUpdateViewName(), "groupName", group.getGroupName());
  	}
	// getter和setter方法定义等
}

为了突出重点,整个GroupAdminDelegate的实现忽略了部分细节上的东西,比如异常的处理,以及相应情况下的视图跳转等。你在实现真实环境下的MultiActionController逻辑的时候,应该将这些逻辑添加进去,比如,直接使用MultiActionController可以定义异常处理方法的能力(custom exception handler method)。

有了MultiActionController使用的委派对象实现和MethodNameResolver,我们就能够在WebApplicationContext中,为操作员组权限管理相关的Web请求提供可用的MultiActionController了。如下方代码清单所示。

<bean name="/groupAdmin.do" class="org.springframework.Web.servlet.mvc.multiaction.MultiActionController">
	<property name="delegate" ref="groupAdminDelegate"/>
	<property name="methodNameResolver" ref="methodNameResolver"/>
</bean>

<bean id="methodNameResolver" class="org.springframework.Web.servlet.mvc.multiaction.ParameterMethodNameResolver">
	<property name="methoaParamNames" value="create,update,delete"/>
	<property name="defaultMethodName" value="list"></property>
</bean>

下面这行是重点
<bean id="groupAdminDelegate" class="cn.spring21.simplefx.controllers.GroupAdminDelegate">
	<property name="groupAdminService" ref="groupAdminService"/>
	<property name="listViewName" value="groupAdmin"/>
	<property name="createViewName" value="redirect:createGroup.do"/>
	<property name="updateViewName" value="redirect:updateGroup.do"/>
	<property name="deleteSuccessViewName" value="redirect::groupAdmin.do"/>
</bean>

通过MultiActionController的delegate属性,我们指定了它要使用的委派对象。MultiActionController内部将使用反射机制调用相应MethodNameResolver所返回的处理方法。

提示:指定的逻辑视图名可以添加相应的前缀(prefix),比如redirect:viewName,将以重定向(redirect)的形式跳转到相应的视图。有关视图的转发(forward)和重定向(redirect),我们将在稍后ViewResolver和View部分详细介绍。

(3)构建组权限管理默认视图。 默认到/groupAdmin.do的Web请求将被引导到groupAdmin.jsp模板所渲染的视图画面。在这里,我们将根据情况选择要对操作员组做哪些操作,最终视图显示类似图24-8所示。

image-20220629143906365

但不管具体操作是什么,最终该页面提交的请求将同样由我们的MultiActionController进行处理。

我们使用了DisplayTag进行了信息列表的输出。下方代码清单中是groupAdmin.jsp模板文件的具体内容。

<jsp:root version="1.2" xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:display="urn:jsptld:http://displaytag.sf.net">
<jsp:directive.page contentType="text/html;charset=UTF8"1>
<jsp:include page="/WEB-INF/jsp/header.jsp" flush="true"/>
  
<jsp:body>
	<form action="groupAdmin.do" method="post">
	<input name="create" type="submit" id="create" value="创建"/>
	<input name="update" type="submit" id="update" value="更新"/>
	<input name="delete" type="submit" id="de1ete" value="删除"/>
	<display:table name="groups" id="group">
		<display:column title="选择"><input name="groupName" type="radio" value="$(group.groupName)"/></display:column>
		<display:column title="组名" property="groupName"></display:column>
	</display:table>
	</form>
</jsp:body>
</jsp:root>

希望你能够在该模板的基础上进行完善,以进一步提升用户的体验,比如,如果用户要执行更新/删除操作,应该选取相应的组,否则,我们应该给予友好的提示。

MultiActionController可以帮助我们集中管理一组Web请求的处理逻辑,但因为需要通过反射调用相应的处理方法,所以,自身也就失去了编译期间检查等好处。另外,虽然MultiActionController也提供了数据绑定和数据验证功能,但并没有在表单的处理上做更多的流程定义,在允许开发人员有更多的发挥余地的同时,也可能引入风险:

  • 滥用MultiActionController,进而导致同一个MultiActionController处理的职责过多体积臃肿而难以维护;

  • 表单数据的处理上可能每个开发人员都有一套单独的处理方式,造成后期维护困难。

总之,要在合适的场景使用MultiActionController。如果涉及单一复杂的表单交互,尽量使用即将上场的SimpleFormController进行处理。

猜你喜欢

转载自blog.csdn.net/qq_34626094/article/details/125769913