写点什么

Spring 2.5:Spring MVC 中的新特性

2008 年 9 月 17 日

Spring 框架从创建伊始就致力于为复杂问题提供强大的、非侵入性的解决方案。Spring 2.0 当中为缩减 XML 配置文件数量引入定制命名空间功能,从此它便深深植根于核心 Spring 框架(aop、context、jee、jms、 lang、tx 和 util 命名空间)、Spring Portfolio 项目(例如 Spring Security)和非 Spring 项目中(例如 CXF)。

Spring 2.5 推出了一整套注解,作为基于 XML 的配置的替换方案。注解可用于 Spring 管理对象的自动发现、依赖注入、生命周期方法、Web 层配置和单元 / 集成测试。

探索 Spring 2.5 中引入的注解技术系列文章由三部分组成,本文是其中的第二篇,它主要讲述了 Web 层中的注解支持。最后一篇文章将着重介绍可用于集成和测试的其它特性。

这个系列文章的第一部分论述了Java 注解(annotation)是如何代替XML 来配置Spring 管理对象和依赖注入的。我们再用一个例子回顾一下:

@Controller<br></br> public class ClinicController {<p> private final Clinic clinic;</p><p> @Autowired</p><br></br> public ClinicController(Clinic clinic) {<br></br> this.clinic = clinic;<br></br> }<br></br> ...@Controller 表明 ClinicController 是 Web 层组件,@Autowired 请求一个被依赖注入的 Clinic 实例。这个例子只需要少量的 XML 语句就能使容器识别两个注解,并限定组件的扫描范围:

<context:component-scan base-package="org.springframework.samples.petclinic"/> <component-scan base-package="org.springframework.samples.petclinic"></component-scan>这对 Web 层可谓是个福音,因为在这层 Spring 的 XML 配置文件已日益臃肿,甚至可能还不如层下的配置来得有用。控制器掌握着许多属性,例如视图名称、表单对象名称和验证器类型,这些多是关乎配置的,甚少关于依赖注入的。通过 bean 定义继承,或者避免配置变化不是很频繁的属性,也可以有效的管理类似的配置。不过以我的经验,很多开发人员都不会这样做,结果就是 XML 文件总比实际需要的要庞大。不过 @Controller 和 @Autowired 对 Web 层的配置会产生积极的作用。

在系列文章的第二部分我们将继续讨论这个问题,并浏览 Spring 2.5 在 Web 层的注解技术。这些注解被非正式的称为 @MVC,它涉及到了 Spring MVC 和 Spring Porlet MVC,实际上本文讨论的大部分功能都可以应用在这两个框架上。

从 Controller 到 @Controller

与第一部分讨论的注解相比,@MVC 已不只是作为配置的一种替换方案这样简单了,考虑下面这个著名的 Spring MVC 控制器签名:

public interface Controller {<br></br> ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse <br></br>response) throws Exception;<br></br> }所有的 Spring MVC 控制器要么直接实现 Controller 接口,要么就得扩展类似 AbstractController、 SimpleFormController、 MultiActionController 或 AbstractWizardFormController 这样的基类实现。正是 Controller 接口允许 Spring MVC 的 DispatcherServlet 把所有上述对象都看作是“处理器(handlers)”,并在一个名为 SimpleControllerHandlerAdapter 的适配器的帮助下调用它们。

@MVC 从三个重要的方面改变了这个程序设计模型:

  1. 不需要任何接口或者基类。
  2. 允许有任意数量的请求处理方法。
  3. 在方法签名上具有高度的灵活性。

考虑到以上三个要点,就可以说很公平的说 @MVC 不仅仅是个替换方案了,它将会是 Spring MVC 的控制器技术演变过程中下一个重要步骤。

DispatcherServlet 在名为 AnnotationMethodHandlerAdapter 的适配器帮助下调用被注解的控制器。正是这个适配器做了大量工作支持我们此后将会讨论的注解,同时也是它有效的取代了对于控制器基类的需求。

@RequestMapping 简介

我们还是从一个类似于传统的 Spring MVC Controller 控制器开始:

@Controller<br></br> public class AccountsController {<p> private AccountRepository accountRepository;</p><p> @Autowired</p><br></br> public AccountsController(AccountRepository accountRepository) {<br></br> this.accountRepository = accountRepository;<br></br> }<p> @RequestMapping("/accounts/show")</p><br></br> public ModelAndView show(HttpServletRequest request,<br></br> HttpServletResponse response) throws Exception {<br></br> String number = ServletRequestUtils.getStringParameter(request, "number");<br></br> ModelAndView mav = new ModelAndView("/WEB-INF/views/accounts/show.jsp");<br></br> mav.addObject("account", accountRepository.findAccount(number));<br></br> return mav;<br></br> }<br></br> }此处与以往的不同在于,这个控制器并没有扩展 Controller 接口,并且它用 @RequestMapping 注解指明 show() 是映射到 URI 路径 “/accounts/show”的请求处理方法。除此以外,其余代码都是一个典型的 Spring MVC 控制器应有的内容。

