一、控制器定義
控制器提供訪問應用程序的行為,通常通過服務接口定義或注解定義兩種方法實現。 控制器解析用戶的請求並將其轉換為一個模型。在Spring MVC中一個控制器可以包含多個Action(動作、方法)。
1.1、實現接口Controller定義控制器
Controller是一個接口,處在包org.springframework.web.servlet.mvc下,接口中只有一個未實現的方法,具體的接口如下所示:
package org.springframework.web.servlet.mvc;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
//實現該接口的類獲得控制器功能與類型, 解析用戶的請求並將其轉換為一個模型
public interface Controller {
//處理請求且返回一個模型與視圖對象
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
在自定義控制器前先創建一個基於maven的web項目,添加包的依賴,pom.xml文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zhangguo</groupId>
<artifactId>SpringMVC02</artifactId>
<version>0.0.1</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.3.0.RELEASE</spring.version>
</properties>
<dependencies>
<!--Spring框架核心庫 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- JSTL -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Servlet核心包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<!--JSP應用程序接口 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
如果不配置scope,會把jar包發布,會跟容器里的jar包沖突、scope要設置為provided,由容器提供,不會發布(或者不配這兩個依賴,在項目的Java BuildPath的Libraries里添加Server Runtime)目前scope可以使用5個值:
compile:缺省值,適用於所有階段,會隨着項目一起發布。
provided:類似compile,期望JDK、容器或使用者會提供這個依賴。如servlet.jar。
runtime:只在運行時使用,如JDBC驅動,適用運行和測試階段。test,只在測試時使用,用於編譯和運行測試代碼。不會隨項目發布。
system:類似provided,需要顯式提供包含依賴的jar,Maven不會在Repository中查找它。
創建一個名為Foo的類,實現接口Controller,重寫handleRequest方法,代碼如下:
package com.zhangguo.springmvc02.controllers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
/*
* 定義控制器
*/
public class FooController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
//返回一個模型視圖對象,指定路徑,指定模型的名稱為message,值為一段字符串
return new ModelAndView("foo/index", "message", "Hello,我是通過實現接口定義的一個控制器");
}
}
在WEB-INF/views/foo目錄下創建一個名為index.jsp的視圖,內容如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Foo</title>
</head>
<body>
${message}
</body>
</html>
修改springmvc-servlet.xml配置文件,增加一個控制器bean的聲明,具體內容如下:
<?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: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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd">
<!-- 自動掃描包,實現支持注解的IOC -->
<context:component-scan base-package="com.zhangguo.springmvc02" />
<!-- Spring MVC不處理靜態資源 -->
<mvc:default-servlet-handler />
<!-- 支持mvc注解驅動 -->
<mvc:annotation-driven />
<!-- 視圖解析器 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
id="internalResourceViewResolver">
<!-- 前綴 -->
<property name="prefix" value="/WEB-INF/views/" />
<!-- 后綴 -->
<property name="suffix" value=".jsp" />
</bean>
<bean name="/foo" class="com.zhangguo.springmvc02.controllers.FooController"></bean>
</beans>
基中name是訪問路徑,class是自定義的控制器的全名稱。運行后的結果如下:

小結:實現接口Controller定義控制器是較老的辦法,缺點是:一個控制器中只有一個Action,如果要多個Action則需要定義多個Controller;定義的方式比較麻煩;Spring 2.5以后采用注解的方式定義解決這引起問題。
1.2、使用注解@Controller定義控制器
org.springframework.stereotype.Controller注解類型用於聲明Spring類的實例是一個控制器(在講IOC時還提到了另外3個注解);Spring可以使用掃描機制來找到應用程序中所有基於注解的控制器類,為了保證Spring能找到你的控制器,需要在配置文件中聲明組件掃描。
創建一個名了Bar的類,定義為一個控制器,類的具體實現如下:
package com.zhangguo.springmvc02.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 定義控制器
*/
//BarController類的實例是一個控制器,會自動添加到Spring上下文中
@Controller
public class BarController {
//映射訪問路徑
@RequestMapping("/bar")
public String index(Model model){
//Spring MVC會自動實例化一個Model對象用於向視圖中傳值
model.addAttribute("message", "這是通過注解定義的一個控制器中的Action");
//返回視圖位置
return "foo/index";
}
}
還要需要修改Spring mvc配置文件,啟用自動組件掃描功能,在beans中增加如下配置:
<!-- 自動掃描包,實現支持注解的IOC -->
<context:component-scan base-package="com.zhangguo.springmvc02" />
base-package屬性用於指定掃描的基礎包,可以縮小掃描的范圍。運行結果如下:

