自成立以来,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层中的注释支持。 最后一篇文章将重点介绍可用于集成和测试的其他功能。
该系列的第1部分演示了如何使用Java批注代替XML来配置Spring管理的对象和进行依赖项注入。 这里再次是一个示例:
@Controller
public class ClinicController {
private final Clinic clinic;
@Autowired
public ClinicController(Clinic clinic) {
this.clinic = clinic;
}
...
@Controller表示ClinicController是Web层组件。 @Autowired请求诊所实例被依赖项注入。 此示例仅需要少量XML即可识别两个注释并限制组件扫描的范围:
<context:component-scan base-package="org.springframework.samples.petclinic"/>
对于Web层来说,这是个好消息,在Web层中,Spring XML配置往往比下面的层更冗长,甚至价值更低。 控制器拥有许多属性,例如视图名称,表单对象名称和验证器类型,这些属性与配置有关,而与依赖性注入无关。 有一些方法可以通过bean定义继承或避免对不经常更改的属性进行配置来有效地管理此类配置。 但是根据我的经验,许多开发人员没有这样做,结果是XML超出了必要。 因此,@Controller和@Autowired可以对Web层配置产生非常积极的影响。
在本系列的第2部分中,我们将继续讨论Web层的Spring 2.5批注。 这些注释被非正式地称为@MVC,它是对Spring MVC和Spring Portlet MVC的引用。 确实,本文讨论的大多数功能都适用于这两者。
从Controller到@Controller
与第1部分中讨论的注释相反,@ MVC不仅仅是配置的替代形式。 考虑一下Spring MVC控制器的这个众所周知的签名:
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse
response) throws Exception;
}
所有Spring MVC控制器要么直接实现Controller,要么从可用的基类实现之一扩展,例如AbstractController,SimpleFormController,MultiActionController或AbstractWizardFormController。 正是这个接口允许Spring MVC的DispatcherServlet将以上所有内容视为“处理程序”,并在名为SimpleControllerHandlerAdapter的适配器的帮助下调用它们。
@MVC以3种重要方式更改了此编程模型。
- 它没有任何接口或基类要求。
- 它允许任何数量的请求处理方法。
- 它使方法签名具有高度的灵活性。
鉴于这三个,可以说@MVC不仅仅是一个替代方案。 这是Spring MVC控制器技术发展的下一步。
DispatcherServlet在名为AnnotationMethodHandlerAdapter的适配器的帮助下调用带注释的控制器。 该适配器完成了大部分工作,以支持下文讨论的注释。 也是这个适配器有效地代替了对基类控制器的需求。
介绍@RequestMapping
我们从外观类似于传统Spring MVC控制器的控制器开始:
@Controller
public class AccountsController {
private AccountRepository accountRepository;
@Autowired
public AccountsController(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@RequestMapping("/accounts/show")
public ModelAndView show(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
ModelAndView mav = new ModelAndView("/WEB-INF/views/accounts/show.jsp");
mav.addObject("account", accountRepository.findAccount(number));
return mav;
}
}
此处的区别是此控制器未扩展Controller接口,并且还使用@RequestMapping批注指示show()是映射到URI路径“ / accounts / show”的请求处理方法。 其余代码对于Spring MVC控制器而言是典型的。
在将上述方法完全转换为@MVC之后,我们将返回@RequestMapping,但在继续之前,值得一提的是上述请求映射URI也匹配具有任何扩展名的URI路径,例如:
/accounts/show.htm
/accounts/show.xls
/accounts/show.pdf
...
灵活的请求处理方法签名
我们承诺使用灵活的方法签名。 让我们继续,从输入参数中删除响应对象,而不是返回ModelAndView,我们将添加一个Map作为代表我们模型的输入参数。 另外,我们将返回一个String来指示渲染响应时要使用的视图名称:
@RequestMapping("/accounts/show")
public String show(HttpServletRequest request, Map<String, Object> model)
throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
model.put("account", accountRepository.findAccount(number));
return "/WEB-INF/views/accounts/show.jsp";
}
Map输入参数称为“隐式”模型,在调用该方法之前为我们方便地创建了该参数。 向其添加键值对使数据可用于在视图中呈现-在本例中为show.jsp页面。
@MVC允许将多种类型用作输入参数,例如HttpServletRequest / HttpServletResponse,HttpSession,Locale,InputStream,OutputStream,File []等。 可以以任何顺序提供它们。 它还允许许多返回类型,例如ModelAndView,Map,String和void。 检查@RequestMapping的JavaDoc以获取受支持的输入和输出参数类型的完整列表。
一种有趣的情况是,当方法未指定视图时(例如,返回类型为void)会发生什么。 在这种情况下,DispatcherServlet的约定是重用请求URI的路径信息,以除去前导斜杠和扩展名。 让我们将返回类型更改为void:
@RequestMapping("/accounts/show")
public void show(HttpServletRequest request, Map<String, Object> model) throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
model.put("account", accountRepository.findAccount(number));
}
给定此请求处理方法和“ / accounts / show”的请求映射,我们可以预期DispatcherServlet会使用默认的视图名称“ accounts / show”,当与合适的视图解析器(例如下面的视图解析器)结合使用时-产生与以前相同的结果:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
强烈建议依赖于视图名称的约定,因为这有助于从控制器中消除硬编码的视图名称。 如果您需要自定义DispatcherServlet派生默认视图名称的方式,请在Servlet上下文中配置自己的RequestToViewNameTranslator实现,为其提供bean ID“ viewNameTranslator”。
使用@RequestParam提取和解析参数
@MVC的另一个功能是它能够提取和解析请求参数。 让我们继续重构我们的方法并添加@RequestParam批注:
@RequestMapping("/accounts/show")
public void show(@RequestParam("number") String number, Map<String, Object> model) {
model.put("account", accountRepository.findAccount(number));
}
在这里,@ RequestParam批注有助于提取名为“ number”的String参数,并将其作为输入参数传递。 @RequestParam支持类型转换以及必需参数和可选参数。 对于类型转换,所有基本Java类型均受支持,您可以使用自定义PropertyEditor对其进行扩展。 以下是一些其他示例,包括必需和可选参数:
@RequestParam(value="number", required=false) String number
@RequestParam("id") Long id
@RequestParam("balance") double balance
@RequestParam double amount
请注意,上一个示例如何不提供显式参数名称。 仅当代码使用调试符号编译时,这将导致提取一个称为“金额”的参数。 如果代码未使用调试符号进行编译,则将抛出IllegalStateException,因为没有足够的信息来从请求中提取参数。 因此,最好显式指定参数名称。
@RequestMapping续
将@RequestMapping放在类级别是合理的,并与方法级别的@RequestMapping注释结合使用,以达到缩小选择范围的效果。 这里有些例子。
班级:
RequestMapping("/accounts/*")
方法级别:
@RequestMapping(value =“ delete”,method = RequestMethod.POST)第一个方法级别的请求映射与类级别的映射相结合,在HTTP方法为POST的情况下匹配到“ / accounts / delete”。 第二个要求增加了对请求参数“ type”和值“ checking”的请求参数的要求。 第三种方法根本不指定路径。 该方法与所有HTTP方法匹配,并且在必要时将使用其方法名称。 让我们更改方法以依赖于方法名称解析,如下所示:
@Controller
@RequestMapping("/accounts/*")
public class AccountsController {
@RequestMapping(method=RequestMethod.GET)
public void show(@RequestParam("number") String number, Map<String, Object> model)
{
model.put("account", accountRepository.findAccount(number));
}
...
该方法基于类级@RequestMapping为“ / accounts / *”和方法名称为“ show”来匹配对“ / accounts / show”的请求。
删除类级别的请求映射
Web层中对注释的一种常见反对意见是URI路径嵌入在源代码中。 通过使用XML配置的策略(用于将URI路径与控制器类进行匹配)和@RequestMapping注释(仅用于方法级映射),可以轻松地解决此问题。
我们将配置一个ControllerClassNameHandlerMapping,它使用依赖于控制器类名的约定将URI路径映射到控制器:
<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>
现在,对“ / accounts / *”的请求将与AccountsController匹配。 与方法级别的@RequestMapping批注结合使用时效果很好,后者通过向其添加方法名称来完成上述映射。 此外,由于我们的方法不返回视图名称,因此我们现在使用一种与类名称,方法名称,URI路径和视图名称匹配的约定。
这是@Controller完全转换为@MVC后的样子:
@Controller
public class AccountsController {
private AccountRepository accountRepository;
@Autowired
public AccountsController(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@RequestMapping(method=RequestMethod.GET)
public void show(@RequestParam("number") String number, Map<String, Object> model)
{
model.put("account", accountRepository.findAccount(number));
}
...
以及支持的XML:
<context:component-scan base-package="com.abc.accounts"/>
<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
如您所见,几乎没有XML,没有在注释中嵌入URI路径,没有显式视图名称,请求处理方法仅包含一行,方法签名恰好符合我们的需求,并且可以轻松添加其他请求处理方法。 所有这些优点都不需要基类,也不需要XML-至少没有一个直接归因于此控制器。
也许您可以开始了解这种编程模型的有效性。
@MVC表单处理
典型的表单处理方案包括检索要编辑的对象,以编辑模式显示其持有的数据,允许用户提交,最后验证并保存更改。 为了帮助完成所有这些工作,Spring MVC提供了以下功能:一种数据绑定机制,用于从请求参数中完全填充一个对象;支持处理错误和进行验证;一个JSP表单标签库;以及基类控制器。 使用@MVC时,由于以下注释,不再需要基类控制器,因此没有任何更改:@ ModelAttribute,@ InitBinder和@SessionAttributes。
@ModelAttribute批注
看一下这些请求处理方法签名:
@RequestMapping(method=RequestMethod.GET)
public Account setupForm() {
...
}
@RequestMapping(method=RequestMethod.POST)
public void onSubmit(Account account) {
...
}
它们是完全有效的请求处理方法签名。 第一种方法处理初始HTTP GET。 它准备要编辑的数据,并返回一个帐户,供Spring MVC的表单标签使用。 当用户提交更改时,第二种方法处理后续的HTTP POST。 它接受使用Spring MVC的数据绑定机制从请求参数自动填充的Account。 这是一个非常简单的编程模型。
Account对象保存要编辑的数据。 在Spring MVC术语中,帐户称为表单模型对象。 必须以某种名称使该对象可用于表单标签(以及数据绑定机制)。 以下是JSP页面的摘录,该页面引用了名为“帐户”的表单模型对象:
<form:form modelAttribute="account" method="post">
Account Number: <form:input path="number"/><form:errors path="number"/>
...
</form>
即使我们未在任何地方指定名称“ account”,此JSP代码段也可以与上述方法签名很好地配合使用。 这是因为@MVC使用返回的对象类型的名称来选择默认名称。 因此,默认情况下,类型为Account的对象会生成一个名为“ account”的表单模型对象。 如果默认值不合适,我们可以使用@ModelAttribute更改其名称,如下所示:
@RequestMapping(method=RequestMethod.GET)
public @ModelAttribute("account") SpecialAccount setupForm() {
...
}
@RequestMapping(method=RequestMethod.POST)
public void update(@ModelAttribute("account") SpecialAccount account) {
...
}
也可以将@ModelAttribute放在方法级别,以产生稍微不同的效果:
@ModelAttribute
public Account setupModelAttribute() {
...
}
在此,setupModelAttribute()不是请求处理方法。 相反,它是用于在调用任何其他请求处理方法之前准备表单模型对象的方法。 对于熟悉的人来说,这与SimpleFormController的formBackingObject()方法非常相似。
将@ModelAttribute放在方法上在表单处理场景中很有用,在这种情况下,当我们希望数据绑定将现有的Account对象与用户的更改覆盖在一起时,我们在初始GET期间检索一次表单模型对象,然后在后续的POST中再次检索表单模型对象。 当然,两次检索对象的替代方法是将其存储在两个请求之间的HTTP会话中。 这就是我们接下来要研究的内容。
使用@SessionAttributes存储属性
@SessionAttributes批注可用于指定在两次请求之间的会话中保留的表单模型对象的名称或类型。 以下是几个示例:
@Controller
@SessionAttributes("account")
public class AccountFormController {
...
}
@Controller
@SessionAttributes(types = Account.class)
public class AccountFormController {
...
}
使用此注释,AccountFormController在初始GET和后续POST之间的HTTP会话中存储名为“ account”的表单模型对象(或在第二种情况下为Account类型的任何表单模型对象)。 更改保留后,应从会话中删除该属性。 我们可以在SessionStatus实例的帮助下完成此操作,如果将@MVC添加到onSubmit方法的方法签名中,它将通过:
@RequestMapping(method=RequestMethod.POST)
public void onSubmit(Account account, SessionStatus sessionStatus) {
...
sessionStatus.setComplete(); // Clears @SessionAttributes
}
自定义DataBinder
有时数据绑定需要自定义。 例如,我们可能需要指定必填字段或为日期,货币金额等注册自定义PropertyEditor。 使用@MVC,这很容易做到:
@InitBinder
public void initDataBinder(WebDataBinder binder) {
binder.setRequiredFields(new String[] {"number", "name"});
}
使用@InitBinder注释的方法可以访问@MVC用于绑定请求参数的DataBinder实例。 它使我们能够为每个控制器进行必要的自定义。
数据绑定结果和验证
数据绑定可能会导致错误,例如类型转换失败或缺少字段。 如果确实发生任何错误,我们想返回到编辑表单,并允许用户进行更正。 为此,我们在表单模型对象之后立即将BindingResult对象添加到方法签名中。 这是一个例子:
@RequestMapping(method=RequestMethod.POST)
public ModelAndView onSubmit(Account account, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
ModelAndView mav = new ModelAndView();
mav.getModel().putAll(bindingResult.getModel());
return mav;
}
// Save the changes and redirect to the next view...
}
如果发生错误,我们返回视图,这是由于将BindingResult中的属性添加到了模型中,因此可以将特定于字段的错误显示回给用户。 请注意,我们没有指定明确的视图名称。 相反,我们允许DispatcherServlet依靠默认的视图名称,该名称将与传入URI的路径信息匹配。
验证只需要另外一行即可调用Validator对象,该对象将BindingResult传递给它。 这使我们可以在一个地方累积绑定和验证错误:
@RequestMapping(method=RequestMethod.POST)
public ModelAndView onSubmit(Account account, BindingResult bindingResult) {
accountValidator.validate(account, bindingResult);
if (bindingResult.hasErrors()) {
ModelAndView mav = new ModelAndView();
mav.getModel().putAll(bindingResult.getModel());
return mav;
}
// Save the changes and redirect to the next view...
}
到此,我们结束了对Web层的Spring 2.5批注的浏览,非正式地将其称为@MVC。
摘要
Web层中的注释已被证明是非常有益的。 它们不仅大大减少了XML配置的数量,而且还可以通过完全访问Spring MVC的控制器技术来实现优雅,灵活和简单的编程模型。 强烈建议使用配置约定约定以及集中的处理程序映射策略将请求委派给控制器,以避免将URI路径嵌入源代码或定义对视图名称的显式引用。
最后,尽管本文没有讨论,但值得一提的是非常重要的Spring MVC扩展。 最近发布的Spring Web Flow版本2添加了功能,例如基于Spring MVC的JSF视图,Spring JavaScript库以及支持更高级编辑场景的高级状态和导航管理。