Spring Web MVC框架(二) 控制器

在前面我們搭建了基本的Spring Web MVC環(huán)境,并配置了一個控制器。下面我們來詳細學習一下控制器。控制器的主要作用就是處理特定URL發(fā)過來的HTTP請求,然后進行業(yè)務邏輯處理,將結(jié)果返回給某個特定的視圖。

處理請求

我們在前面定義了如下一個控制器。在Spring中定義控制器非常簡單,新建一個類然后應用@Controller注解即可,當然一般習慣上將控制器類也命名為XXController。每個控制器可以有若干方法,分別處理不同的請求。要指定處理請求的URL,使用@RequestMapping注解。控制器方法處理之后,返回一個字符串,指定要使用的視圖名稱,然后該名稱交給視圖解析器轉(zhuǎn)換成真正的視圖,然后返回給客戶端。@RequestMapping還可以注解到控制器類上,這樣一來每個方法處理的URL就是控制器和方法上URL的組合。

@Controller
public class MainController {

    @RequestMapping("/hello")
    public String hello(@RequestParam(defaultValue = "茍") String name, Model model) {
        model.addAttribute("name", name);
        return "hello";
    }

    @RequestMapping("/index")
    public String index() {
        return "index";
    }

}

默認情況下@RequestMapping會處理所有請求,如果希望只處理GET或者POST等請求,可以使用@RequestMapping的method屬性。

@RequestMapping(value = "/index", method = {RequestMethod.GET})
public String index() {
    return "index";
}

當然也可以直接使用Spring定義的幾個Mapping注解,包括了GET、POST、DELETE、PUT等。需要注意這幾個注解只能應用于方法上。

@GetMapping(value = "/index")
public String index() {
    return "index";
}

路徑參數(shù)

細心的同學可能會發(fā)現(xiàn)有些網(wǎng)站的URL很特別,類似http://mysite.com/list/yitian這樣的。Spring也支持這樣的路徑參數(shù)。這時候路徑模式中相應部分需要用花括號括起來,然后在方法中使用@PathVariable注解(注解中的名稱需要和花括號中的參數(shù)相同)。這樣對應的路徑參數(shù)就會由Spring自動賦給方法中的參數(shù),我們直接在方法中使用即可。

@RequestMapping("/hello/{name}")
public String hey(@PathVariable("name") String username, Model model) {
    model.addAttribute("name", username);
    return "hello";
}

如果方法參數(shù)和路徑中花括號部分相同,那么@PathVariable中的名稱可以省略。

@RequestMapping("/hello/{name}")
public String hey(@PathVariable String name, Model model) {
    model.addAttribute("name", name);
    return "hello";
}

另外,方法中可以有多個路徑參數(shù)。而且路徑參數(shù)并不一定只能是字符串,也可以是int、longDate這樣的簡單類型,Spring會自動進行轉(zhuǎn)換,如果轉(zhuǎn)換失敗,就會拋出TypeMismatchException。

正則表達式匹配

有時候可能需要匹配一個比較復雜的路徑,這時候可以使用正則表達式,語法是{varName:regex}。例如為了匹配"/spring-web/spring-web-3.0.5.jar",我們需要這樣一個方法。

@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String extension) {
    // ...
}

