上期回顧:鏈接
源碼clone地址:鏈接 (spring-mvc模塊)
SpringMVC的底層細節不可不知,但在日常開發的大部分時間裏,我們還是要專注於業務邏輯的開發,因此詳細瞭解接口的 "管家"——Controller自然很重要:(還是先擺出這張圖片,然後根據官方文檔來討論)
目錄
2.@Controller Or @RestController ?
4.1.1.@PathVariable & @MatrixVariable
4.1.2. @RequestParam & @RequestHeader
4.2.1.@ResposeBody & ResponseEntity
4.2.3.@JsonAlias & @JsonProperty
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 {}