第4章 Spring Boot Web开发
4.1 Web开发简介
4.1.1 Web入门
Spring毫无疑问已经成为Java企业应用开发的标准框架之一,它提供了众多的可配置功能模块和第三方组件,几乎可以解决企业开发中的所有问题。不过,Spring也带来了复杂的配置项,这对初学者而言简直就是灾难,于是Spring Boot应运而生。
Spring Boot将传统Web开发的mvc、json、validation、tomcat等框架整合,提供了spring-boot-starter-web组件,简化了Web应用配置、开发的难度,将初学者从繁杂的配置项中解放出来,专注于业务逻辑的实现。
4.1.1.1 spring-boot-starter-web组件介绍
Spring Boot自带的spring-boot-starter-web组件为Web应用开发提供支持,它内嵌的Tomcat以及Spring MVC的依赖使用起来非常方便。
Spring Boot创建Web应用非常简单,先创建一个普通的Spring Boot项目,然后修改pom.xml文件将spring-boot-starter-web组件加入项目就可以创建Web应用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
我们使用IDEA编辑器打开新创建的Web项目。打开Maven中的Dependencies,查看spring-boot-starter-web启动器(Starters)会引入哪些依赖JAR包,如图4-1所示。
其中webmvc为Web开发的基础框架,json为JSON数据解析组件,tomcat为自带的容器依赖。所以,只需引入spring-boot-starter-web启动器即可实现Web应用开发,而无须额外引入Tomcat以及其他Web依赖文件。
4.1.1.2 Web项目结构
Spring Boot的Web应用与其他的Spring Boot应用基本没有区别,只是resources目录中多了static静态资源目录以及templates页面模板目录。Spring Boot Web项目结构如图4-2所示。
Spring Boot不像传统的MVC框架那样必须继承某个基础类才能处理HTTP请求,只需要在类上声明@Controller注解,标注这是一个控制器,然后使用@RequestMapping注解把HTTP请求映射到对应的方法即可。具体使用如下:
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println(xuedenHello);
return xuedenHello;
}
}
上面的示例中,@RequestMapping注解用于定义请求的路由地址,既可以作用在方法上,又可以作用在类上。
4.1.2 @Controller和@RestController
Spring Boot提供了@Controller和@RestController两种注解来标识此类负责接收和处理HTTP请求。如果请求的是页面和数据,使用@Controller注解即可;如果只是请求数据,则可以使用@RestController注解。
4.1.2.1 @Controller的用法
Spring Boot提供的@Controller注解主要用于页面和数据的返回。下面创建HelloController响应前台页面请求,示例代码如下:
@Controller
@RequestMapping("user")
public class HelloController {
@RequestMapping("/index")
public String index(Model model){
model.addAttribute("name", "xueden");
return "index";
}
}
上面的示例用于请求/user/index地址,返回具体的index页面和name=xueden的数据。在前端页面中可以通过${name}参数获取后台返回的数据并显示到页面中。示例代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>这是一个首页</title>
</head>
<body>
<h1 th:text="${name}"></h1>
</body>
</html>
1、需要引用组件:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
2、在application.properties文件下新增如下代码
# 是否开启缓存,开发时可以设置为false,默认为true spring.thymeleaf.cache=false # 模板文件位置 spring.thymeleaf.prefix=classpath:/templates/ # Content-Type配置 spring.thymeleaf.servlet.content-type=text/html # 模板文件后缀 spring.thymeleaf.suffix=.html
在@Controller类中,如果只返回数据到前台页面,需要使用@ResponseBody注解,否则会报错。示例代码如下:
@RequestMapping("/hello")
@ResponseBody
public String hello(){
return "hello,world";
}
4.1.2.2 @RestController的用法
Spring Boot提供的@RestController注解用于实现数据请求的处理。默认情况下,@RestController注解会将返回的对象数据转换为JSON格式。示例代码如下:
@RequestMapping("/getUser")
public User getUser(){
User u = new User();
u.setName("xueden");
u.setAge(20);
u.setPassWorld("xueden");
return u;
}
User实体类代码如下:
@Data
public class User {
private String name;
private int age;
private String passWorld;
}
在上面的示例中,定义/user/getUser接口返回JSON格式的User数据。同时,@RequestMapping注解可以通过method参数指定请求的方式。如果请求方式不对,则会报错。
近几年前端框架越来越强大,前后端分离的RESTful架构成为主流。Spring Boot对RESTful也做了非常完善的支持,使用也特别简单,使用@RestController注解自动返回JSON格式的数据,同时使用@GetMapping、PostMapping等注解实现映射RESTful接口。
4.1.2.3 @RestController和@Controller的区别
@Controller和@RestController注解都是标识该类是否可以处理HTTP请求,可以说@RestController是@Controller和@ResponseBody的结合体,是这两个注解合并使用的效果。虽然二者的用法基本类似,但还是有一些区别,具体如下:
- @Controller标识当前类是Spring MVC Controller处理器,而@RestController则只负责数据返回。
- 如果使用@RestController注解,则Controller中的方法无法返回Web页面,配置的视图解析器InternalResourceViewResolver 不起作用,返回的内容就是Return中的数据。
- 如果需要返回指定页面,则使用@Controller注解,并配合视图解析器返回页面和数据。如果需要返回JSON、XML或自定义内容到页面,则需要在对应的方法上加上@ResponseBody注解。
- 使用@Controller注解时,在对应的方法上,视图解析器可以解析返回的JSP、HTML页面,并且跳转到相应页面。若返回JSON等内容到页面,则需要添加@ResponseBody注解。
- @RestController注解相当于@Controller和@ResponseBody两个注解的结合,能直接将返回的数据转换成JSON数据格式,无须在方法前添加@ResponseBody注解,但是使用@RestController注解时不能返回JSP、HTML页面,因为视图解析器无法解析JSP、HTML页面。
总之,在Web系统中使用@Controller较多,而在Web API中基本使用@RestController注解。
4.1.3 @RequestMapping
@RequestMapping注解主要负责URL的路由映射。它可以添加在Controller类或者具体的方法上,如果添加在Controller类上,则这个Controller中的所有路由映射都将会加上此映射规则,如果添加在方法上,则只对当前方法生效。@RequestMapping注解包含很多属性参数来定义HTTP的请求映射规则。常用的属性参数如下:
- value 英 [ˈvæljuː] 美 [ˈvæljuː] :请求URL的路径,支持URL模板、正则表达式。
- method 英 [ˈmeθəd] 美 [ˈmeθəd]:HTTP请求的方法。
- consumes 英 [kənˈsjuːmz] 美 [kənˈsuːmz] :允许的媒体类型,如consumes="application/json"为HTTP的Content-Type。
- produces 英 [prəˈdjuːsɪz] 美 [prəˈduːsɪz] :相应的媒体类型,如consumes="application/json"为HTTP的Accept 英 [əkˈsept] 美 [əkˈsept]字段。
- params:请求参数。
- headers:请求头的值。
以上属性基本涵盖了一个HTTP请求的所有参数信息。其中,value和method属性比较常用。
4.1.4 @ResponseBody
@ResponseBody注解主要用于定义数据的返回格式,作用在方法上,默认使用Jackson序列化成JSON字符串后返回给客户端,如果是字符串,则直接返回。
在Controller中有时需要返回JSON格式的数据,如果想直接返回数据体而不是视图名,则需要在方法上使用@ResponseBody。使用方式如下:
@RequestMapping("/getUser")
@ResponseBody
public User getUser(){
User u = new User();
u.setName("xueden");
u.setAge(18);
u.setPassWorld("xueden");
return u;
}
在上面的示例中,请求/user/getUser时,返回JSON格式的User数据。这与@RestController的作用类似。
需要注意的是,使用@ResponseBody注解时需要注意请求的类型和地址,如果期望返回JSON,但是请求URL以html结尾的页面,就会导致Spring Boot认为请求的是HTML类型的资源,而返回JSON类型的资源,与期望类型不一致,因此报出如下错误:
There was an unexpected error (type=Not Acceptable, status=406). Could not find acceptable representation
根据RESTful规范的建议,在Spring Boot应用中,如果期望返回JSON类型的资源,URL请求资源后缀就使用json;如果期望返回视图,URL请求资源后缀就使用html。
4.2 URL映射
上一节介绍了Spring Boot对Web项目开发都做了哪些支持,还介绍了@Controller和@RestController等注解,那么Spring Boot是如何将HTTP请求映射到具体方法的呢?Spring Boot支持URL路径匹配、HTTP Method匹配、params和header匹配等URL映射。本节开始介绍Spring Boot的URL映射。
4.2.1 URL路径匹配
4.2.1.1 精确匹配
@RequestMapping的value属性用于匹配URL映射,value支持简单表达式:
@RequestMapping("/getDataById/{id}")
public String getDataById(@PathVariable("id") Long id) {
return "getDataById:"+id ;
}
在上面的示例中,@PathVariable注解作用在方法参数中,用于表示参数的值来自URL路径。如果URL中的参数名称与方法中的参数名称一致,则可以简化为:
@RequestMapping("/getDataById/{id}")
public String getDataById(@PathVariable Long id) {
return "getDataById:"+id ;
}
在上面的示例中,当在浏览器中访问/getDataById/1时,会自动映射到后台的getDataById方法,传入参数id的值为1。
4.2.1.2 通配符匹配
@RequestMapping支持使用通配符匹配URL,用于统一映射某些URL规则类似的请求,示例代码如下:
@RequestMapping("/getJson/*.json")
public String getJson() {
return "get json data";
}
在上面的示例中,当在浏览器中请求/getJson/a.json或者/getJson/b.json时都会匹配到后台的Json方法。
@RequestMapping的通配符匹配非常简单实用,支持“*”“?”“**”等通配符。使用时需要了解通配符的匹配规则,否则容易出错。通配符匹配规则如下:
- 符号“*”匹配任意字符,符号“**”匹配任意路径,符号“?”匹配单个字符。
- 有通配符的优先级低于没有通配符的,比如/user/add.json比/user/*.json优先匹配。
- 有“**”通配符的优先级低于有“”通配符的。
4.2.2 Method匹配
HTTP请求Method有GET、POST、PUT、DELETE等方式。HTTP支持的全部Method和说明如表4-1所示。
对于Web应用,GET和POST是经常使用的选项,而对于RESTful接口,则会使用PUT、DELETE等从语义上进一步区分操作。
@RequestMapping注解提供了method参数指定请求的Method类型,包括RequestMethod.GET、RequestMethod.POST、RequestMethod.DELETE、RequestMethod.PUT等值,分别对应HTTP请求的Method。示例代码如下:
@RequestMapping(value="/getData",method = RequestMethod.GET)
public String getData() {
return "RequestMethod GET";
}
@RequestMapping(value="/getData",method = RequestMethod.POST)
public String PostData() {
return "RequestMethod POST";
}
上面的示例实现了GET和POST两种方式。当使用GET方式请求/data/getData接口时,会返回“RequestMethod GET”,使用POST方式请求/data/getData接口时,则返回“RequestMethod POST”,说明@RequestMapping通过HTTP请求Method映射不同的后台方法。
4.2.3 consumes和produces匹配
@RequestMapping注解提供了consumes和produces参数用于验证HTTP请求的内容类型和返回类型。
- consumes表示请求的HTTP头的Content-Type媒体类型与consumes的值匹配才可以调用方法。
- produces表示HTTP请求中的Accept字段只有匹配成功才可以调用。
下面通过示例演示consumes和produces参数的用法。
//处理request Content-Type为“application/json”类型的请求
@RequestMapping(value = "/Content", method = RequestMethod.POST, consumes = "application/json")
public String Consumes(@RequestBody Map param) {
return "Consumes POST Content-Type=application/json";
}
上面的示例只允许Content-Type=application/json的HTTP请求映射此方法,其他类型则返回“Unsupported Media Type”的错误。
4.2.4 params和header匹配
@RequestMapping注解提供了header参数和params参数,通过header参数可以根据HTTP请求中的消息头内容映射URL请求,通过params参数可以匹配HTTP中的请求参数实现URL映射。
4.2.4.1 params
Spring Boot除了通过匹配URL和Method的方式实现映射HTTP请求之外,还可以通过匹配params的方式来实现。Spring Boot从请求参数或HTTP头中提取参数,通过判断参数,如params="action=save"确定是否通过。同时还可以设置请求参数包含某个参数、不包含某个参数或者参数等于某个值时通过,具体如下:
- params={“username”},存在“username”参数时通过。
- params={"!password"},不存在“password”参数时通过。
- params={“age=20”},参数age等于20时通过。
通过@PostMapping设置的params参数来检查请求的params,实现HTTP的URL映射。示例代码如下:
@RequestMapping(value="paramsTest",params="action=save")
public String paramsTest(@RequestBody Map param){
return "params test";
}
在上面的示例中,当请求的参数action=save时,映射此方法。
4.2.4.2 header
header的使用和params类似,它检查HTTP的header头中是否有Host=localhost:8080的参数,如果有则匹配此方法。示例代码如下:
@RequestMapping(value="headerTest",headers={"Host=localhost:8080"})
public String headerTest(){
return "header test";
}
4.3 参数传递
本节开始介绍Spring Boot是如何实现参数传递的。参数传递是Web开发的基础内容,前端页面和后端服务通过请求和返回的参数来判断所执行的业务逻辑,因此参数的传递和接收是Web开发中最基础却非常重要的功能。
Spring Boot支持多种参数接收方式,通过提供注解来帮助限制请求的类型、接收不同格式的参数等,接下来我们通过示例一一介绍。
4.3.1 @PathVariable
在Web应用中,最常用的参数传递方式就是URL传参,也就是将参数放在请求的URL中。例如学灯网上不同用课程对应不同的URL:http://www.xueden.cn/details/69 http://www.xueden.cn/details/66 我们不可能对每个课程都定义一个@RequestMapping注解的方法来映射URL请求。对于相同模式的URL,可以采用同一种规则进行处理。
4.3.1.1 定义URL变量
@RequestMapping注解使用{}来声明URL变量,例如@RequestMapping("/user/{username}")。其中,{username}是定义的变量规则,username是变量的名字。此URL路由可以匹配下列任意URL请求:
/user/zhangsan
/user/lisi
/user/wangwu
在@RequestMapping中定义变量规则后,Spring Boot提供的@PathVariable注解帮助我们获取URL中定义的变量参数,示例如下:
@RequestMapping("/info/{username}")
@ResponseBody
public String userProfile(@PathVariable String username){
return "user:" + username;
}
在上面的示例中,Spring Boot会自动将URL中定义的变量传递给userProfile方法的username参数(同名赋值),例如当HTTP请求为/users/zhangsan时,URL变量username的值zhangsan会被赋给函数参数username,返回的数据为user:zhangsan。
需要注意的是,在默认情况下,变量参数不能包含URL的分隔符“/”,也就是说上面定义的URL路由不能匹配/users/lisi/zhang,即使lisi/zhang是一个存在的用户名。
4.3.1.2 定义多个URL变量
上面介绍了传递单个变量的例子,那么多个变量呢?同样,@RequestMapping支持定义包含多个URL变量的路由,示例如下:
@RequestMapping("/info/{username}/blog/{blogId}")
@ResponseBody
public String getUerBlog(@PathVariable String username , @PathVariable int blogId) {
return "user: " + username + "blog:" + blogId;
}
在上面的示例中,@RequestMapping("/user/{username}/blog/{blogId}")传入{username}和{blogId}两个参数,然后使用@PathVariable映射对应的变量参数。
在多变量参数的情况下,Spring Boot能够根据变量名自动赋值对应的函数参数值,也可以在@PathVariable中显式声明具体的URL变量名。
在默认情况下,@PathVariable注解的参数支持自动转换一些基本的数据类型,如int、long、date、string等,Spring Boot能够根据URL变量的具体值以及函数参数的数据类型来进行转换,例如/user/lisi/blog/1会将“lisi”的值赋给username,而1赋给int类型的变量blogId。
4.3.1.3 匹配正则表达式
虽然@RequestMapping路由支持URL变量,但是很多时候需要对URL变量进行更加精确的定义和限制,例如用户名只包含小写字母、数字、下划线:
/user/xueden是一个合法的URL
/user/#不是一个合法的URL
这种情况下,简单定义{username}变量就无法满足需求了。没关系,@RequestMapping注解同样支持正则表达式匹配,可以通过定义正则表达式更精确地控制,定义语法是{变量名:正则表达式},示例代码如下:
@RequestMapping("/info/{username:[a-zA-Z0-9_]+}/blog/{blogId}")
@ResponseBody
public String getUerBlog2(@PathVariable String username , @PathVariable int blogId) {
return "user: " + username + "blog:" + blogId;
}
在上面的示例中,使用[a-zA-Z0-9_]+正则表达式来限定username参数值只能包含小写字母、大写字母、数字、下划线。如此设置URL变量规则后,不合法的URL不会被处理,直接返回404Not Found。
4.3.2 使用Bean对象接收参数
针对参数较多的表单提交,Spring Boot可以通过创建一个JavaBean对象来接收HTTP传入的表单参数。需要注意的是,JavaBean对象中必须含有默认的构造函数,同时,需要设置属性字段必须有setter方法。
4.3.2.1 增加Bean实体类
首先,增加表单对应的实体类,具体代码如下:
@Data
public class Student {
private String name;
private String sex;
private String age;
}
上面的示例定义了User数据实体类。
4.3.2.2 增加后台方法
在StudentController控制器中增加save()方法,接收前台传来的数据。定义save()方法的示例代码如下:
@RequestMapping("/save")
public String save(Student student){
String name = student.getName();
String age = student.getAge();
String sex = student.getSex();
return name +" "+ age+" "+ sex;
}
定义Student对象的示例代码如下:
@Data
public class Student {
private String name;
private String sex;
private String age;
}
在浏览器中提交表单数据时,Spring Boot会自动把提交的表单数据转为Student对象,然后传递给save()方法。
4.3.3 @RequsetBody接收JSON数据
@RequestBody主要是将前端传入的JSON数据对象映射成后端的实体对象。比如,前端传入JSON格式的数据后,@RequestBody注解会自动将JSON数据反序列化成Student对象。使用时需要注意以下两点:
- 前端传递的对象属性和类型必须与后端对应。比如后端定义的user属性为“int id,String name”,前端必须使用相同的数据类型和字段来定义。
- 要使用JSON数据集进行传递,也就是设置为contentType: “application/json”。
下面通过示例代码演示如何使用@RequsetBody接收JSON数据:
@PostMapping(path = "/save2")
public String save2(@RequestBody Student student) {
String name = student.getName();
String age = student.getAge();
String sex = student.getSex();
return name +" "+ age+" "+ sex;
}
@PostMapping注解包含consumes参数,默认为application/json,表示前台需要传入JSON格式的参数。
另外,Spring Boot会根据名称一一对应,将数据转换成相应的数据类型。例如JSON数据中有int或date类型,前台传过来都是字符串,Spring Boot会自动将其转换成实体类中的数据类型。
4.3.4 @ModelAttribute
熟悉MVC的读者应该都知道,我们可以将@ModelAttribute注解放置在控制器(Controller)中的某个方法上。
当请求这个控制器中的某个URL时,会首先调用这个被注解的方法并将该方法的结果作为公共模型的属性,然后调用对应URL的处理方法,前端页面通过模型获取返回的数据。
@ModelAttribute标注的方法会在Controller类的每个映射URL的控制执行方法之前执行。使用方法如下面的示例代码所示:
@ModelAttribute
public void findUserById(@PathVariable("userId") Long userId, Model model) {
User tempUser = new User();
tempUser.setName("张三");
tempUser.setPassWorld("123456");
tempUser.setAge(18);
model.addAttribute("user", tempUser);
}
@GetMapping("/user/{userId}")
public String findUser(Model model) {
System.out.println(model.containsAttribute("user"));
return "success !";
}
在上面的示例中,当我们请求接口/user/1时,会先调用findUserById()方法,在方法内通过userId查询到对应的User对象放置到Model中。如果仅仅添加一个对象到Model中,上面的代码可以再精练一些:
@ModelAttribute
public User findUserById(@PathVariable("userId") Long userId) {
User tempUser = new User();
tempUser.setName("张三");
tempUser.setPassWorld("123456");
tempUser.setAge(18);
return tempUser;
}
通过上述代码返回的User对象会被自动添加到Model中,相当于手动调用了model.addAttribute(user)方法。
Model通过addAttribute()方法向页面传递参数。@ModelAttribute修饰的方法会先于findUser调用,它把请求的参数值赋给对应的变量。可以向方法中的Model添加对象,前提是在方法中加入一个Model类型的参数。
需要注意的是,被@ModelAttribute注释的方法会在此控制器的每个方法执行前被执行,因此对于一个控制器映射多个URL,要谨慎使用。
4.3.5 ModelAndView对象
ModelAndView也是Spring MVC中常用的数据返回对象。当控制器处理完请求时,通常会将包含视图对象和数据的ModelAndView对象一起返回前台。它的作用类似于request对象的setAttribute()方法。
ModelAndView对象有两个作用:
- 设置转向地址(这也是ModelAndView和ModelMap的主要区别)。
- 将后台数据传回到前台页面。
ModelAndView使用起来也特别简单,在控制器中把前台页面需要的数据放到ModelAndView对象中,然后返回mv对象。下面通过示例演示使用ModelAndView对象返回数据到前台页面:
@RequestMapping(value="/detail/{id}")
public ModelAndView detail(@PathVariable Long id){
ModelAndView mv = new ModelAndView();
User tempUser = new User();
tempUser.setName("张三");
tempUser.setPassWorld("123456");
tempUser.setAge(18);
tempUser.setId(id);
// 设置user对象的username属性
mv.addObject("user",tempUser);
// 地址跳转,设置返回的视图名称
mv.setViewName("detail");
return mv;
}
detail.html页面代码如下所示:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户详情页</title>
</head>
<body>
<h1 th:text="${user.name}"></h1>
</body>
</html>
上面的示例中,先获取用户数据,然后将数据和对象一起返回到前台detail页面。这样Spring MVC将使用包含的视图对模型数据进行渲染。
4.4 数据验证
对于应用系统而言,任何客户端传入的数据都不是绝对安全有效的,这就要求我们在服务端接收到数据时也对数据的有效性进行验证,以确保传入的数据安全正确。接下来介绍Spring Boot是如何实现数据验证的。
4.4.1 Hibernate Validator简介
数据校验是Web开发中的重要部分,也是必须要考虑和面对的事情。应用系统必须通过某种手段来确保输入的数据从语义上来讲是正确的。
目前数据校验的规范、组件非常多,有JSR-303/JSR-349、Hibernate Validator 英 [ˈvælɪdeɪtə] 美 [ˈvælədeɪtər] 、Spring Validation。下面就来捋一捋它们之间的关系。
- JSR(Java Specification 英 [ˌspesɪfɪˈkeɪʃn] 美 [ˌspesɪfɪˈkeɪʃn] Request)规范是Java EE 6中的一项子规范,也叫作Bean Validation。它指定了一整套基于bean的验证API,通过标注给对象属性添加约束条件。
- Hibernate Validator是对JSR规范的实现,并增加了一些其他校验注解,如@Email、@Length、@Range等。
- Spring Validation是Spring为了给开发者提供便捷,对Hibernate Validator进行了二次封装。同时,Spring Validation在SpringMVC模块中添加了自动校验,并将校验信息封装进了特定的类中。
所以,JSR定义了数据验证规范,而Hibernate Validator则是基于JSR规范,实现了各种数据验证的注解以及一些附加的约束注解。Spring Validation则是对Hibernate Validator的封装整合。
JSR和Hibernate Validator中的常用注解如表4-2所示。
表4-2中包含了Hibernate Validator实现的JSR-303定义的验证注解和Hibernate Validator自己定义的验证注解,同时也支持自定义约束注解。所有的注解都包含code和message这两个属性。
- Message定义数据校验不通过时的错误提示信息。
- code定义错误的类型。
Spring Boot是从Spring发展而来的,所以自然支持Hibernate Validator和Spring Validation两种方式,默认使用的是Hibernate Validator组件。
4.4.2 数据校验
使用Hibernate Validator校验数据需要定义一个接收的数据模型,使用注解的形式描述字段校验的规则。下面以User对象为例演示如何使用Hibernate Validator校验数据。
4.4.2.1 JavaBean参数校验
Post请求参数较多时,可以在对应的数据模型(Java Bean)中进行数据校验,通过注解来指定字段校验的规则。下面以具体的实例来进行演示。
首先,创建Java Bean实体类:
@Data
public class User {
private Long id;
@NotBlank(message = "姓名不允许为空!")
@Length(min = 2,max = 10,message = "姓名长度错误,姓名长度2-10!")
private String name;
@NotNull(message = "年龄不能为空!")
@Min(18)
private int age;
@NotBlank(message = "地址不能为空!")
private String address;
@Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
private String phone;
@Email(message = "邮箱格式错误")
private String email;
private String passWorld;
}
在上面的示例中,每个注解中的属性message是数据校验不通过时要给出的提示信息,如@Email(message=“邮件格式错误”),当邮件格式校验不通过时,提示邮件格式错误。
然后,添加数据校验方法:
@PostMapping(path = "/check")
public String check(@RequestBody @Valid User user, BindingResult result) {
String name = user.getName();
if (result.hasErrors()) {
List<ObjectError> list = result.getAllErrors();
for (ObjectError error : list) {
System.out.println(error.getCode() + "-" + error.getDefaultMessage());
}
}
return name;
}
在上面的示例中,在@RequestBody注解后面添加了@Valid注解,然后在后面添加了BindingResult返回验证结果,BindingResult是验证不通过时的结果集合。
注意,BindingResult必须跟在被校验参数之后,若被校验参数之后没有BindingResult对象,则会抛出BindException。
4.4.2.2 URL参数校验
一般GET请求都是在URL中传入参数。对于这种情况,可以直接通过注解来指定参数的校验规则。下面通过实例进行演示。
@RequestMapping("/query")
public String query(@Length(min = 2, max = 10, message = "姓名长度错误,姓名长度2-10!")
@RequestParam(name = "name", required = true) String name,
@Min(value = 1, message = "年龄最小只能1")
@Max(value = 99, message = "年龄最大只能99")
@RequestParam(name = "age", required = true) int age){
System.out.println(name + "," + age);
return name + "," + age;
}
在上面的示例中,使用@Range、@Min、@Max等注解对URL中传入的参数进行校验。需要注意的是,使用@Valid注解是无效的,需要在方法所在的控制器上添加@Validated注解来使得验证生效。
4.4.2.3 JavaBean对象级联校验
对于JavaBean对象中的普通属性字段,我们可以直接使用注解进行数据校验,那如果是关联对象呢?其实也很简单,在属性上添加@Valid注解就可以作为属性对象的内部属性进行验证(验证User对象,可以验证UserDetail的字段)。示例代码如下:
@Data
public class UserDetail {
@Length(min = 5, max = 17, message = "length长度在[5,17]之间")
private String remarks;
}
在User实体类添加如下代码:
@NotNull
@Valid
private UserDetail userDetail;
在上面的示例中,在属性上添加@Valid就可以对User中的关联对象UserDetail的字段进行数据校验。
4.4.2.4 分组校验
在不同情况下,可能对JavaBean对象的数据校验规则有所不同,有时需要根据数据状态对JavaBean中的某些属性字段进行单独验证。这时就可以使用分组校验功能,即根据状态启用一组约束。Hibernate Validator的注解提供了groups参数,用于指定分组,如果没有指定groups参数,则默认属于javax.validation.groups.Default分组。
应用场景: 新增用户信息的时候,不需要验证userId(因为系统生成);修改的时候需要验证userId,这时候可用用户到validator的分组验证功能。
首先,创建分组GroupA和GroupB 示例代码如下:
public interface GroupA {
}
public interface GroupB {
}
然后,创建实体类Person,并在相关的字段中定义校验分组规则,示例代码如下:
@Data
public class Person {
@NotBlank
@Range(min = 1,max = Integer.MAX_VALUE,message = "必须大于0",groups = {GroupA.class})
/**用户id*/
private Integer userId;
@NotBlank
@Length(min = 4,max = 20,message = "必须在[4,20]",groups = {GroupB.class})
/**用户名*/
private String userName;
@NotBlank
@Range(min = 0,max = 100,message = "年龄必须在[0,100]",groups={Default.class})
/**年龄*/
private Integer age;
@Range(min = 0,max = 2,message = "性别必须在[0,2]",groups = {GroupB.class})
/**性别 0:未知;1:男;2:女*/
private Integer sex;
}
如上Person所示,3个分组分别验证字段如下:
- GroupA验证字段userId;
- GroupB验证字段userName、sex;
- Default验证字段age(Default是Validator自带的默认分组)
最后,使用校验分组:
@RequestMapping("/save")
public void save(@Validated({GroupA.class, GroupB.class}) Person p, BindingResult result){
if(result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println(error);
}
}
}
在上面的示例中,在@Validated注解中增加了{GroupA.class,Default.class}参数,表示对于定义了分组校验的字段使用GroupA校验规则,其他字段使用默认规则。
除了按组指定是否验证之外,还可以指定组的验证顺序,前面组验证不通过的,后面组不进行验证:
首先, 指定组的序列(GroupA》GroupB》Default):
@GroupSequence({GroupA.class, GroupB.class, Default.class})
public interface GroupOrder {
}
最后,使用校验分组:
@RequestMapping("/save2")
public void save2(@Validated({GroupOrder.class}) Person p, BindingResult result){
if(result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println(error);
}
}
}
在上面的示例中,分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不需要验证。
4.4.3 自定义校验
Hibernate Validator支持自定义校验规则。通过自定义校验规则,可以实现一些复杂、特殊的数据验证功能。下面通过示例演示如何创建和使用自定义验证规则。
4.4.3.1 声明一个自定义校验注解
首先,定义新的校验注解@CustomUserNameValidator,示例代码如下:
@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME) //表示会被JVM保留,可以在运行时被使用
@Constraint(validatedBy = CheckCaseValidator.class) //指定校验器
@Documented // 将注解中的元素包含到 Javadoc 中去
public @interface CustomUserNameValidator {
String message() default ""; // message方法是每个校验注解必备方法,主要用于设置校验失败的提示信息。
Class<?>[] groups() default {}; // 这个方法时用来实现分组校验功能的
Class<? extends Payload>[] payload() default {}; // 这个方法用于设置校验负载
CaseMode value(); // 校验大小写枚举类
}
然后定义一个枚举类,示例代码如下:
public enum CaseMode {
UPPER,
LOWER;
}
接着定义一个校验类,示例代码如下:
public class CheckCaseValidator implements ConstraintValidator<CustomUserNameValidator, String> {
private CaseMode caseMode;
public void initialize(CustomUserNameValidator checkCase) {
this.caseMode = checkCase.value();
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (s == null) {
return true;
}
if (caseMode == CaseMode.UPPER) {
return s.equals(s.toUpperCase());
} else {
return s.equals(s.toLowerCase());
}
}
}
4.4.3.2 使用自定义校验注解
创建自定义校验注解CustomUserNameValidator之后,在Person类中的trueName属性上使用自定义组合注解,示例代码如下:
@CustomUserNameValidator(value = CaseMode.LOWER,message = "trueName必须是小写",groups={GroupB.class})
private String trueName;
在上面的示例中,我们在需要做特殊校验的trueName字段上添加@CustomUserNameValidator自定义注解,这样trueName字段就会使用我们自定义的校验规则。
4.5 拦截器
拦截器在Web系统中非常常见,一般用于拦截用户请求,实现访问权限控制、日志记录、敏感过滤等功能。本节首先介绍实际项目中拦截器的应用场景,然后介绍如何实现自定义拦截器的功能。
4.5.1 应用场景
拦截器在实际的应用开发中非常常见,对于某些全局统一的操作,我们可以把它提取到拦截器中实现。总结起来,拦截器大致有以下几种使用场景:
- 权限检查:如登录检测,进入处理程序检测是否登录,如果没有,则直接返回登录页面。
- 性能监控:有时系统在某段时间莫名其妙很慢,可以通过拦截器在进入处理程序之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如Apache,可以自动记录)。
- 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有提取Locale、Theme信息等,只要是多个处理程序都需要的,即可使用拦截器实现。
- OpenSessionInView:如Hibernate,在进入处理程序时打开Session(会话),在完成后关闭Session。
4.5.2 HandlerInterceptor简介
Spring Boot定义了HandlerInterceptor接口来实现自定义拦截器的功能。HandlerInterceptor接口定义了preHandle、postHandle、afterCompletion三种方法,通过重写这三种方法实现请求前、请求后等操作。
-
preHandle:预处理回调方法实现处理程序的预处理(如登录检查),第三个参数为响应的处理程序。
返回值:true表示继续流程(如调用下一个拦截器或处理程序);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理程序,此时需要通过response来产生响应。
-
postHandle:后处理回调方法,实现处理程序的后处理(但在渲染视图之前),此时可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
-
afterCompletion:整个请求处理完之后回调方法,即在视图渲染完毕时回调,如在性能监控中,可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但是只调用处理程序执行preHandle,返回true所对应的拦截器的afterCompletion。
有时我们只需要实现3种回调方法之一,如果实现HandlerInterceptor接口,则无论是否需要3种方法都必须实现,此时Spring提供了一个HandlerInterceptorAdapter适配器(一种适配器设计模式的实现),允许我们只实现需要的回调方法。
4.5.3 使用HandlerInterceptor实现拦截器
我们在访问某些需要授权的页面,如订单详情、订单列表等需要用户登录后才能查看的功能时,需要对这些请求拦截,进行登录检测,符合规则的才允许请求通过。接下来通过登录状态检测的例子演示拦截器的使用。
步骤01 创建自定义登录拦截器,示例代码如下:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
User user = (User)request.getSession().getAttribute("user");
if(user==null){
request.setAttribute("msg","您没有权限操作!");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
return true;
}
}
在上面的示例中,LoginInterceptor继承HandlerInterceptor接口,实现preHandle接口,验证用户的Session状态。如果当前用户有登录信息,则可以继续访问;如果当前用户没有登录信息,则返回无权限。
步骤02 将拦截器注入系统配置。
定义MyMvcConfig配置类,将上面定义的LoginInterceptor拦截器注入系统中,示例代码如下:
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**").excludePathPatterns("/","/user/login",
"/asserts/**","/webjars/**");
}
}
通过WebMvcConfigurer类的addInterceptors方法将刚刚自定义的LoginInterceptor拦截器注入系统中。
- addPathPatterns 英 [ˈpætnz] 美 [ˈpætərnz] 定义拦截的请求地址。
- excludePathPatterns的作用是排除某些地址不被拦截,例如登录地址/user/login不需要进行登录验证。
4.6 过滤器
本节介绍如何使用Spring Boot实现自定义过滤器,在开发Web项目时,经常需要过滤器(Filter)来处理一些请求,包括字符集转换、过滤敏感词汇等场景。
4.6.1 过滤器简介
过滤器是Java Servlet规范中定义的,能够在HTTP请求发送给Servlet之前对Request(请求)和Response(返回)进行检查和修改,从而起到过滤的作用。通过对Web服务器管理的所有Web资源(如JSP、Servlet、静态图片文件或静态HTML文件等)过滤,实现特殊的功能,例如,实现URL级别的权限访问控制、过滤敏感词汇、排除有XSS威胁的字符等。
Spring Boot内置了很多过滤器,比如处理编码的OrderedCharacterEncodingFilter和请求转化的HiddenHttpMethodFilter,也支持根据实际需求自定义过滤器。自定义过滤器有两种实现方式:第一种是使用@WebFilter,第二种是使用FilterRegistrationBean。经过实践之后,发现使用@WebFilter自定义的过滤器优先级顺序不能生效,因此推荐使用第二种方案。
过滤器和拦截器的功能类似,但技术实现差距比较大,两者的区别包括以下几个方面:
- 过滤器依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。
- 过滤器的执行由Servlet容器回调完成,而拦截器通常通过动态代理的方式来执行。
- 过滤器的生命周期由Servlet容器管理,而拦截器可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用更方便。
拦截器和过滤器的执行顺序是:先过滤器后拦截器。具体执行过程为:过滤前→拦截前→执行→拦截后→过滤后。
4.6.2 使用@WebFilter实现过滤器
给某个类添加上@WebFilter则表示这个类就是一个过滤器。过滤器里面的三个方法 ,说明如下:
-
filter对象只会创建一次,init方法也只会执行一次。
-
doFilter : 主要的业务代码编写方法,可以多次重复调用
-
destroy 英 [dɪˈstrɔɪ] 美 [dɪˈstrɔɪ] : 在销毁Filter时自动调用(程序关闭或者主动销毁Filter)。
使用@WebFilter实现过滤器,实现自定义过滤器的步骤如下:
步骤01 创建自定义过滤器,示例代码如下:
@WebFilter(filterName = "consumerLoginFilter",urlPatterns = "/**",initParams = {
@WebInitParam(name = "URL",value = "http://localhost:8099")
})
public class ConsumerLoginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化登录过滤器");
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
System.out.println("销毁登录过滤器");
Filter.super.destroy();
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("进入处理登录过滤器业务");
}
}
参数说明如下:
- @Order(1):表示过滤器的顺序,假设我们有多个过滤器,你如何确定过滤器的执行顺序?这个注解就是规定过滤器的顺序。
- WebFilter:表示这个class是过滤器。
里面的参数,filterName 为过滤器名字,urlPatterns 为过滤器的范围,initParams 为过滤器初始化参数。
步骤02 启动类中增加@ServletComponentScan注解,自动注册Filter
在SpringBootApplication上使用@ServletComponentScan注解后,Servlet、Filter、Listener可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册,无需其他代码。
@SpringBootApplication
@ServletComponentScan
public class SpringbootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDemoApplication.class, args);
}
}
4.6.3 使用FilterRegistrationBean实现过滤器
Spring Boot提供了FilterRegistrationBean 英 [ˌredʒɪˈstreɪʃn] 美 [ˌredʒɪˈstreɪʃn] 类实现过滤器注入,实现自定义过滤器的步骤如下:
- 添加自定义Filter类,实现Filter接口,并实现其中的doFilter()方法。
- )添加@Configuration注解,将自定义过滤器加入过滤链。
接下来以监控请求执行时间为例,通过自定义过滤器实现系统性能监控的功能。步骤如下:
步骤01 创建过滤器,示例代码如下:
@Component
public class ConsumerTimerFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化过滤器");
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
System.out.println("销毁过滤器");
Filter.super.destroy();
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
long start=new Date().getTime();
System.out.println("开始时间:"+start);
filterChain.doFilter(servletRequest, servletResponse);
long end=new Date().getTime();
System.out.println("结束时间:"+end);
System.out.println("消耗时间:"+(end-start));
}
}
上面的示例代码实现了doFilter()方法记录所有HTTP请求的时间。
步骤02 将过滤器注入系统配置中。
通过FilterRegistrationBean类将定义的ConsumerTimerFilter过滤器注入系统中,并配置过滤的地址和执行顺序,示例代码如下:
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean consumerLoginFilterRegistration(){
FilterRegistrationBean<ConsumerTimerFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new ConsumerTimerFilter());
registration.addUrlPatterns("/*");
registration.setName("ConsumerTimerFilter");
registration.setOrder(2);
return registration;
}
}
使用registration.setOrder(2)进行排序,数字越小越先执行。当有多个过滤器时,通过设置Order属性决定过滤器的执行顺序。
开始时间:1641179458041
结束时间:1641179458062
消耗时间:21
如上所示,后台输出了请求的消耗时间,说明刚刚自定义的过滤器已经对所有的URL进行了过滤处理。
4.7 Web配置
本节介绍Spring Boot Web中非常重要的类:WebMvcConfigurer。有时我们需要自定义Handler、Interceptor、ViewResolver、MessageConverter实现特殊的Web配置功能,通过WebMvcConfigurer接口即可实现项目的自定义配置。
4.7.1 WebMvcConfigurer简介
在Spring Boot 1.5版本都是靠重写WebMvcConfigurerAdapter的方法来添加自定义拦截器、消息转换器等。Spring Boot 2.0以后,该类被标记为@Deprecated(弃用)。官方推荐直接实现WebMvcConfigurer接口或者直接继承WebMvcConfigurationSupport类。
WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的XML配置文件形式进行针对框架的个性化定制,可以自定义Handler、Interceptor、ViewResolver、MessageConverter。基于java-based方式的Spring MVC配置需要创建一个配置类并实现WebMvcConfigurer接口。
4.7.2 跨域访问
出于安全的考虑,浏览器会禁止Ajax访问不同域的地址,而在如今微服务横行的年代,跨域访问是非常常见的。这就需要应用系统既要保证系统安全,又要对前端跨域访问提供支持。所以W3C提出了CORS(Cross-Origin-Resource-Sharing)跨域访问规范,并被主流浏览器所支持。
Spring Boot可以基于CORS解决跨域问题,CORS是一种机制,告诉后台哪边(Origin)来的请求可以访问服务器的数据。Spring Boot 配置 CORS常用有两种方式,下面我们就分别实现这两种方式。
4.7.2.1 使用@CrossOrigin
注解实现
-
如果想要对某一接口配置
CORS
,可以在方法上添加@CrossOrigin
注解示例代码如下:@RestController public class CorsController { @CrossOrigin(origins = {"http://localhost:9000", "null"}) @RequestMapping(value = "/testCors", method = RequestMethod.GET) public String testCors(){ return "测试使用@CrossOrigin实现跨域"; } }
-
如果想对一系列接口添加 CORS 配置,可以在类上添加注解,对该类声明所有接口都有效示例代码如下所示:
@RestController
@CrossOrigin(origins = {"http://localhost:9000", "null"})
public class CorsController {
}
4.7.2.2 使用WebMvcConfigurer实现跨域
WebMvcConfigurer配置类中的addCorsMappings()方法是专门为开发人员解决跨域而诞生的接口,其中构造参数为CorsRegistry,示例代码如下:
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许整个系统支持跨域访问
.allowedHeaders("*") // 器允许使用的字段
.allowedMethods("POST","GET","DELETE","PUT") // 请求允许的方法
// .allowedOrigins("*")// 指定允许其他域名访问
.allowedOriginPatterns("*") // 设置允许跨域请求的域名
.allowCredentials(true) //是否允许后续请求携带认证信息(cookies)
.maxAge(1200);//预检结果缓存时间
}
从上面的示例代码可以看出,将pathPattern设置为/**,即整个系统支持跨域访问。当然也可以根据不同的项目路径定制访问行为。CorsRegistry提供了registrations属性,通过getCorsConfigurations()方法设置特定路径的跨域访问。
4.7.3 数据转换配置
Spring Boot支持对请求或返回的数据类型进行转换,常用到的是统一对返回的日期数据自动格式化。配置如下:
// 定义时间格式转换器
@Bean
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
converter.setObjectMapper(mapper);
return converter;
}
在上面的示例中,首先创建一个MessageConverter时间格式转换器,将设置时间的格式为"yyyy-MM-dd HH:mm:ss",然后configureMessageConverters方法将转换器添加到系统中。这样JSON数据格式化时,统一将时间类型转换成我们定义的格式。
4.7.4 静态资源
在开发Web应用的过程中,需要引用大量的JS、CSS、图片等静态资源。Spring Boot默认提供静态资源的目录置于classpath下,目录名规则如下:
- /static
- /public
- /resources
- /META-INF/resources
比如,我们可以在src/main/resources/目录下创建static,在该位置放置一个文件名为xx.jpg的图片。启动程序后,访问http://localhost:8080/xx.jpg即可访问该图片,无须其他额外配置。
Spring Boot同样支持自定义静态资源目录,如果需要自定义静态资源映射目录,只需重写addResourceHandlers()方法即可,示例代码如下:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 处理静态资源,例如图片、JS、CSS等
registry.addResourceHandler("/images/**").addResourceLocations
("classpath:images/");
}
在上面的示例中,创建的webconfig类继承自WebMvcConfigure类,重写了addResourceHandler()方法,通过addResourceHandler添加映射路径,然后通过addResourceLocations来指定路径。
- addResourceLocations指的是文件放置的目录。
- addResoureHandler指的是对外暴露的访问路径。
4.7.5 跳转指定页面
以前编写Spring MVC的时候,如果需要访问一个页面,必须要在Controller类中编写一个页面跳转的方法。Spring Boot重写WebMvcConfigurer中的addViewControllers()方法即可达到同样的效果。示例代码如下:
// 跳转指定页面
@Override
public void addViewControllers(ViewControllerRegistry registry) {
WebMvcConfigurer.super.addViewControllers(registry);
registry.addViewController("/").setViewName("/index");
// 实现一个请求到视图的映射,无须编写controller
registry.addViewController("/login").setViewName
("forward:/index.html");
}
值得指出的是,在这里重写addViewControllers()方法并不会覆盖WebMvcAutoConfiguration中的addViewControllers(在此方法中,Spring Boot将“/”映射至index.html),这就意味着我们自己的配置和Spring Boot的自动配置同时有效,这也是推荐添加自己的MVC配置的原因。
4.8 实现数据返回
本节介绍如何让前后台优雅地进行数据交互,正常的数据如何统一数据格式,以及异常情况如何统一处理并返回统一格式的数据。
4.8.1 为什么要统一返回值
在项目开发过程中经常会涉及服务端、客户端接口数据传输或前后台分离的系统架构下的数据交互问题。如何确保数据完整、清晰易懂是考验开发者的大难题。定义统一的数据返回格式有利于提高开发效率、降低沟通成本,降低调用方的开发成本。目前比较流行的是基于JSON格式的数据交互。但是JSON只是消息的格式,其中的数据内容还需要重新设计和定义。无论是HTTP接口还是RPC接口,保持返回值格式统一很重要。
在项目中,我们会将响应封装成JSON返回,一般会统一所有接口的数据格式,使前端(iOS、Android、Web)对数据的操作一致、轻松。一般情况下,统一返回数据格式没有固定的规范,只要能描述清楚返回的数据状态以及要返回的具体数据即可,但是一般会包含状态码、消息提示语、具体数据这3部分内容。例如,一般的系统要求返回的基本数据格式如下:
{
"code": 200,
"message": "成功",
"data": {
"items": [
{
"id": "1",
"name": "xueden",
"intro": "备注"
}
]
}
}
通过上面的示例我们知道,定义的返回值包含4要素:响应结果、响应码、消息、返回数据。
4.8.2 统一数据返回
前面介绍了为什么要统一返回值以及如何实现统一JSON数据返回。接下来通过示例演示如何实现统一JSON数据返回。
4.8.2.1 定义数据格式
定义返回值的基本要素,确保后台执行无论成功还是失败都是返回这些字段,而不会出现其他的字段。定义的返回值包含如下内容:
- Integer code:成功时返回0,失败时返回具体错误码。
- String message:成功时返回null,失败时返回具体错误消息。
- T data:成功时返回具体值,失败时为null。
根据上面的返回数据格式的定义,实际返回的数据模板如下:
{
"code": 200,
"message": "成功",
"data": {
"items": [
{
"id": "1",
"name": "xueden",
"intro": "备注"
}
]
}
}
其中,data字段为泛型字段,根据实际的业务返回前端需要的数据类型。
4.8.2.2 定义状态码
返回的数据中有一个非常重要的字段:状态码。状态码字段能够让服务端、客户端清楚知道操作的结果、业务是否处理成功,如果失败,失败的原因等信息。所以,定义清晰易懂的状态码非常重要。状态码定义如表4-3所
以上定义的是通用状态码,其他的业务相关状态码需要根据实际业务定义。
4.8.2.3 定义数据处理类
前面定义了返回数据的格式和处理结果的状态码,接下来定义通用的结果处理类。在实际使用时可以根据情况处理。本示例中简单定义如下:
@Data
public class JSONResult {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
// 响应业务状态
private Integer code;
// 响应消息
private String msg;
// 响应中的数据
private Object data;
public static JSONResult ok(Object data) {
return new JSONResult(data);
}
public static JSONResult ok() {
return new JSONResult(null);
}
public static JSONResult errorMsg(String msg) {
return new JSONResult(500, msg, null);
}
public static JSONResult errorMap(Object data) {
return new JSONResult(501, "error", data);
}
public static JSONResult errorTokenMsg(String msg) {
return new JSONResult(502, msg, null);
}
public static JSONResult errorException(String msg) {
return new JSONResult(555, msg, null);
}
public JSONResult() {
}
public JSONResult(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public JSONResult(Object data) {
this.code = 200;
this.msg = "OK";
this.data = data;
}
public Boolean isOK() {
return this.code == 200;
}
}
上面定义了数据返回处理类,定义了响应数据结构,所有接口的数据返回统一通过此类处理,接收此类数据后,需要使用本类的方法转换成对应的数据类型格式(类或者list)。
4.8.2.4 处理数据返回
定义数据处理类后,在控制器中将返回的数据统一加上数据处理。调用如下:
@RequestMapping("getPerson")
public JSONResult getPerson() {
Person person = new Person();
person.setAge(18);
person.setSex(1);
person.setTrueName("张三");
person.setUserName("xueden");
person.setCDate(new Date());
return new JSONResult(200,"获取成功",person);
}
返回的结果数据在正常的时候能够按照我们的预期结果格式返回。
4.8.3 全局异常处理
在项目开发的过程中肯定会碰到异常的情况,出现异常情况时如何处理,如何确保出现异常时程序也能正确地返回数据?总不能所有的方法都加上try catch吧?接下来介绍Spring Boot如何进行全局异常处理,捕获异常后如何按照统一格式返回数据。
4.8.3.1 全局异常处理的实现方式
在介绍之前,我们需要先了解Spring中常见的异常处理方式有哪些。一般Spring Boot框架的异常处理有多种方式,从范围来说,包括全局异常捕获处理方式和局部异常捕获处理方式。下面介绍3种比较常用的异常处理解决方案。
-
使用@ExceptionHandler处理局部异常
在控制器中通过加入@ExceptionHandler注解的方法来实现异常的处理。这种方式非常容易实现,但是只能处理使用@ExceptionHandler注解方法的控制器异常,而无法处理其他控制器的异常,所以不推荐使用。
-
配置SimpleMappingExceptionResolver类来处理异常
通过配置SimpleMappingExceptionResolver类实现全局异常的处理,但是这种方式不能针对特定的异常进行特殊处理,所有的异常都按照统一的方式处理。
-
使用ControllerAdvice注解处理全局异常
使用@ControllerAdvice、@ExceptionHandler注解实现全局异常处理,@ControllerAdvice定义全局异常处理类,@ExceptionHandler指定自定义错误处理方法拦截的异常类型。实现全局异常捕获,并针对特定的异常进行特殊处理。
以上三种解决方案,都能实现全局异常处理。但是,推荐使用@ControllerAdvice注解方式处理全局异常,这样可以针对不同的异常分开处理。
4.8.3.2 使用@ControllerAdvice注解实现全局异常处理
下面通过示例演示@ControllerAdvice注解实现全局统一异常处理。
定义一个自定义的异常处理类GlobalExceptionHandler,具体示例代码如下:
@ControllerAdvice
public class GlobalExceptionHandler {
public static final String ERROR_VIEW = "error";
Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(value = {Exception.class })
public Object errorHandler(HttpServletRequest reqest,
HttpServletResponse response, Exception e) throws Exception {
e.printStackTrace();
// 是否是Ajax请求
if (isAjax(reqest)) {
return JSONResult.errorException(e.getMessage());
} else {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", reqest.getRequestURL());
mav.setViewName(ERROR_VIEW);
return mav;
}
}
/**
*
* @Title: GlobalExceptionHandler.java
* @Package com.weiz.exception
* @Description: 判断是否是Ajax请求
*/
public static boolean isAjax(HttpServletRequest httpRequest){
return (httpRequest.getHeader("X-Requested-With") != null
&& "XMLHttpRequest"
.equals( httpRequest.getHeader("X-Requested-With")) );
}
}
上面的示例,处理全部Exception的异常,如果需要处理其他异常,例如NullPointerException异常,则只需要在GlobalException类中使用@ExceptionHandler(value = {NullPointerException.class})注解重新定义一个异常处理的方法即可。
vice注解实现全局异常处理
下面通过示例演示@ControllerAdvice注解实现全局统一异常处理。
定义一个自定义的异常处理类GlobalExceptionHandler,具体示例代码如下:
@ControllerAdvice
public class GlobalExceptionHandler {
public static final String ERROR_VIEW = "error";
Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(value = {Exception.class })
public Object errorHandler(HttpServletRequest reqest,
HttpServletResponse response, Exception e) throws Exception {
e.printStackTrace();
// 是否是Ajax请求
if (isAjax(reqest)) {
return JSONResult.errorException(e.getMessage());
} else {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", reqest.getRequestURL());
mav.setViewName(ERROR_VIEW);
return mav;
}
}
/**
*
* @Title: GlobalExceptionHandler.java
* @Package com.weiz.exception
* @Description: 判断是否是Ajax请求
*/
public static boolean isAjax(HttpServletRequest httpRequest){
return (httpRequest.getHeader("X-Requested-With") != null
&& "XMLHttpRequest"
.equals( httpRequest.getHeader("X-Requested-With")) );
}
}
上面的示例,处理全部Exception的异常,如果需要处理其他异常,例如NullPointerException异常,则只需要在GlobalException类中使用@ExceptionHandler(value = {NullPointerException.class})注解重新定义一个异常处理的方法即可。