在将上述的方法完全转化到 @MVC 后,我们会再回过头来看 @RequestMapping,但是在此之前还有一点需要提请注意,上面的请求映射 URI 也可匹配带有任意扩展名的 URI 路径,例如:

/accounts/show.htm<br></br> /accounts/show.xls<br></br> /accounts/show.pdf<br></br> ...### 灵活的请求处理方法签名

我们曾经承诺过要提供灵活的方法签名,现在来看一下成果。输入的参数中移除了响应对象,增加了一个代表模型的 Map;返回的不再是 ModelAndView,而是一个字符串,指明呈现响应时要用的视图名字:

@RequestMapping("/accounts/show")<br></br> public String show(HttpServletRequest request, Map<String, Object> model)<br></br> throws Exception {<br></br> String number = ServletRequestUtils.getStringParameter(request, "number");<br></br> model.put("account", accountRepository.findAccount(number));<br></br> return "/WEB-INF/views/accounts/show.jsp";<br></br> }Map 输入参数是一个“隐式的”模型,对于我们来说在调用方法前创建它很方便,其中添加的键—值对数据便于在视图中解析应用。本例视图为 show.jsp 页面。

@MVC 可以接受多种类型的输入参数,例如 HttpServletRequest/HttpServletResponse、HttpSession、Locale、InputStream、 OutputStream、File[] 等等,它们的顺序不受任何限制;同样它也允许多种返回类型,例如 ModelAndView、Map、 String,或者什么都不返回。你可以查看 @RequestMapping 的 JavaDoc 以了解它支持的所有输入和返回参数类型。

有种令人感兴趣的情形是当方法没有指定视图时(例如返回类型为 void)会有什么事情发生,按照惯例 DispatcherServlet 要再使用请求 URI 的路径信息,不过要移去前面的斜杠和扩展名。让我们把返回类型改为 void:

@RequestMapping("/accounts/show")<br></br> public void show(HttpServletRequest request, Map<String, Object> model) throws Exception {<br></br> String number = ServletRequestUtils.getStringParameter(request, "number");<br></br> model.put("account", accountRepository.findAccount(number));<br></br> }对于给定的请求处理方法和“/accounts/show”的请求映射,我们可以期望 DispatcherServlet 能够获得“accounts/show”的默认视图名称,当它与如下适当的视图解析器结合共同作用时,会产生与前面指明返回视图名同样的结果:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"><br></br> <property name="prefix" value="/WEB-INF/views/" /><br></br> <property name="suffix" value=".jsp" /><br></br> </bean>强烈推荐视图名称依赖惯例的方式,因为这样可以从控制器代码中消除硬编码的视图名称。如果你想定制 DispatcherServlet 获取默认视图名的方式,就在 servlet 上下文环境中配置一个你自己的 RequestToViewNameTranslator 实现,并为其 bean id 赋名为“viewNameTranslator”。

用 @RequestParam 提取和解析参数

@MVC 另外一个特性是其提取和解析请求参数的能力。让我们继续重构上面的方法,并在其中添加 @RequestParam 注解:

@RequestMapping("/accounts/show")<br></br> public void show(@RequestParam("number") String number, Map<String, Object> model) {<br></br> model.put("account", accountRepository.findAccount(number));<br></br> }这里 @RequestParam 注解可以用来提取名为“number”的 String 类型的参数,并将之作为输入参数传入。 @RequestParam 支持类型转换,还有必需和可选参数。类型转换目前支持所有的基本 Java 类型,你可通过定制的 PropertyEditors 来扩展它的范围。下面是一些例子,其中包括了必需和可选参数:

@RequestParam(value="number", required=false) String number<br></br> @RequestParam("id") Long id<br></br> @RequestParam("balance") double balance<br></br> @RequestParam double amount注意,最后一个例子没有提供清晰的参数名。当且仅当代码带调试符号编译时,结果会提取名为“amount ”的参数,否则,将抛出 IllegalStateException 异常,因为当前的信息不足以从请求中提取参数。由于这个原因,在编码时最好显式的指定参数名。

继续 @RequestMapping 的讨论

