Spring Boot对表单实体做REST API请求自动封装验证

阅读Spring.io官网的valid表单验证demo和教程。我先说一下spring Boot官方的教程,然后再简单说一下统一验证管理的一些写法方便懒人或者说代码优化。

表单的注解 需要项目依赖于hibernate-validtor组件,在spring-boot-starter-web中已经自带了hibernate-validtor,无需再对maven进行依赖。

创建一个Spring boot的项目,pom文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework</groupId>
<artifactId>gs-validating-form-input</artifactId>
<version>0.1.0</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
</parent>

<properties>
<java.version>1.8</java.version>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

定义实体

定义个PersonForm实体,并填写注解信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package hello;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class PersonForm {
@NotNull
@Size(min=2, max=30)
private String name;

@NotNull
@Min(18)
private Integer age;

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String toString() {
return "Person(Name: " + this.name + ", Age: " + this.age + ")";
}
}

PersonForm 类验证两个信息,一个是name,限制长度在2~30位之间;age,限制最小值18。

创建一个Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("test1")
public AjaxResponse test(@Valid PersonForm personForm,BindingResult bindingResult){//【1】
if (bindingResult.hasErrors()) {//【2】
List<FieldError> errorList = bindingResult.getFieldErrors();
String errorMsg = errorList.get(0).getField() + " 字段错误,错误原因:" + errorList.get(0).getDefaultMessage();
return error(errorMsg, FIELD_VALIDATE_ERROR.getKey());
} else {
return error(GLOBAL_UNKNOWN_ERROR);
}
return ok("suc");
}
}
  1. 定义了一个Rest的Controller,在1处增加一个Valid注解
  2. 在第二个参数定义BindingResult对象,Spring将会为我们自动注入该值
  3. 通过Controller方法去验证本次请求是否成功,如果此处不传递BindingResult对象,Spring将会向上抛出一个BindException异常,如果我们不做处理,Spring 内部对该异常做了一个html渲染异常的处理。

启动项目,使用postman或其它http请求模拟插件做一个post请求http://localhost:8080/test/test1,通过填写的参数是否符合规范将返回不同的信息。

通过例子我想大家已经学会使用Valid注解了,但是上面的讲解主要是针对controller具体的每一个方法做验证,没有做统一验证的处理。说明了一个问题如果每个Controller都这样写肯要搞死人,可能有很多个请求都会用到这个对象,那么都这样处理是不是写了很多废代码,没太搞明白Spring 的官方教程讲的这么浅显,没有做深入探究。小生在此探究一下上层的异常处理器来处理统一的BindException

Spring Boot定义统一上层的异常拦截器

Spring mvc 3.2引入了一个ControllerAdvice注解,这个组件我们可以用来处理Controller层向上抛出的异常,通过一下步骤来实现

1. 创建一个GlobalExceptionHandler类并在类上添加ControllerAdvice注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.iflytek.adsring.rosp.config;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

import static com.iflytek.adsring.rosp.api.AjaxResponse.error;
import static com.iflytek.adsring.rosp.api.message.ServiceCode.FIELD_VALIDATE_ERROR;
import static com.iflytek.adsring.rosp.api.message.ServiceCode.GLOBAL_UNKNOWN_ERROR;

/**
* @author yoqu
* @date 2017年06月30日
* @time 下午2:53
* @email wcjiang2@iflytek.com
*/
@ControllerAdvice
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public AjaxResponse errorHandler(HttpServletRequest request, Exception e) {
logger.error("request page url{}, params:{}, error:{}", request.getRequestURI(), request.getParameterMap(), e);
return error(e.getMessage(), ServiceCode.GLOBAL_EXCEPTION_ERROR.getKey());
}

