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
public class ClinicController {
private final Clinic clinic;
@Autowired
public ClinicController(Clinic clinic) {
this.clinic = clinic;
}
...
@Controller表明ClinicController是Web層組件,@Autowired請求一個被依賴注入的Clinic實例。這個例子只需要少量的XML語句就能使容器識別兩個注解,並限定組件的掃描范圍:
<context:component-scan base-package="org.springframework.samples.petclinic"/>
這對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 {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse
response) throws Exception;
}
所有的Spring MVC控制器要麼直接實現Controller接口,要麼就得擴展類似AbstractController、 SimpleFormController、 MultiActionController或AbstractWizardFormController這樣的基類實現。正是Controller接口允許Spring MVC的DispatcherServlet把所有上述對象都看作是“處理器(handlers)”,並在一個名為 SimpleControllerHandlerAdapter的適配器的幫助下調用它們。
@MVC從三個重要的方面改變了這個程序設計模型:
不需要任何接口或者基類。
允許有任意數量的請求處理方法。
在方法簽名上具有高度的靈活性。
考慮到以上三個要點,就可以說很公平的說@MVC不僅僅是個替換方案了,它將會是Spring MVC的控制器技術演變過程中下一個重要步驟。
DispatcherServlet在名為AnnotationMethodHandlerAdapter的適配器幫助下調用被注解的控制器。正是這個適配器做了大量工作支持我們此後將會討論的注解,同時也是它有效的取代了對於控制器基類的需求。
@RequestMapping簡介
我們還是從一個類似於傳統的Spring MVC Controller控制器開始:
@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
...
靈活的請求處理方法簽名
我們曾經承諾過要提供靈活的方法簽名,現在來看一下成果。輸入的參數中移除了響應對象,增加了一個代表模型的Map;返回的不再是ModelAndView,而是一個字符串,指明呈現響應時要用的視圖名字:
@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,或者什麼都不返回。你可以查看@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類型,你可通過定制的PropertyEditors 來擴展它的范圍。下面是一些例子,其中包括了必需和可選參數:
@RequestParam(value="number", required=false) String number
@RequestParam("id") Long id
@RequestParam("balance") double balance
@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
@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));
}
...
方法匹配的請求是“/accounts/show”,依據的是類級別的@RequestMapping指定的匹配路徑“/accounts/*”和方法名“show”。
消除類級別的請求映射
Web層注解頻遭诟病是有事實依據的,那就是嵌入源代碼的URI路徑。這個問題很好矯正,URI路徑和控制器類之間的匹配關系用XML配置文件去管理,只在方法級的映射中使用@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請求,准備被編輯的數據,返回一個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"/>
...
</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()不是一個請求處理方法,而是任何請求處理方法被調用之前,用來准備表單項模型對象的一個方法。對那些熟悉 Spring MVC的老用戶來說,這和SimpleFormController的formBackingObject()方法是非常相似的。
最初的GET方法中我們得到一次表單模型對象,在隨後的POST方法中當我們依靠數據綁定機制用用戶所做的改變覆蓋已有的Account對象時,我們會第二次得到它,在這種表單處理場景中把@ModelAttribute放在方法上是很有用的。當然,作為一種兩次獲得對象的替換方案,我們也可以在兩次請求過程中將它保存進HTTP的會話(session),這就是我們下面將要分析的情況。
用@SessionAttributes存儲屬性
@SessionAttributes注解可以用來指定請求過程中要放進session中的表單模型對象的名稱或類型,下面是一些例子:
@Controller
@SessionAttributes("account")
public class AccountFormController {
...
}
@Controller
@SessionAttributes(types = Account.class)
public class AccountFormController {
...
}
根據上面的注解,AccountFormController會在初始的GET方法和隨後的POST方法之間,把名為 “account”的表單模型對象(或者象第二個例子中的那樣,把所有Account類型的表單模型對象)存入HTTP會話(session)中。不過,當有改變連續發生的時候,就應當把屬性對象從會話中移除了。我們可以借助SessionStatus實例來做這件事,如果把它添加進onSubmit的方法簽名中,@MVC會完成這個任務:
@RequestMapping(method=RequestMethod.POST)
public void onSubmit(Account account, SessionStatus sessionStatus) {
...
sessionStatus.setComplete(); // Clears @SessionAttributes
}
定制數據綁定
有時數據綁定需要定制,例如我們也許需要指定必需填寫的域,或者需要為日期、貨幣金額等類似事情注冊定制的PropertyEditors。用@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...
}
現在是時候結束我們的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。