spring-mvc第二期:讓Controller沒有祕密

上期回顧:鏈接

源碼clone地址:鏈接 (spring-mvc模塊)

SpringMVC的底層細節不可不知,但在日常開發的大部分時間裏,我們還是要專注於業務邏輯的開發,因此詳細瞭解接口的 "管家"——Controller自然很重要:(還是先擺出這張圖片,然後根據官方文檔來討論)

目錄

1.簡單回顧一下 HTTP

2.@Controller Or @RestController ?

3.@RquestMapping

4.Handler methods

4.1.參數

4.1.1.@PathVariable & @MatrixVariable

4.1.2. @RequestParam & @RequestHeader

4.1.3.@CookieValue

4.1.4.Model & @ModelAttribute

 4.1.5.Multipart

4.1.6.@RequestBody

4.2.返回值

4.2.1.@ResposeBody & ResponseEntity

4.2.2.@JsonIgnore & @JsonView

4.2.3.@JsonAlias & @JsonProperty

5.Exceptions

6.Controller Advice


1.簡單回顧一下 HTTP

Http報頭分爲通用報頭,請求報頭,響應報頭和實體報頭。
請求方的http報頭結構:通用報頭+請求報頭+實體報頭
響應方的http報頭結構:通用報頭+響應報頭+實體報頭

而這裏我們討論兩個常用報頭信息:

  • 請求報頭 Accept ( 例如 (Accept:application/json) 代表客戶端希望接受的數據類型是 json 類型)
  • 實體報頭 Content-Type (Content-Type代表發送端(客戶端或者服務器)發送的實體數據的數據類型)

2.@Controller Or @RestController ?

在上一期的基礎上,我們可以直接開始新建一個Controller類了

@Controller
public class BoringController {
}

@Controller 或 @RestController 註解可謂是Controller的靈魂,至於他們兩個有啥區別捏,先給出官方解釋:讀完此文你會更加明白 

@RestController is a composed annotation that is itself meta-annotated with @Controller and @ResponseBody to indicate a controller whose every method inherits the type-level @ResponseBody annotation and, therefore, writes directly to the response body versus view resolution and rendering with an HTML template. 

當讓配置了這些還不夠,你還得讓ServletWebApplicationContext 發現這個Bean(@Controller 中  包含@Component 註解),還記得我們在上一期中說到SevletWebApplicationContext 是由MvcConfig得到的,因此這個組件掃描也應當陪在這裏:

3.@RquestMapping

requestMapping 即瀏覽器請求的接口路徑,和下面的 Handler methods可以說是對好戀人,而HandlerMapping則是他們的介紹人(上一期有提到)

    @RequestMapping(name = "boring1", value = {"/1/{id}", "/101/{id}"})
    public String boring1(@PathVariable Long id) {
        return "home";
    }

@RequetsMapping 註解有如下幾個修飾:

  • name 此接口的名字,和 <servlet-name>同義
  • value : 這個就很重要啦,是請求的路徑,可以同時配多個,而且支持模糊匹配,如下表

可以使用@PathVariable 接受請求傳入的參數,甚至這樣:(這裏要注意請求的參數和方法傳入的參數類型要匹配,否則會拋出TypeMismatchException 異常)

或者這樣:

  • method: 請求方式
@RequestMapping(value = {"/2"}, method = RequestMethod.POST)

以上等價於@PostMapping 註解, 其他的 GET PUT DELETE同理

  •  consumes 與 produce :
@RequestMapping(name = "boring1", value = {"/1/{id}", "/101/{id}"}, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)

如果寫成如上形式,表面該請求需滿足:

  •  params參數
@GetMapping(value = {"/3"}, params = {"name","age"})

如上寫法,則表示請求要爲如下才可以:

 

4.Handler methods

當使用@RequestMapping 爲handler規範了url後,我們就來專心討論請求的處理方法 Handler

4.1.參數

4.1.1.@PathVariable & @MatrixVariable

爲了讓url的參數傳入更靈活,spring支持以上兩種方式從url中獲取數據,@PathVariable 上文已講,這裏着重說一下 @MatrixVariable

    /**
     * GET http://localhost:8080/boring/4/swing;a=blue;b=yellow/api/12;b=red;c=black
     */
    @GetMapping(value = {"/4/{name}/api/{age}"})
    public String boring4(@PathVariable String name,
                          @MatrixVariable(pathVar = "name", name = "a", required = false) String a,
                          @PathVariable String age,
                          @MatrixVariable(pathVar = "age", name = "b", required = false) String b,
                          @MatrixVariable(pathVar = "name") MultiValueMap<String, String> multiValueMap,
                          @MatrixVariable MultiValueMap<String, String> map) {
        //swing
        System.out.println(name);
        //blue
        System.out.println(a);
        //12
        System.out.println(age);
        //red
        System.out.println(b);
        //{a=[blue], b=[yellow]}
        System.out.println(multiValueMap);
        //{a=[blue], b=[yellow, red], c=[black]}
        System.out.println(map);
        return "home";
    }