把 @RequestMapping 放在类级别上是合法的,这可令它与方法级别上的 @RequestMapping 注解协同工作,取得缩小选择范围的效果,下面是一些例子。

类级别:

RequestMapping("/accounts/*")方法级别:

@RequestMapping(value=“delete”, method=RequestMethod.POST)
@RequestMapping(value=“index”, method=RequestMethod.GET, params=“type=checking”)
@RequestMapping 第一个方法级的请求映射和类级别的映射结合,当 HTTP 方法是 POST 时与路径“/accounts/delete”匹配;第二个添加了一个要求,就是名为“type”的请求参数和其值“checking”都需要在请求中出现;第三个根本就没有指定路径,这个方法匹配所有的 HTTP 方法,如果有必要的话可以用它的方法名。下面改写我们的方法,使它可以依靠方法名进行匹配,程序如下:

@Controller<br></br> @RequestMapping("/accounts/*")<br></br> public class AccountsController {<p> @RequestMapping(method=RequestMethod.GET)</p><br></br> public void show(@RequestParam("number") String number, Map<String, Object> model)<br></br> {<br></br> model.put("account", accountRepository.findAccount(number));<br></br> }<br></br> ...方法匹配的请求是“/accounts/show”,依据的是类级别的 @RequestMapping 指定的匹配路径“/accounts/*”和方法名“show”。

消除类级别的请求映射

Web 层注解频遭诟病是有事实依据的,那就是嵌入源代码的 URI 路径。这个问题很好矫正,URI 路径和控制器类之间的匹配关系用 XML 配置文件去管理,只在方法级的映射中使用 @RequestMapping 注解。

我们将配置一个 ControllerClassNameHandlerMapping,它使用依赖控制器类名字的惯例,将 URI 映射到控制器:

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/> <bean></bean>现在“/accounts/*”这样的请求都被匹配到 AccountsController 上,它与方法级别上的 @RequestMapping 注解协作的很好,只要添加上方法名就能够完成上述映射。此外,既然我们的方法并不会返回视图名称,我们现在就可以依据惯例匹配类名、方法名、URI 路径和视图名。

当 @Controller 被完全转换为 @MVC 后,程序的写法如下:

@Controller<br></br> public class AccountsController {<p> private AccountRepository accountRepository;</p><p> @Autowired</p><br></br> public AccountsController(AccountRepository accountRepository) {<br></br> this.accountRepository = accountRepository;<br></br> }<p> @RequestMapping(method=RequestMethod.GET)</p><br></br> public void show(@RequestParam("number") String number, Map<String, Object> model)<br></br> {<br></br> model.put("account", accountRepository.findAccount(number));<br></br> }<br></br> ...对应的 XML 配置文件如下:

<context:component-scan base-package="com.abc.accounts"/><p> <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/></p><p> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"></p><br></br> <property name="prefix" value="/WEB-INF/views/" /><br></br> <property name="suffix" value=".jsp" /><br></br> </bean>你可以看出这是一个最精减的 XML。程序里注解中没有嵌入 URI 路径,也没有显式指定视图名,请求处理方法也只有很简单的一行,方法签名与我们的需求精准匹配,其它的请求处理方法也很容易添加。不需要基类,也不需要 XML(至少也是没有直接配置控制器),我们就能获得上述所有优势。

也许接下来你就可以看到,这种程序设计模型是多么有效了。

@MVC 表单处理

一个典型的表单处理场景包括:获得可编辑对象,在编辑模式下显示它持有的数据、允许用户提交并最终进行验证和保存变化数据。Spring MVC 提供下列几个特性辅助进行上述所有活动:数据绑定机制,完全用从请求参数中获得的数据填充一个对象;支持错误处理和验证;JSP 表单标记库;基类控制器。使用 @MVC,除了由于 @ModelAttribute、@InitBinder 和 @SessionAttributes 这些注解的存在而不再需要基类控制器外,其它一切都不需要改变。

@ModelAttribute 注解

看一下这些请求处理方法签名:

@RequestMapping(method=RequestMethod.GET)<br></br> public Account setupForm() {<br></br> ...<br></br> }<p> @RequestMapping(method=RequestMethod.POST)</p><br></br> public void onSubmit(Account account) {<br></br> ...<br></br> }它们是非常有效的请求处理方法签名。第一个方法处理初始的 HTTP GET 请求,准备被编辑的数据,返回一个 Account 对象供 Spring MVC 表单标签使用。第二个方法在用户提交更改时处理随后的 HTTP POST 请求,并接收一个 Account 对象作为输入参数,它是 Spring MVC 的数据绑定机制用请求中的参数自动填充的。这是一个非常简单的程序模型。

Account 对象中含有要被编辑的数据。在 Spring MVC 的术语当中,Account 被称作是表单模型对象。这个对象必须通过某个名称让表单标签(还有数据绑定机制)知道它的存在。下面是从 JSP 页面中截取的部分代码,引用了一个名为“account”的表单模型对象:

<form:form modelAttribute=“account” method=“post”>
Account Number: <form:input path="number"/><form:errors path="number"/><br></br> ...<br></br> </form>即使我们没有在任何地方指定“account”的名称,这段 JSP 程序也会和上面所讲的方法签名协作的很好。这是因为 @MVC 用返回对象的类型名称作为默认值,因此一个 Account 类型的对象默认的就对应一个名为“account”的表单模型对象。如果默认的不合适,我们就可以用 @ModelAttribute 来改变它的名称,如下所示:

@RequestMapping(method=RequestMethod.GET)<br></br> public @ModelAttribute("account") SpecialAccount setupForm() {<br></br> ...<br></br> }<br></br> @RequestMapping(method=RequestMethod.POST)<br></br> public void update(@ModelAttribute("account") SpecialAccount account) {<br></br> ...<br></br> }@ModelAttribute 同样也可放在方法级的位置上,取得的效果稍有不同:

@ModelAttribute<br></br> public Account setupModelAttribute() {<br></br> ...<br></br> }此处 setupModelAttribute() 不是一个请求处理方法,而是任何请求处理方法被调用之前,用来准备表单项模型对象的一个方法。对那些熟悉 Spring MVC 的老用户来说,这和 SimpleFormController 的 formBackingObject() 方法是非常相似的。

最初的 GET 方法中我们得到一次表单模型对象,在随后的 POST 方法中当我们依靠数据绑定机制用用户所做的改变覆盖已有的 Account 对象时,我们会第二次得到它,在这种表单处理场景中把 @ModelAttribute 放在方法上是很有用的。当然,作为一种两次获得对象的替换方案,我们也可以在两次请求过程中将它保存进 HTTP 的会话(session),这就是我们下面将要分析的情况。

用 @SessionAttributes 存储属性

@SessionAttributes 注解可以用来指定请求过程中要放进 session 中的表单模型对象的名称或类型,下面是一些例子:

@Controller<br></br> @SessionAttributes("account")<br></br> public class AccountFormController {<br></br> ...<br></br> }<p> @Controller</p><br></br> @SessionAttributes(types = Account.class)<br></br> public class AccountFormController {<br></br> ...<br></br> }根据上面的注解,AccountFormController 会在初始的 GET 方法和随后的 POST 方法之间,把名为 “account”的表单模型对象(或者象第二个例子中的那样,把所有 Account 类型的表单模型对象)存入 HTTP 会话(session)中。不过,当有改变连续发生的时候,就应当把属性对象从会话中移除了。我们可以借助 SessionStatus 实例来做这件事,如果把它添加进 onSubmit 的方法签名中,@MVC 会完成这个任务:

@RequestMapping(method=RequestMethod.POST)<br></br> public void onSubmit(Account account, SessionStatus sessionStatus) {<br></br> ...<br></br> sessionStatus.setComplete(); // Clears @SessionAttributes<br></br> }### 定制数据绑定

有时数据绑定需要定制,例如我们也许需要指定必需填写的域,或者需要为日期、货币金额等类似事情注册定制的 PropertyEditors。用 @MVC 实现这些功能是非常容易的:

@InitBinder<br></br> public void initDataBinder(WebDataBinder binder) {<br></br> binder.setRequiredFields(new String[] {"number", "name"});<br></br> }@InitBinder 注解的方法可以访问 @MVC 用来绑定请求参数的 DataBinder 实例,它允许我们为每个控制器定制必须项。

数据绑定结果和验证

数据绑定也许会导致类似于类型转换或域缺失的错误。不管发生什么错误,我们都希望能返回到编辑的表单,让用户自行更正。要想实现这个目的,我们可直接在方法签名的表单模型对象后面追加一个 BindingResult 对象,例程如下:

@RequestMapping(method=RequestMethod.POST)<br></br> public ModelAndView onSubmit(Account account, BindingResult bindingResult) {<br></br> if (bindingResult.hasErrors()) {<br></br> ModelAndView mav = new ModelAndView();<br></br> mav.getModel().putAll(bindingResult.getModel());<br></br> return mav;<br></br> }<br></br> // Save the changes and redirect to the next view...<br></br> }发生错误时我们返回到出现问题的视图,并把从 BindingResult 得到的属性增加到模型上,这样特定域的错误就能够反馈给用户。要注意的是,我们并没有指定一个显式的视图名,而是允许 DispatcherServlet 依靠与入口 URI 路径信息匹配的默认视图名。

调用 Validator 对象并把 BindingResult 传给它,仅这一行代码就可实现验证操作。这允许我们在一个地方收集绑定和验证错误:

@RequestMapping(method=RequestMethod.POST)<br></br> public ModelAndView onSubmit(Account account, BindingResult bindingResult) {<br></br> accountValidator.validate(account, bindingResult);<br></br> if (bindingResult.hasErrors()) {<br></br> ModelAndView mav = new ModelAndView();<br></br> mav.getModel().putAll(bindingResult.getModel());<br></br> return mav;<br></br> }<br></br> // Save the changes and redirect to the next view...<br></br> }现在是时候结束我们的 Spring 2.5 Web 层注解(非正式称法为 @MVC)之旅了。

总结

Web 层的注解已经证明是相当有用的,不仅是因为它能够大大减少 XML 配置文件的数量,而且还在于它能成就一个可自由访问 Spring MVC 控制器技术的精致、灵活和简洁的程序设计模型。我们强烈推荐使用“惯例优先原则(convention-over-configuration)” 特性,以及以处理器映射为中心的策略给控制器派发请求,避免在源码中嵌入 URI 路径或是定义显式的视图名引用。

最后是本文没有讨论,但值得关注的一些非常重要的 Spring MVC 扩展。最新发布的 Spring Web Flow 版本 2 添加了一些特性,例如基于 JSF 视图的 Spring MVC、Spring 的 JavaScript 库,还有支持更先进编辑场景的高级状态和导航管理。

查看英文原文: Spring 2.5: New Features in Spring MVC


志愿参与 InfoQ 中文站内容建设,请邮件至 editors@cn.infoq.com 。也欢迎大家到 InfoQ 中文站用户讨论组参与我们的线上讨论。

2008 年 9 月 17 日 01:1819833
用户头像

发布了 127 篇内容, 共 36.2 次阅读, 收获喜欢 0 次。

关注

评论

发布
暂无评论
发现更多内容

人民自己创造的节日 | 经济

chaozh

Spring Cloud微服务技术栈:搭建高可用Eureka Server、服务注册与发现

独钓寒江雪

Spring Cloud

Ubuntu 20.04 上安装和配置 VNC

酱紫的小白兔

Flask 中的 Sessions

Leetao

Python flask Web框架

压测工具如何选择?

elfkingw

架构师训练营第7周作业

时来运转

架构师训练营第7周总结

时来运转

深入Java Web技术内幕(一)浅析Web请求过程

独钓寒江雪

Java

JVM 类加载机制

Alex🐒

JVM 深入理解JVM

如何挑选编程笔记本 | 数码产品

chaozh

JVM 垃圾回收机制

Alex🐒

JVM 深入理解JVM

深入理解 JS 中的 this

Verlime

Java 前端

深入 Java Web 技术内幕(二)浅析DNS域名解析过程

独钓寒江雪

DNS 域名解析

神国统治者 | 中国古神话

chaozh

腾讯的背水一战还是奋力一搏? | 互联网

chaozh

JVM 对象内存布局

Alex🐒

JVM 深入理解JVM

深入理解 JS 中的变量提升

Verlime

Java 前端

JVM 垃圾回收器 CMS

Alex🐒

JVM 深入理解JVM GC

架构师训练营 - 命题作业 第 7 周

铁血杰克

JVM 垃圾回收器 G1

Alex🐒

JVM 深入理解JVM GC

彻底弄懂C++11右值引用 | 技术总结

chaozh

c++

女娲造物与补天 | 中国古神话

chaozh

每个现代人都应该知道的包豪斯| 艺术

chaozh

Presto性能调优的五大技巧

华为云开发者社区

大数据 数据 内存 存储 华为云

【干货分享】通过命令操作来学习Git

独钓寒江雪

git git入门

优雅地利用c++编程从1乘到20 | 技术总结

chaozh

c++

创世 | 中国古神话

chaozh

神话

架构师训练营第七周作业-性能测试

sunnywhy

程序员面试必备战衣 | T恤衫 - 程序员穿搭

chaozh

GEEK

JVM 运行时数据区

Alex🐒

JVM 深入理解JVM

深入理解 JS 参数传递

Verlime

Java 前端

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

Spring 2.5:Spring MVC中的新特性-InfoQ