另外路徑模式中還支持通配符,例如/myPath/*.do。

最后一個問題就是這些路徑優(yōu)先級的問題。如果一個請求匹配了多個路徑模式,那么最具體的那個會被使用。規(guī)則如下:

  • 路徑中路徑參數(shù)和通配符越少,路徑越具體。
  • 路徑參數(shù)和通配符個數(shù)相同的話,路徑越長越具體。
  • 個數(shù)和長度都相同的話,通配符個數(shù)越少路徑越具體。
  • 默認匹配/**優(yōu)先級最低。
  • 前綴模式例如/public/**比其他兩個通配符的模式優(yōu)先級更低。

矩陣變量Matrix Variables

RFC 3986定義了可以在路徑中添加鍵值對,這樣的鍵值對叫做矩陣變量。Spring默認沒有啟用矩陣變量。要啟用它,在dispatcher-servlet.xml中添加或修改如下一行。

<mvc:annotation-driven enable-matrix-variables="true"/>

矩陣變量可以用在路徑的任何部分,需要和路徑之間使用分號;分隔開,每個矩陣變量之間也是用分號分隔。如果一個矩陣變量有多個值,使用逗號,分隔,例如"/matrix/42;colors=red,blue,yellow;year=2012"。

對應的控制器方法如下。

// 處理請求 /matrix/42;colors=red,blue,yellow;year=2012
@RequestMapping("/matrix/{count}")
public String matrix(@PathVariable int count, @MatrixVariable String[] colors, @MatrixVariable int year, Model model) {
    model.addAttribute("colors", colors);
    model.addAttribute("year", year);
    return "matrix";
}

還可以將所有矩陣變量映射成一個Map。

// 處理請求 /matrix2/42;colors=red,blue,yellow;year=2012
@RequestMapping("/matrix2/{count}")
public String matrix2(@PathVariable int count, @MatrixVariable MultiValueMap<String, String> map, Model model) {
    model.addAttribute("map", map);
    return "matrix";
}

對應的視圖文件是matrix.jsp。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>矩陣變量</title>
    <meta charset="utf-8"/>
</head>
<body>
<p>顏色是:
    <c:forEach var="color" items="${colors}">
        ${color},
    </c:forEach>
</p>

<p>年份是:${year}</p>

<p>矩陣變量:${map}</p>
</body>
</html>

其他詳細用法參見Spring參考文檔-matrix-variables

媒體類型

通過使用@RequestMapping的consumes屬性,還可以指定某個處理方法只處理某個或某些媒體類型的請求。下面的請求方法只處理Content-Typeapplication/json的請求。

@RequestMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet, Model model) {
    // implementation omitted
}

另外comsumes部分還可以寫為非的形式,表示匹配不是某種類型的請求。例如comsumes="!text/html"表示處理Content-Type不是text/html的請求。除了直接指定字符串,還可以指定org.springframework.http.MediaType提供的一組常量。

另外@RequestMapping還有一個produces屬性,指定匹配Accept是某種類型的請求,并且使用指定的類型來編碼返回的響應。下面是一個例子。

@GetMapping(path = "/pets/{petId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {
    // implementation omitted
}

定義處理方法

前面我們通過@RequestMapping和另外幾個注解,來匹配特定的請求。下面來學習一下如何定義處理方法。

方法參數(shù)

處理方法的參數(shù)并不是任意的,Spring處理方法支持的參數(shù)列表很長,可以參考Spring文檔。這些參數(shù)可以分為幾類:一是Servlet相關(guān)的類,例如HttpServletRequest、HttpServletResponseHttpSession等;二是應用程序相關(guān)的,例如Timezone、Locale等;三是Spring提供的各類注解;四是輸入輸出流,用于直接操作HTTP請求和響應

返回類型

處理方法的返回類型也不是任意的。詳細的返回類型參見Spring官方文檔。常用的一些返回類型如下:

  • String,表示要返回視圖的名稱。
  • View,會由RequestToViewNameTranslator翻譯實際視圖的名稱。
  • void,表示方法會自己生成響應,不需要視圖支持。
  • Callable<?>,表示異步請求的返回。

綁定請求參數(shù)

我們還記得直接使用Servlet API中g(shù)etParameter方法的恐懼吧,對于每個Servlet我們都要調(diào)用多次getParameter方法獲取參數(shù),而且獲取到的是字符串,我們需要手動轉(zhuǎn)換類型。

在Spring中就非常簡單了,我們可以將請求參數(shù)綁定到方法參數(shù)上,使用@RequestParam即可。該注解有三個屬性,name表示請求參數(shù)的名稱;defaultValue表示請求參數(shù)的默認值;required表示請求參數(shù)是否是必需的。如果請求參數(shù)的名稱和方法參數(shù)相同,那么name還可以省略。


// GET /hello?name=yitian
@RequestMapping("/hello")
public String hello(@RequestParam(defaultValue = "茍") String name, Model model) {
    model.addAttribute("name", name);
    return "hello";
}

向視圖傳遞數(shù)據(jù)

如果處理方法的擁有一個org.springframework.ui.Model類型參數(shù),那么我們就可以調(diào)用該參數(shù)的addAttribute方法添加屬性,然后在視圖中就可以訪問這些屬性了。例子見上。

綁定請求體和響應體

綁定請求體使用@RequestBody注解。下面的例子將請求體直接返回給響應。這里的處理方法用到了Writer參數(shù)直接輸出HTTP響應,不需要視圖,因此這里返回空。為了運行這個例子,需要一個表單,發(fā)送到該控制器上,然后我們就可以看到表單對應的請求體了。

@PostMapping("/requestBody")
public void handle(@RequestBody String body, Writer writer) throws IOException {
    writer.write(body);
}

綁定響應體類似,我們需要使用@ResponseBody注解到方法上,這會告訴Spring直接將該方法的返回結(jié)果作為響應返回給客戶端。

@GetMapping("/something")
@ResponseBody
public String helloWorld() {
    return "Hello World";
}

在底層,Spring會使用HttpMessageConverter來將請求信息轉(zhuǎn)換成我們需要的類型。Spring Web MVC為我們自動注冊了一些HttpMessageConverter,詳細情況參見Spring 參考文檔 Section 22.16.1, “Enabling the MVC Java Config or the MVC XML Namespace”。

Rest控制器

@RestController會向所有@RequestMapping方法添加@ResponseBody注解。如果控制器需要實現(xiàn)REST API,那么這時候就很方便。

使用HttpEntity

HttpEntity和請求體、響應體這兩個類似,可以在一個地方同時處理請求和響應。下面是Spring官方的一個例子,獲取了請求HttpEntity,處理之后返回一個響應HttpEntity。Spring會使用HttpMessageConverter做必要的轉(zhuǎn)換。

@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
    String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader"));
    byte[] requestBody = requestEntity.getBody();

    // do something with request header and body

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("MyResponseHeader", "MyValue");
    return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}

使用ModelAttribute

@ModelAttribute注解用于向模型添加屬性??梢宰饔玫椒椒ǎ@時候該方法會在該控制器的所有處理方法前執(zhí)行。在方法中可以接受多個參數(shù)和一個模型參數(shù),然后將這些參數(shù)處理之后添加到模型中。這樣每次處理方法執(zhí)行前都會先執(zhí)行一次該方法。因此如果控制器中有多個處理方法要小心使用這個注解。

@ModelAttribute
public void addModel(@RequestParam String name, Model model) {
    model.addAttribute("name",name);
    // add more ...
}

@ModelAttribute還可以作用到方法參數(shù)上。這種情況更常見也更加有用。這時候Spring會先從model中尋找@ModelAttribute參數(shù),如果沒找到則實例化一個(因此這個類必須有無參構(gòu)造函數(shù)),然后添加到model中。然后將請求參數(shù)(下面例子中是name=易天&age=24&gender=男)添加到模型中。這樣當我們查看視圖的時候,一個完整的實體類已經(jīng)準備就緒了。

//  請求 /modelAttribute?name=易天&age=24&gender=男
@RequestMapping("/modelAttribute")
public String modelAttribute(@ModelAttribute User user, Model model) {
    model.addAttribute("user", user);
    return "modelAttribute";
}

User類的內(nèi)容如下, 各種方法已省略。

public class User {
    private String name;
    private int age;
    private String gender;
}

使用SessionAttribute

@SessionAttribute可以用于控制器上,這時候它會將上面介紹的ModelAttribute保存到Session中,方便多個方法間使用。

@Controller
@SessionAttributes("user")
public class MainController {
    //  請求 /modelAttribute?name=易天&age=24&gender=男
    @RequestMapping("/modelAttribute")
    public String modelAttribute(@ModelAttribute User user, Model model) {
        model.addAttribute("user", user);
        return "modelAttribute";
    }
}

@SessionAttributes還可以用到處理方法的參數(shù)上,這時候可以獲取到Session中相應名稱的屬性,需要注意這個屬性必須是已存在的。如果改屬性不存在Spring就會拋出異常,然后將我們導向400頁面。

@RequestMapping("/accessSession")
public String accessSession(@SessionAttribute User user, Model model) {
    model.addAttribute("user", user);
    return "accessSession";
}

如果需要還可以直接使用HttpSession,方法很簡單,將方法參數(shù)定義為HttpSession,然后Spring就會將session對象注入到方法中。我們可以直接進行操作。

@RequestMapping("/httpSession")
public String httpSession(HttpSession session, Model model) {
    User user = new User("林妹妹", 24, "女");
    session.setAttribute("user", user);
    model.addAttribute("session", session);
    return "accessSession";
}

使用@RequestAttribute

@RequestAttribute用于獲取RequestAttribute,這些請求屬性可能是由過濾器或攔截器產(chǎn)生的。

@RequestMapping("/")
public String handleInfo(@RequestAttribute String info) {
    // ...
}

處理application/x-www-form-urlencoded數(shù)據(jù)

瀏覽器會使用GET或者POST方法發(fā)送數(shù)據(jù),非瀏覽器客戶端可以使用PUT方法發(fā)送數(shù)據(jù)。但是PUT發(fā)送過來的數(shù)據(jù),不能被Servlet系列方法ServletRequest.getParameter*()獲取到。Spring提供了一個過濾器HttpPutFormContentFilter,用于支持非瀏覽器的PUT信息發(fā)送。

HttpPutFormContentFilter需要在web.xml中配置。<servlet-name>配置的是Spring的DispatcherServlet的名稱。

<filter>
    <filter-name>httpPutFormFilter</filter-name>
    <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>httpPutFormFilter</filter-name>
    <servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>

這個過濾器會攔截Content-Type是application/x-www-form-urlencoded的PUT請求,讀取其請求體然后包裝到ServletRequest中,以便ServletRequest.getParameter*()可以獲取到參數(shù)。

使用@CookieValue

@CookieValue可以獲取某個Cookie的值。如果該cookie不存在,就會拋出異常,可以使用required和defaultValue指定是否必須和默認值。

@RequestMapping("/cookie")
public void cookie(@CookieValue("JSESSIONID") String cookie) {
    //...
}

@RequestHeader

@RequestHeader注解可以獲取RequestHeader的信息,可以使用required和defaultValue指定是否必須和默認值。

@RequestMapping("/header")
public String headerInfo(@RequestHeader("Accept-Encoding") String encoding, Model model) {
    model.addAttribute("encoding", encoding);
    return "info";
}

控制器通知

先來介紹一下@InitBinder注解,它可以放到控制器的一個方法上,這個方法有一個WebDataBinder參數(shù),用它可以對控制器進行定制,添加格式轉(zhuǎn)換、驗證等功能。

@InitBinder
protected void initBinder(WebDataBinder binder) {
//添加功能
}

然后就可以介紹@ControllerAdvice@RestControllerAdvice這兩個注解了。它們可以定義控制器通知,這個AOP中的Advice概念是一樣的。這些注解需要應用到類上,這些類可以包括@ExceptionHandler, @InitBinder@ModelAttribute注解的方法,然后這些方法就會在恰當?shù)臅r機來執(zhí)行。

// 注解了RestController的控制器
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}

// 特定包下的控制器
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}

// 特定類型的控制器
@ControllerAdvice(assignableTypes = {MainController.class})
public class AssignableTypesAdvice {}

攔截請求

我們可以使用攔截器攔截請求并進行處理,這一點有點像Servlet的過濾器。我們需要實現(xiàn)org.springframework.web.servlet.HandlerInterceptor接口,不過更好的方法是繼承HandlerInterceptorAdapter類,這個類將幾個攔截方法進行了默認實現(xiàn)。我們只需要重寫需要的方法即可。

下面定義了一個簡單的攔截器,作用僅僅是輸出攔截時間。我們可以看到有四個攔截時機,處理請求前,處理請求后,完成請求后和異步處理開始后,這些攔截方法的參數(shù)是Http請求和響應,使用很方便。

public class LogInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle:" + LocalTime.now());
        return super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle:" + LocalTime.now());
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion:" + LocalTime.now());
        super.afterCompletion(request, response, handler, ex);
    }

    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("afterConcurrentHandlingStarted:" + LocalTime.now());
        super.afterConcurrentHandlingStarted(request, response, handler);
    }
}

有了攔截器之后,我們還需要注冊它。首先將其注冊為一個Spring Bean。然后定義一個RequestMappingHandlerMapping并將攔截器傳遞給它。

<beans>
    <bean id="handlerMapping"
            class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="logInterceptor"/>
            </list>
        </property>
    </bean>

    <bean id="logInterceptor"
            class="samples.LogInInterceptor"/>

</beans>

定義攔截器的這部分配置可以使用mvc命名空間簡化。

<mvc:interceptors>
    <ref bean="logInterceptor"/>

</mvc:interceptors>

默認情況下攔截器針對所有處理方法。如果希望只匹配某些URL,可以定義一個org.springframework.web.servlet.handler.MappedInterceptor,使用它的構(gòu)造方法設置映射。

<bean class="org.springframework.web.servlet.handler.MappedInterceptor">
    <constructor-arg index="0">
        <list>
            <value>/</value>
        </list>
    </constructor-arg>
    <constructor-arg index="1" ref="logInterceptor"/>
</bean>

也可以直接使用mvc命名空間。

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/"/>
        <ref bean="logInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

攔截器可能不適用@ResponseBodyResponseEntity方法,因為這些方法會使用HttpMessageConverter來輸出響應。這時候我們可以實現(xiàn)ResponseBodyAdvice接口,然后使用@ControllerAdvice注解或者直接在RequestMappingHandlerAdapter.配置它們來攔截這些方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,973評論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,970評論 6 342
  • 1、Spring MVC請求流程 (1)初始化:(對DispatcherServlet和ContextLoderL...
    拾壹北閱讀 1,974評論 0 12
  • spring官方文檔:http://docs.spring.io/spring/docs/current/spri...
    牛馬風情閱讀 1,726評論 0 3
  • 下班時,天下了雨,丟掉晴天的明朗,雨淅淅瀝瀝的打濕了整個天地,滿眼望去暮色盡染,秋天的涼意隨風彌漫了整個視野,召喚...
    叮當不是夢閱讀 401評論 0 0