以上基本是官網列出的所有用法,分析: ( @MatrixVariable(pathVar = "name", name = "a", required = false) String a ) 表示在 url 中name部分尋找一個a的值,並將其付給 參數a

注意要想實現這個註解,必須在mvcConfig中增加如下配置(將urlPathHelper.setRemoveSemicolonContent設置爲false)

    @Bean
    public UrlPathHelper urlPathHelper() {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setRemoveSemicolonContent(false);
        return urlPathHelper;
    }

4.1.2. @RequestParam & @RequestHeader

    /**
     * GET http://localhost:8080/boring/5?name=swing
     * Accept: multipart/form-data
     */
    @GetMapping(value = {"/5"})
    public String boring5(@RequestHeader(value = "Accept",required = false) String accept, @RequestParam(value = "name",required = false) String name) {
        //multipart/form-data
        System.out.println(accept);
        //swing
        System.out.println(name);
        return "home";
    }

4.1.3.@CookieValue

    /**
     * GET http://localhost:8080/boring/6
     * Cookie: sentence=swing
     */
    @GetMapping(value = {"/6"})
    public String boring6(@CookieValue("sentence") String sentence) {
        //swing
        System.out.println(sentence.toString());
        return "home";
    }

4.1.4.Model & @ModelAttribute

上一期有講到,Model用來將handler處理後的數據傳到視圖層進行渲染,數據是以鍵值對的形式存儲起來的

@ModelAttribute註解用於將方法的參數或方法的返回值綁定到指定的模型屬性上,並返回給Web視圖

當作用在方法上時:

    /**
     * GET http://localhost:8080/boring/8
     */
    @GetMapping(value = {"/8"})
    public String boring8(Model model) {
        return "home";
    }

    @ModelAttribute(name = "message")
    public String initName() {
        return "swing world";
    }

表示在請求到達handler之前,先將 <"message":"swing world">放入 Model中

當作用在參數上時:

    /**
     * GET http://localhost:8080/boring/9
     */
    @GetMapping(value = {"/9"})
    public String boring9(@ModelAttribute UserDO userDO, Model model) {
        userDO.setUsername("swing");
        userDO.setPassword("312312");
        userDO.setAge(11);
        userDO.setId(10L);
        return "home";
    }

 

此時如果Model 裏不存在 userDO屬性,則創建一個,並放入Model中(注意這時候這個UserDO類一定要有無參數的構造函數)

 4.1.5.Multipart

文件上傳是一個很常用的功能,而從前端傳入的二進制文件數據,便是通過MultipartFile 參數傳入Handler

首先我們咋們先增加個依賴:

    <!--文件的上傳與下載-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.3</version>
            <!--排除其中與本項目重複的包-->
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>javax.servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

然後在mvcConfig中配置一下 MultipartResolver 用來解析文件

    /**
     * 配置文件上傳解析器
     */
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        multipartResolver.setMaxUploadSize(10485760);
        multipartResolver.setDefaultEncoding("UTF-8");
        return multipartResolver;
    }

 簡單寫個上傳頁面:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Welcome!</title>
</head>
<body>
<h1>Welcome ${message}</h1>
<form action="/boring/upLoad" method="post" enctype="multipart/form-data">
    File:
    <input type="file" name="file"/>
    <input type="submit" value="UpLoad"/>
    <input type="reset" value="Reset"/>
</form>
</body>
</html>

最後開始我們的handler

    /**
     * 文件的上傳
     */
    @RequestMapping("/upLoad")
    public String upLoadFile(@RequestParam("file") MultipartFile uploadFile, HttpServletResponse response) {
        System.out.println(uploadFile.getName());
        System.out.println(uploadFile.getSize());
        return "home";
    }

4.1.6.@RequestBody

這個參數可以獲取客戶端傳來的請求體,由於Get的請求參數是放入url中,因此這個註解自然是實用於POST請求

當請求體是json格式,那麼我我們有如下兩種獲取方式,使用字符串或數據傳輸對象:

/**
     * POST http://localhost:8080/swing/1
     * Content-Type: application/json
     * {
     * "id": 12,
     * "username": "swing"
     * }
     */
    @PostMapping("/1")
    public String swing1(@RequestBody String jsonString) {
//        {
//            "id": 12,
//            "username": "swing"
//        }
        log.info(jsonString);
        return "home";
    }

    /**
     * POST http://localhost:8080/swing/2
     * Content-Type: application/json
     * {
     * "id": 12,
     * "username": "swing"
     * }
     */
    @PostMapping("/2")
    public String swing2(@RequestBody UserDTO user) {
        //UserDTO(id=12, username=swing)
        log.info(user.toString());
        return "home";
    }