@ExceptionHandler(BindException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public AjaxResponse validateErrorHandler(BindException e) {
BindingResult bindingResult = e.getBindingResult();
if (bindingResult.hasErrors()) {
List<FieldError> errorList = bindingResult.getFieldErrors();
String errorMsg = errorList.get(0).getField() + " 字段错误,错误原因:" + errorList.get(0).getDefaultMessage();
return error(errorMsg, FIELD_VALIDATE_ERROR.getKey());
} else {
return error(GLOBAL_UNKNOWN_ERROR);
}
}
}
  1. 在方法上写了一个@ExceptionHandler注解,value写拦截的异常,Exception为最大级别异常,如果一个子类异常没有被定义,所有的异常都将接入到该方法中去。
  2. ResponseStatus,如果发生了异常,需要返回一个状态码到前台处理
  3. ResponseBody,返回json串

我们在ValidateErrorHandler中,通过上面的讲解,知道了验证不通过是导致发生了BindException异常,那么我们在这一层进行一个异常处理,通过记录日志和返回实体对象的方式来生成最后的结果。

2. 重写之前的controller层的方法

1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("test1")
public AjaxResponse test(@Valid PersonForm personForm){//【1】
return ok("suc");
}
}

在1处,我们删除了之前传递的第二个参数BindingResult,这样当验证不通过会将异常向上抛出,我们的全局拦截器将会拦截处理。

用到的一些类:
AjaxResponse用于向前台打印消息的实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package com.iflytek.adsring.rosp.api;
import com.alibaba.fastjson.JSON;
import java.io.Serializable;
import java.util.Map;
/**
* @author yoqu
* @date 2017年06月29日
* @time 下午2:29
* @email wcjiang2@iflytek.com
*/
public class AjaxResponse implements Serializable {

/**
* 是否成功
*/
private boolean flag;

/**
* 响应吗
*/
private int code;

/**
* 业务状态码
*/
private String stateCode;

/**
* 数据
*/
private Object data;

/**
* 消息
*/
private String msg;

public AjaxResponse() {

}

public AjaxResponse(boolean flag, String msg) {
this.flag = flag;
this.msg = msg;
}

public AjaxResponse(boolean flag, String msg, Object data) {
this(flag, msg);
this.data = data;
}

public AjaxResponse(boolean flag, String msg, String stateCode, Object data) {
this(flag, msg, data);
this.stateCode = stateCode;
}

public static AjaxResponse ok(Object data) {
return ok("success", data);
}

public static AjaxResponse ok(String msg){
return ok(msg,null);
}

public static AjaxResponse ok(String msg, Object data) {
return new AjaxResponse(true, msg, data);
}

public static AjaxResponse error(String msg, String stateCode, Object data) {
return new AjaxResponse(false, msg, stateCode, data);
}

public static AjaxResponse error(Map.Entry<String,String> code) {
return new AjaxResponse(false,code.getValue(),code.getKey(),null);
}

public static AjaxResponse error(Map.Entry<String,String> code,Object data) {
return new AjaxResponse(false,code.getValue(),code.getKey(),data);
}

public static AjaxResponse error(String msg, String stateCode) {
return error(msg, stateCode, null);
}

public static AjaxResponse error(String msg, Object data) {
return error(msg, "", data);
}

public boolean isFlag() {
return flag;
}

public void setFlag(boolean flag) {
this.flag = flag;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getStateCode() {
return stateCode;
}

public void setStateCode(String stateCode) {
this.stateCode = stateCode;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public String toJSON() {
return JSON.toJSONString(this);
}
}

系统错误码ServiceCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.iflytek.adsring.rosp.api.message;

import java.util.AbstractMap;
import java.util.Map;

/**
* @author yoqu
* @date 2017年06月30日
* @time 下午2:48
* @email wcjiang2@iflytek.com
* 系统错误码对应库
*/
public class ServiceCode {
public static Map.Entry<String, String> FIELD_VALIDATE_ERROR = new AbstractMap.SimpleEntry<String, String>("100000000", "参数错误");
public static Map.Entry<String, String> GLOBAL_EXCEPTION_ERROR = new AbstractMap.SimpleEntry<String, String>("100000001", "系统异常");
public static Map.Entry<String, String> GLOBAL_UNKNOWN_ERROR = new AbstractMap.SimpleEntry<String, String>("100000002", "未知错误");
}