上期回顾:链接
源码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 {}