當然和上面提到的 @RequestParam 一起使用也是沒問題的

/**
     * POST http://localhost:8080/swing/3?age=45
     * Content-Type: application/json
     * {
     * "id": 12,
     * "username": "swing"
     * }
     */
    @PostMapping("/3")
    public String swing3(@RequestBody UserDTO user, @RequestParam Integer age) {
        //UserDTO(id=12, username=swing)
        log.info(user.toString());
        //45
        log.info(age.toString());
        return "home";
    }

4.2.返回值

4.2.1.@ResposeBody & ResponseEntity

處理完了請求,我們自然要來考慮:如何將我們的結果返回呢?常用的兩種模式如下

  • 返回爲ModelAndView 然後交給視圖解析器渲染出對應的頁面,然後以html的形式傳給瀏覽器顯示
  • 第二種是基於前後端分離的模式,後端的程序員不必再去糾結如何渲染頁面,只需要處理請求,然後將處理結果以約定的數據格式傳送到前端(通常是JSON或XML格式),然後交由前端自行渲染

之前我們使用的都是基於第一種方式的數據返回,服務端視圖渲染,現在我們着重來說一下第二種模式,這裏就不得不提到@ResposeBody註解啦:

它作用在handler上時候表示將handler的返回值以字符串的形式返回給前端,如下演示:

    /**
     * 請求:
     * GET http://localhost:8080/swing/4
     * 結果:
     * {
     * "id": 20,
     * "username": "swing"
     * }
     */
    @GetMapping("/4")
    @ResponseBody
    public UserDTO swing4() {
        UserDTO userDTO = new UserDTO();
        userDTO.setId(20L);
        userDTO.setUsername("swing");
        return userDTO;
    }

 如果@ResponseBody作用在Controller上,則對該類中的所有handler起作用

而@RestController和@Controller的區別也是應爲前者多了一個@ResponseBody註解,因此在前後端分離模式的開發時,我們常採用@RestController

另外官方還提供一個和@ResponseBody作用相似的類 ResponseEntity 但是它多兩個屬性,status,headers

/**
     * 請求:
     * GET http://localhost:8080/swing/5
     * 結果:
     * {
     * "id": 20,
     * "username": "swing"
     * }
     */
    @GetMapping("/5")
    public ResponseEntity<UserDTO> swing5() {
        UserDTO userDTO = new UserDTO();
        userDTO.setId(20L);
        userDTO.setUsername("swing");
        return ResponseEntity.ok(userDTO);
    }

OK!既然已經搞清楚了返回數據的格式,那麼我們便來討論一下返回數據的內容,試想一下,如果一個接口返回的內容是根據業務隨便改變,一會兒三個字段,一會兒十個字段,那怕是會被前端的小夥伴噴成篩子,所以,如果是使用前後端分離模式,接口的響應數據一定要做到規範統一。

4.2.2.@JsonIgnore & @JsonView

既然接口可以返回JOSN類型的數據,那麼我們就不得不考慮一個數據隱私的問題,例如像 password這樣的字段,可不能隨隨便便的返回給前端,於是我們便可以使用@JsonIgnore註解來讓對象在序列化爲Json時候忽略password字段,如下:

public class UserDO implements Serializable {
    private Long id;

    private String username;
    @JsonIgnore
    private String password;

    private Integer age;

    private static final long serialVersionUID = 1L;
}


/**
     * 請求:
     * GET http://localhost:8080/swing/6
     * 結果:
     * {
     * "id": 12,
     * "username": "swing",
     * "age": 18
     * }
     */
    @GetMapping("/6")
    @ResponseBody
    public UserDO swing6() {
        UserDO user = new UserDO();
        user.setId(12L);
        user.setAge(18);
        user.setUsername("swing");
        user.setPassword("42423423");
        return user;
    }

不過新的問題又來了,一個字段可能並不是一直都不需要,如果在某個業務場景下,我們需要json將密碼返回,那可咋辦,於是@JsonView 便來了:

@Data
public class UserDO implements Serializable {
    /**
     * 沒有密碼的視圖
     */
    public interface WithoutPasswordView {
    }

    /**
     * 有密碼的視圖
     */
    public interface WithPasswordView extends WithoutPasswordView {
    }

    @JsonView(WithoutPasswordView.class)
    private Long id;

    @JsonView(WithoutPasswordView.class)
    private String username;

    @JsonView(WithPasswordView.class)
    private String password;

    private Integer age;

    private static final long serialVersionUID = 1L;
}

 注意:沒有被@JsonView註解的字段不會被序列化