小結:從代碼與運行結果可以看出BarController與FooController同時都指定了一個視圖foo/index.jsp,但是頁面結果的結果是不一樣的,從這里可以看出視圖是被復用的,而控制器與視圖之間是弱偶合關系。
二、@RequestMapping詳解
@RequestMapping注釋用於映射url到控制器類或一個特定的處理程序方法。可用於類或方法上。用於類上,表示類中的所有響應請求的方法都是以該地址作為父路徑。該注解共有8個屬性,注解源碼如下:
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.Callable;
import org.springframework.core.annotation.AliasFor;
/**
* 用於映射url到控制器類或一個特定的處理程序方法.
*/
//該注解只能用於方法或類型上
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
/**
* 指定映射的名稱
*/
String name() default "";
/**
* 指定請求的路徑映射,指定的地址可以是uri模板,別名為path
*/
@AliasFor("path")
String[] value() default {};
/** 別名為value,使用path更加形象
* 只有用在一個Servlet環境:路徑映射URI(例如“/myPath.do”)。
* Ant風格的路徑模式,同時也支持(例如,“/myPath/*.do”)。在方法層面,在主要的映射在類型級別表示相對路徑(例如,“edit.do”)
* 的支持。路徑映射的URI可能包含占位符(例如“/$ {}連接”)
*/
@AliasFor("value")
String[] path() default {};
/**
* 指定請求謂詞的類型如GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. 收窄請求范圍 The
* HTTP request methods to map to, narrowing the primary mapping: GET, POST,
* HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
*/
RequestMethod[] method() default {};
/**
* 映射請求的參數,收窄請求范圍 The parameters of the mapped request, narrowing the
* primary mapping.
*/
String[]params() default {};
/**
* 映射請求頭部,收窄請求范圍 The headers of the mapped request, narrowing the primary
* mapping. RequestMapping(value = "/something", headers =
* "content-type=text/*")
*/
String[] headers() default {};
/**
* 指定處理請求的提交內容類型(Content-Type),例如application/json, text/html,收窄請求范圍 The
* consumable media types of the mapped request, narrowing the primary
* mapping.
*/
String[] consumes() default {};
/**
* 指定返回的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回 The producible media types
* of the mapped request, narrowing the primary mapping. produces =
* "text/plain" produces = {"text/plain", "application/*"} produces =
* "application/json; charset=UTF-8"
*/
String[] produces() default {};
}
從上面的源碼可以發現除了name基本都是數組類型,在設置時我們可以指定單個值,如@RequestMapping(value="/foo");也可以同時指定多個值如:@RequestMapping(value={"/foo","/bar"})。
2.1、value 屬性指定映射路徑或URL模板
指定請求的實際地址,指定的地址可以是URL模板,正則表達式或路徑占位,該屬性與path互為別名關系,@RequestMapping("/foo")} 與 @RequestMapping(path="/foo")相同。該屬性是使用最頻繁,最重要的一個屬性,如果只指定該屬性時可以把value略去。Spring Framework 4.2引入了一流的支持聲明和查找注釋屬性的別名。@AliasFor注解可用於聲明一雙別名屬性,來給注解的屬性起別名, 讓使用注解時, 更加的容易理解(比如給value屬性起別名, 更容易讓人理解)。先看一個官網的示例:
@Controller
@RequestMapping("/appointments")
public class AppointmentsController {
private final AppointmentBook appointmentBook;
@Autowired
public AppointmentsController(AppointmentBook appointmentBook) {
this.appointmentBook = appointmentBook;
}
@RequestMapping(method = RequestMethod.GET)
public Map<String, Appointment> get() {
return appointmentBook.getAppointmentsForToday();
}
@RequestMapping(value = "/{day}", method = RequestMethod.GET)
public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso = ISO.DATE) Date day, Model model) {
return appointmentBook.getAppointmentsForDay(day);
}
@RequestMapping(value = "/new", method = RequestMethod.GET)
public AppointmentForm getNewForm() {
return new AppointmentForm();
}
@RequestMapping(method = RequestMethod.POST)
public String add(@Valid AppointmentForm appointment, BindingResult result) {
if (result.hasErrors()) {
return "appointments/new";
}
appointmentBook.addAppointment(appointment);
return "redirect:/appointments";
}
}
2.1.1、指定具體路徑字符
2.1.1.1 只注解方法
@Controller
public class FooBarController {
@RequestMapping("/action1")
public String action1(){
return "foo/index";
}
}
訪問路徑:http://localhost:8087/SpringMVC02/action1
2.1.1.2 同時注解類與方法
@Controller
@RequestMapping("/foobar")
public class FooBarController {
@RequestMapping("/action1")
public String action1(){
return "foo/index";
}
}
訪問路徑:http://localhost:8087/SpringMVC02/foobar/action1
需要先指定類的路徑再指定方法的路徑
2.1.1.3 當value為空值
注解在方法上時,如果value為空則表示該方法為類下默認的Action。
@Controller
@RequestMapping("/foobar")
public class FooBarController {
@RequestMapping("/action1")
public String action1(Model model){
//在模型中添加屬性message值為action1,渲染頁面時使用
model.addAttribute("message", "action1");
return "foo/index";
}
@RequestMapping
public String action2(Model model){
//在模型中添加屬性message值為action2,渲染頁面時使用
model.addAttribute("message", "action2");
return "foo/index";
}
}
訪問action2的路徑是:http://localhost:8087/SpringMVC02/foobar,如果加上action2就錯誤了。
注解在類上時,當value為空值則為默認的控制器,可以用於設置項目的起始頁。
@Controller
@RequestMapping
public class FooBarController {
@RequestMapping("/action1")
public String action1(Model model){
//在模型中添加屬性message值為action1,渲染頁面時使用
model.addAttribute("message", "action1");
return "foo/index";
}
@RequestMapping
public String action2(Model model){
//在模型中添加屬性message值為action2,渲染頁面時使用
model.addAttribute("message", "action2");
return "foo/index";
}
}
訪問路徑:http://localhost:8087/SpringMVC02/,同時省去了控制器名與Action名稱,可用於歡迎頁。
訪問action1的路徑是:http://localhost:8087/SpringMVC02/action1
2.1.2、路徑變量占位,URI模板模式
在Spring MVC可以使用@PathVariable 注釋方法參數的值綁定到一個URI模板變量。
@RequestMapping("/action3/{p1}/{p2}")
public String action3(@PathVariable int p1,@PathVariable int p2,Model model){
model.addAttribute("message", p1+p2);
return "foo/index";
}
運行結果:

使用路徑變量的好處:使路徑變得更加簡潔;獲得參數更加方便,框架會自動進行類型轉換。通過路徑變量的類型可以約束訪問參數,如果類型不一樣,則訪問不到action,如這里訪問是的路徑是/action3/1/a,則路徑與方法不匹配,而不會是參數轉換失敗。
2.1.3、正則表達式模式的URI模板
@RequestMapping(value="/action4/{id:\\d{6}}-{name:[a-z]{3}}")
public String action4(@PathVariable int id,@PathVariable String name,Model model){
model.addAttribute("message", "id:"+id+" name:"+name);
return "foo/index";
}
正則要求id必須為6位的數字,而name必須為3位小寫字母,訪問結果如下:

2.1.4、矩陣變量@MatrixVariable
矩陣變量可以出現在任何路徑段,每個矩陣變量用“;”分隔。例如:“/汽車;顏色=紅;年=2012”。多個值可以是“,”分隔“顏色=紅、綠、藍”或變量名稱可以重復“顏色=紅;顏色=綠色;顏色=藍”,如下所示:
// GET /pets/42;q=11;r=22
@RequestMapping(value = "/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
// 矩陣變量
@RequestMapping(value = "/action5/{name}")
public String action5(Model model,
@PathVariable String name, //路徑變量,用於獲得路徑中的變量name的值
@MatrixVariable String r,
@MatrixVariable(required = true) String g, //參數g是必須的
@MatrixVariable(defaultValue = "99", required = false) String b) { //參數b不是必須的,默認值是99
model.addAttribute("message", name + " is #" + r + g + b);
return "foo/index";
}
//Get http://localhost:8087/SpringMVC02/action5/the%20book%20color;r=33;g=66
//the book color is #336699
默認是不允許使用矩陣變量的,需要設置配置文中的RequestMappingHandlerMapping的屬性removeSemicolonContent為false;在annotation-driven中增加屬性enable-matrix-variables="true",修改后的springmvc-servlet.xml文件如下:
<!-- 支持mvc注解驅動 -->
<mvc:annotation-driven enable-matrix-variables="true" />
<!-- 配置映射媒體類型的策略 -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="removeSemicolonContent" value="false" />
</bean>
訪問結果如下:

2.1.5、Ant風格路徑模式
@RequestMapping注解也支持ant風格的路徑模式,如/myPath/*.do,/owners/*/pets/{petId},示例代碼如下:
//Ant風格路徑模式
@RequestMapping(value = "/action6/*.do")
public String action6(Model model){
model.addAttribute("message","Ant風格路徑模式");
return "foo/index";
}
運行結果:

當然還有關於路徑匹配的規則,特殊的優先級高過一般的,更多規則可以參考官方幫助。
2.2、method屬性指定謂詞類型
用於約束請求的謂詞類型,可以收窄請求范圍。指定請求謂詞的類型如GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE,如下代碼所示:
//謂詞類型
@RequestMapping(value = "/action6",method={RequestMethod.POST,RequestMethod.DELETE})
public String action6(Model model) {
model.addAttribute("message", "請求謂詞只能是POST與DELETE");
return "foo/index";
}
要訪問action7請求謂詞類型必須是POST或者為DELETE,當我們從瀏覽器的URL欄中直接請求時為一個GET請求,則結果是405,如下所示:

如果將POST修改為GET則正常了,如下所示:
//謂詞類型
@RequestMapping(value = "/action6",method=RequestMethod.GET)
public String action6(Model model) {
model.addAttribute("message", "請求謂詞只能是GET");
return "foo/index";
}

2.3、consumes屬性指定請求的Content-Type
指定處理請求的提交內容類型(Content-Type),例如application/json, text/html,收窄請求范圍,如果用戶發送的請求內容類型不匹配則方法不會響應請求,具體使用如下代碼所示:
package com.zhangguo.springmvc02.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/home")
public class HomeController {
// 請求內容類型必須為text/html,注意瀏覽器默認沒有指定Content-type
@RequestMapping(value = "/action8",consumes="text/html")
public String action8(Model model) {
model.addAttribute("message", "請求的提交內容類型(Content-Type)是text/html");
return "foo/index";
}
}
在action8的注解中約束發送到服務器的Content-Type必須是text/html類型,如果類型不一致則會報錯(415),測試結果如下:


從兩個圖的對比可以看出當內容類型為text/plain時報客戶端錯誤415,當內容類型為text/html時則響應正常,響應的結果如下:
請求的提交內容類型(Content-Type)是text/html
注意:可以使用!號,如consumes="!text/html"
2.4、produces屬性指定響應的Content-Type
指定返回的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回,方法才處理客戶端的請求否則會報406錯誤,常用設置如下:
produces = "text/plain" //客戶端只接收純文本
produces = {"text/plain", "application/*"} //客戶端接收純文本與application/*類型的內容
produces = "application/json; charset=UTF-8" //客戶端接收json且編碼為utf-8
//客戶端接收json且編碼為utf-8,多數瀏覽器Accept設置的為*/*,接收任意類型
@RequestMapping(value = "/action9",produces="application/json; charset=UTF-8")
public String action9(Model model) {
model.addAttribute("message", "客戶端可以接收的類型是application/json; charset=UTF-8");
return "foo/index";
}
運行結果:


注意:可以使用!號,如produces="!text/html"
2.5、params屬性指定請求中必須有特定參數與值
映射請求的參數,收窄請求范圍。可以限制客戶端發送到服務器的請求參數為某些特定值或不為某些值,如下代碼所示:
//請求的參數必須包含id=215與name不等於abc
@RequestMapping(value = "/action10",params={"id=215","name!=abc"})
public String action10(Model model) {
model.addAttribute("message", "請求的參數必須包含id=215與name不等於abc");
return "foo/index";
}
運行結果如下:


name的值如沒有指定也是通過的;可以使用不等於;
2.6、headers屬性指定請求中必須有特定header值
映射請求頭部,收窄請求范圍。約束客戶端發送的請求頭部信息中必須包含某個特定的值或不包含某個值,作用范圍明顯大於前面講過的幾種,示例代碼如下:
//請求頭部信息中必須包含Host=localhost:8088
@RequestMapping(value = "/action11",headers="Host=localhost:8088")
public String action11(Model model) {
model.addAttribute("message", "請求頭部信息中必須包含Host=localhost:8088");
return "foo/index";
}
運行結果:

修改Host為8087時運行就正常了:

這里同樣可以使用!號;可以使用通配符如:Content-Type="application/*"
2.7、name屬性指定名稱
為當前映射指定一個名稱,不常用,一般不會指定。
2.8、path屬性指定路徑
先看源碼中的path與value,定義如下:
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
從Spring 4.2開始引入了@AliasFor注解,可以實現屬性的別名,如value本身並沒有特定的含義,而path會更加具體,能見名知義,通俗說可以認為兩者在使用中是一樣的如:@RequestMapping("/foo")} 與 @RequestMapping(path="/foo")相同。示例代碼如下:
//映射訪問路徑為/action12或/myaction,指定映射名稱為actionTest
@RequestMapping(path ={"/action12","/myaction"},name="actionTest")
public String action12(Model model) {
model.addAttribute("message", "映射訪問路徑為/action12或/myaction,指定映射名稱為actionTest");
return "foo/index";
}
運行結果:



