Spring MVC開發流程詳解
有了上文的初始化配置,開發Spring MVC流程並不困難。開發Spring MVC程序,需要掌握Spring MVC的組件和流程,所以開發過程中也會貫穿着Spring MVC的運行流程。
在目前的開發過程中,大部分都會採用註解的開發方式。使用註解在Spring MVC中十分簡單,主要是以一個註解@Controller標註,一般只需要通過掃描配置,就能夠將其掃描處理,只是往往還要結合註解@RequestMapping去配置它。@RequestMapping可以配置在類或者方法之上,它的作用是指定URI和哪個類(或者方法)作爲一個處理請求的處理器,爲了更加靈活,Spring MVC還定義了處理器的攔截器,當啓動Spring MVC的時候,Spring MVC就會去解析@Controller中@RequestMapping的配置,再結合所配置的攔截器,這樣它就會組成多個攔截器和一個控制器的形式,存放到一個HandlerMapping中去。當請求來到服務器,首先是通過請求信息找到對應的HandlerMapping,進而可以找到對應的攔截器和處理器,這樣就能夠運行對應的控制器和攔截器。
1. 配置@RequestMapping
@RequestMapping的源碼如下:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
/**
* Assign a name to this mapping.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used on both levels, a combined name is derived by concatenation
* with "#" as separator.
* @see org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder
* @see org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy
*/
// 請求路徑
String name() default "";
/**
* The primary mapping expressed by this annotation.
* <p>In a Servlet environment this is an alias for {@link #path}.
* For example {@code @RequestMapping("/foo")} is equivalent to
* {@code @RequestMapping(path="/foo")}.
* <p>In a Portlet environment this is the mapped portlet modes
* (i.e. "EDIT", "VIEW", "HELP" or any custom modes).
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
*/
// 請求路徑,可以是數組
@AliasFor("path")
String[] value() default {};
/**
* In a Servlet environment only: the path mapping URIs (e.g. "/myPath.do").
* Ant-style path patterns are also supported (e.g. "/myPath/*.do").
* At the method level, relative paths (e.g. "edit.do") are supported within
* the primary mapping expressed at the type level. Path mapping URIs may
* contain placeholders (e.g. "/${connect}")
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
* @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
* @since 4.2
*/
// 請求路徑,數組
@AliasFor("value")
String[] path() default {};
/**
* The HTTP request methods to map to, narrowing the primary mapping:
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this HTTP method restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* <p>Supported for Servlet environments as well as Portlet 2.0 environments.
*/
// 請求類型,比如是HTTP的GET請求還是POST請求等,HTTP請求枚舉取值範圍爲:GET、HEAD、PUT、PATCH、DELETE、OPTIONS、TRACE,常用的是GET和POST請求
RequestMethod[] method() default {};
/**
* The parameters of the mapped request, narrowing the primary mapping.
* <p>Same format for any environment: a sequence of "myParam=myValue" style
* expressions, with a request only mapped if each such parameter is found
* to have the given value. Expressions can be negated by using the "!=" operator,
* as in "myParam!=myValue". "myParam" style expressions are also supported,
* with such parameters having to be present in the request (allowed to have
* any value). Finally, "!myParam" style expressions indicate that the
* specified parameter is <i>not</i> supposed to be present in the request.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this parameter restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* <p>In a Servlet environment, parameter mappings are considered as restrictions
* that are enforced at the type level. The primary path mapping (i.e. the
* specified URI value) still has to uniquely identify the target handler, with
* parameter mappings simply expressing preconditions for invoking the handler.
* <p>In a Portlet environment, parameters are taken into account as mapping
* differentiators, i.e. the primary portlet mode mapping plus the parameter
* conditions uniquely identify the target handler. Different handlers may be
* mapped onto the same portlet mode, as long as their parameter mappings differ.
*/
// 請求參數,當請求帶有配置的參數時,才匹配處理器
String[] params() default {};
/**
* The headers of the mapped request, narrowing the primary mapping.
* <p>Same format for any environment: a sequence of "My-Header=myValue" style
* expressions, with a request only mapped if each such header is found
* to have the given value. Expressions can be negated by using the "!=" operator,
* as in "My-Header!=myValue". "My-Header" style expressions are also supported,
* with such headers having to be present in the request (allowed to have
* any value). Finally, "!My-Header" style expressions indicate that the
* specified header is <i>not</i> supposed to be present in the request.
* <p>Also supports media type wildcards (*), for headers such as Accept
* and Content-Type. For instance,
* <pre class="code">
* @RequestMapping(value = "/something", headers = "content-type=text/*")
* </pre>
* will match requests with a Content-Type of "text/html", "text/plain", etc.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this header restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* <p>Maps against HttpServletRequest headers in a Servlet environment,
* and against PortletRequest properties in a Portlet 2.0 environment.
* @see org.springframework.http.MediaType
*/
// 請求頭,當HTTP請求頭爲配置項時,才匹配處理器
String[] headers() default {};
/**
* The consumable media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Content-Type} matches one of these media types.
* Examples:
* <pre class="code">
* consumes = "text/plain"
* consumes = {"text/plain", "application/*"}
* </pre>
* Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Content-Type} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this consumes restriction.
* @see org.springframework.http.MediaType
* @see javax.servlet.http.HttpServletRequest#getContentType()
*/
// 請求類型爲配置類型才匹配處理器
String[] consumes() default {};
/**
* The producible media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Accept} matches one of these media types.
* Examples:
* <pre class="code">
* produces = "text/plain"
* produces = {"text/plain", "application/*"}
* produces = "application/json; charset=UTF-8"
* </pre>
* <p>It affects the actual content type written, for example to produce a JSON response
* with UTF-8 encoding, {@code "application/json; charset=UTF-8"} should be used.
* <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Accept} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this produces restriction.
* @see org.springframework.http.MediaType
*/
// 處理器之後的響應用戶的結果類型,比如{“application/json;charset=UTF-8”,"text/plain","application/*"}
String[] produces() default {};
}
這裏最常用到的時請求路徑和請求類型,其他的大部分作爲限定項,根據需要進行配置。例如在MyController中加入一個index2方法,代碼如下:
@RequestMapping(value = "/index2",method = RequestMethod.GET)
public ModelAndView index2() {
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
這樣對於/my/index2.do的HTTP GET請求提供響應了。
2. 控制器的開發
控制器開發是Spring MVC的核心內容,其步驟一般會分爲3步。
- 獲取請求參數
- 處理業務邏輯
- 綁定模型和視圖
2.1 獲取請求參數
在Spring MVC中接收參數的方法很多,建議不要使用Servlet容器所給予的API,因爲這樣控制器將會依賴於Servlet容器,比如:
@RequestMapping(value = "/index2",method = RequestMethod.GET)
public ModelAndView index2(HttpSession session, HttpServletRequest request) {
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
Spring MVC會自動解析代碼中的方法參數session、request,然後傳遞關於Servlet容器的API,所以是可以獲取到的。通過request或者session都可以很容易地得到HTTP請求過來的參數,這固然是一個方法,但並非一個好的方法。因爲如果這樣做了,那麼對於index2方法而言,它就和Servlet容器緊密關聯了,不利於擴展和測試。爲了給予更好的靈活性,Spring MVC給予了更多的方法和註解以獲取參數。
如果要獲取一個HTTP請求的參數——id,它是一個長整型,那麼可以使用註解@RequestParam來獲取它,代碼修改爲:
@RequestMapping(value = "/index2",method = RequestMethod.GET)
public ModelAndView index2(@RequestParam("id") Long id) {
System.out.println("params[id] = " + id);
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
在默認的情況下對於註解了@RequestParam的參數而言,它要求參數不能爲空,也就是當獲取不到HTTP請求參數的時候,Spring MVC將會拋出異常。有時候還希望給參數一個默認值,爲了解決這樣的困難,@RequestParam還給了兩個有用的配置項:
- required是一個布爾值(boolean),默認是true,也就是不允許參數爲空,如果要允許爲空,則配置它爲false。
- defaultValue的默認值爲"\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n",可以通過配置修改它爲想要的內容。
獲取session中的內容,假設當前的Session中設置了userName,那麼應該如何獲取它呢?Spring MVC還提供了註解@SessionAttribute去從Session中獲取對應的數據。代碼如下:
@RequestMapping(value = "/index3",method = RequestMethod.GET)
public ModelAndView index3(@SessionAttribute("userName") String userName) {
System.out.println("session[userName] = " + userName);
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
2.2 實現邏輯和綁定視圖
一般而言,實現的邏輯和數據庫有關聯,如果採用XML的方式,那麼只需要在applicationContext.xml中配置關於數據庫的部分就可以了;如果使用Java配置的方式,那麼需要在配置類WebConfig中的getRootConfigClasses加入對應的配置類即可。
有時候在使用第三方包開發的時候,使用XML方式會比註解方式方便一些,因爲不需要設計太多關於第三方包內容的Java代碼,甚至可以是混合使用,示例配置如下:
<?xml version='1.0' encoding='UTF-8' ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 使用註解驅動 -->
<context:annotation-config />
<!-- 數據庫連接池 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/chapter14" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="maxActive" value="255" />
<property name="maxIdle" value="5" />
<property name="maxWait" value="10000" />
</bean>
<!-- 集成mybatis -->
<bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:/mybatis/mybatis-config.xml" />
</bean>
<!-- 配置數據源事務管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 採用自動掃描方式創建mapper bean -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ssm.chapter14" />
<property name="SqlSessionFactory" ref="SqlSessionFactory" />
<property name="annotationClass" value="org.springframework.stereotype.Repository" />
</bean>
</beans>
假設上述的XML配置文件,已經通過掃描的方式初始化了一個Spring IoC容器中的Bean——RoleService,而且它提供了一個參數爲long型的方法getRole來獲取角色,那麼可以通過自動裝配的方式在控制器中注入它。角色控制器代碼如下:
/**************import ***************/
@Controller
@RequestMapping("/role")
public class RoleController {
// 注入角色服務類
@Autowired
private RoleService roleService = null;
@RequestMapping(value = "/getRole", method = RequestMethod.GET)
public ModelAndView getRole(@RequestParam("id") Long id) {
Role role = roleService.getRole(id);
ModelAndView mv = new ModelAndView();
mv.setViewName("roleDetails");
// 給數據模型添加一個角色對象
mv.addObject("role", role);
return mv;
}
}
從代碼中注入了RoleService,這樣就可以通過這個服務類使用傳遞的參數id來獲取角色,最後把查詢出來的角色添加給模型和視圖以便將來使用。
3. 視圖渲染
一般地,Spring MVC會默認使用JstlView進行渲染,也就是它將查詢出來的模型綁定到JSTL(JSP標準標籤庫)模型中,這樣通過JSTL就可以把數據模型在JSP中讀出展示數據了,在Spring MVC中,還存在着大量的視圖可供使用,這樣就可以方便地將數據渲染到視圖中,用以響應用戶的請求。
在上文的代碼中使用了roleDetails的視圖名,根據配置,它會使用文件/WEB-INF/jsp/roleDetail.jsp去響應,也就是要在這個文件中編寫JSTL標籤將模型數據讀出即可,例如:
<%@ page pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>out標籤的使用</title>
</head>
<body>
</body>
<center>
<table border="1">
<tr>
<td>標籤</td>
<td>值</td>
</tr>
<tr>
<td>角色編號</td>
<td><c:out value="${role.id}"></c:out></td>
</tr>
<tr>
<td>角色名稱</td>
<td><c:out value="${role.roleName}"></c:out></td>
</tr>
<tr>
<td>角色備註</td>
<td><c:out value="${role.note}"></c:out></td>
</tr>
</table>
</center>
</html>
在目前的前端技術中,普遍使用Ajax技術,在這樣的情況下,往往後臺需要返回JSON數據給前端使用,對此,Spring MVC在模型和視圖也給予了良好的支持。getRole的代碼修改爲:
// 獲取角色
@RequestMapping(value = "/getRole2", method = RequestMethod.GET)
public ModelAndView getRole2(@RequestParam("id") Long id) {
Role role = roleService.getRole(id);
ModelAndView mv = new ModelAndView();
mv.addObject("role", role);
// 指定視圖類型
mv.setView(new MappingJackson2JsonView());
return mv;
}
代碼中視圖類型爲MappingJackson2JsonView,這就要下載關於Jackson2的包。由於這是一個JSON視圖,這樣Spring MVC就會通過這個視圖去渲染所需的結果。於是會在我們請求後得到需要的JSON數據,提供給Ajax異步請求使用了。它的執行流程如下:
只是這不是將結果變爲JSON的唯一方法,使用註解@ResponeBody是更爲簡單和廣泛使用的方法。