/**
     * 請求:
     * GET http://localhost:8080/swing/7
     * 結果:
     * {
     * "id": 12,
     * "username": "swing"
     * }
     */
    @GetMapping("/7")
    @ResponseBody
    @JsonView(UserDO.WithoutPasswordView.class)
    public UserDO swing7() {
        UserDO user = new UserDO();
        user.setId(12L);
        user.setAge(18);
        user.setUsername("swing");
        user.setPassword("42423423");
        return user;
    }

    /**
     * 請求:
     * GET http://localhost:8080/swing/8
     * 結果:
     * {
     * "id": 12,
     * "username": "swing",
     * "password": "42423423"
     * }
     */
    @GetMapping("/8")
    @ResponseBody
    @JsonView(UserDO.WithPasswordView.class)
    public UserDO swing8() {
        UserDO user = new UserDO();
        user.setId(12L);
        user.setAge(18);
        user.setUsername("swing");
        user.setPassword("42423423");
        return user;
    }

4.2.3.@JsonAlias & @JsonProperty

這兩個註解讓我們可以適當地對 json 的序列化和反序列化進行一下設置:

  • @JsonAlias:JSON反序列化時起作用, 給屬性一個別名(即json的key名)(注意:使用這個註解後原來的字段名便不可以再作爲json的key名)
  • @JsonProperty:JSON序列化時起作用,設置字段的key名
@Data
public class UserDTO {
    private Long id;

    @JsonAlias(value = {"myName", "testName"})
    @JsonProperty("myName")
    private String username;
}


    /**
     * 請求:
     * POST http://localhost:8080/swing/9
     * Content-Type: application/json
     * {
     * "id": 12,
     * "testName": "swing"
     * }
     * 結果:
     * {
     * "id": 12,
     * "myName": "swing"
     * }
     */
    @PostMapping("/9")
    @ResponseBody
    public UserDTO swing9(@RequestBody UserDTO user) {
        return user;
    }

 

5.Exceptions

在阿里巴巴代碼規範中的工程結構模塊,對異常的處理有如下建議:

(分層異常處理規約)在DAO層,產生的異常類型有很多,無法用細粒度的異常進行catch,使用catch(Exceptione)方式,並thrownewDAOException(e),不需要打印日誌,因爲日誌在Manager/Service層一定需要捕獲並打印到日誌文件中去,如果同臺服務再打日誌,浪費性能和存儲。在Service層出現異常時,必須記錄出錯日誌到磁盤,儘可能帶上參數信息,相當於保護案發現場。如果Manager層與Service同機部署,日誌方式與DAO層處理一致,如果是單獨部署,則採用與Service一致的處理方式。Web層絕不應該繼續往上拋異常,因爲已經處於頂層,如果意識到這個異常將導致頁面無法正常渲染,那麼就應該直接跳 Java開發手冊38/44轉到友好錯誤頁面,加上用戶容易理解的錯誤提示信息。開放接口層要將異常處理成錯誤碼和錯誤信息方式返回

既然web層的一場不能再往上拋了,Spring爲我們提供了@ExceptonHandler方法,用來捕捉拋到最上層(這裏指web層)的異常,我們來拿程序員的最常見的“小夥伴”NPE 來舉個栗子

/**
 * @author swing
 */
@Slf4j
@Controller
@RequestMapping(value = {"/ex"})
public class ExceptionController {

    @ExceptionHandler({NullPointerException.class})
    public ResponseEntity<String> handle(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Sorry!NullPointerException!");
    }

    /**
     * 請求:
     * POST http://localhost:8080/ex/1
     * Content-Type: application/json
     *
     * {
     *   "id": 12,
     *   "testName": "錢騫",
     *   "birthday": ""
     * }
     * 結果:
     * Sorry!NullPointerException?
     * Response code: 500; Time: 50ms; Content length: 27 bytes
     */
    @PostMapping("/1")
    @ResponseBody
    public UserDTO swing9(@RequestBody UserDTO user) {
        System.out.println(user.getBirthday().getTime());
        return user;
    }
}

 

6.Controller Advice

上文中我們介紹了@ExceptionHandler @ModelAttribute 等註解,但是他們有一個不足:只會在定義他們的Controller中作用,很明顯,當項目中有超級多的Controller時,我們需要尋找一個新的定義辦法,首先想到的當然是Spring AOP的思想,做一個切面,SpringMVC爲我們提供了 @ControllerAdvice 和 @RestControllerAdvice 用來實現這個功能:

如下

/**
 * @author swing
 */
@ControllerAdvice(assignableTypes = {SwingController.class})
public class AdviceController {
    @ExceptionHandler({NullPointerException.class})
    public ResponseEntity<String> handle(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Sorry!NullPointerException!");
    }
}

如果需要精確的作用與某一些Controller,ControllerAdvice提供如下幾種定位

//所用以RestController註解的Controller
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// 該包下的所有Controller
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// 詳細的Controller類
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章