-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathvalidator.md
More file actions
300 lines (228 loc) · 11.1 KB
/
validator.md
File metadata and controls
300 lines (228 loc) · 11.1 KB
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
---
category:
- Java企业级开发
tag:
- Spring Boot
title: SpringBoot中处理校验逻辑的两种方式:Hibernate Validator+全局异常处理
shortTitle: SpringBoot中如何处理校验逻辑
---
最近正在开发一个知识库学习网站编程喵🐱,需要对请求参数进行校验,比如说非空啊、长度限制啊等等,可选的解决方案有两种:
- 一种是用 Hibernate Validator 来处理
- 一种是用全局异常来处理
两种方式,我们一一来实践体验一下。
### 一、Hibernate Validator
Spring Boot 已经内置了 Hibernate Validator 校验框架,这个可以通过 Spring Boot 官网查看和确认。
第一步,进入 Spring Boot 官网,点击 learn 这个面板,点击参考文档。

第二步,在参考文档页点击「依赖的版本」。

第三步,在依赖版本页就可以查看到所有的依赖了,包括版本号。

PS:如果发现没有起效,可能是依赖版本冲突了,手动把 Hibernate Validator 依赖添加到 pom.xml 文件就可以了。
```
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
```
通过 Hibernate Validator 校验框架,我们可以直接在请求参数的字段上加入注解来完成校验。
**具体该怎么做呢**?
第一步,在需要验证的字段上加上 Hibernate Validator 提供的校验注解。
比如说我现在有一个用户名和密码登录的请求参数 UsersLoginParam 类:
```java
@Data
@ApiModel(value="用户登录", description="用户表")
public class UsersLoginParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "登录名")
@NotBlank(message="登录名不能为空")
private String userLogin;
@ApiModelProperty(value = "密码")
@NotBlank(message="密码不能为空")
private String userPass;
}
```
就可以通过 `@NotBlank` 注解来对用户名和密码进行判空校验。除了 `@NotBlank` 注解,Hibernate Validator 还提供了以下常用注解:
- `@NotNull`:被注解的字段不能为 null;
- `@NotEmpty`:被注解的字段不能为空;
- `@Min`:被注解的字段必须大于等于其value值;
- `@Max`:被注解的字段必须小于等于其value值;
- `@Size`:被注解的字段必须在其min和max值之间;
- `@Pattern`:被注解的字段必须符合所定义的正则表达式;
- `@Email`:被注解的字段必须符合邮箱格式。
第二步,在对应的请求接口(`UsersController.login()`)中添加 `@Validated` 注解,并注入一个 `BindingResult` 参数。
```java
@Controller
@Api(tags="用户")
@RequestMapping("/users")
public class UsersController {
@Autowired
private IUsersService usersService;
@ApiOperation(value = "登录以后返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
String token = usersService.login(users.getUserLogin(), users.getUserPass());
if (token == null) {
return ResultObject.validateFailed("用户名或密码错误");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ResultObject.success(tokenMap);
}
}
```
第三步,为控制层(UsersController)创建一个切面,将通知注入到 BindingResult 对象中,然后再判断是否有校验错误,有错误的话返回校验提示信息,否则放行。
```java
@Aspect
@Component
@Order(2)
public class BindingResultAspect {
@Pointcut("execution(public * com.codingmore.controller.*.*(..))")
public void BindingResult() {
}
@Around("BindingResult()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof BindingResult) {
BindingResult result = (BindingResult) arg;
if (result.hasErrors()) {
FieldError fieldError = result.getFieldError();
if(fieldError!=null){
return ResultObject.validateFailed(fieldError.getDefaultMessage());
}else{
return ResultObject.validateFailed();
}
}
}
}
return joinPoint.proceed();
}
}
```
这里涉及到了 Spring AOP 的知识,我在前面的文章里讲解过了,戳这个链接可以直达:[Spring AOP 扫盲](https://javabetter.cn/springboot/aop-log.html)
第四步,访问登录接口,用户名和密码都不传入的情况下,就会返回“用户名不能为空”的提示信息。

通过 debug 的形式,体验一下整个工作流程。

可以看得出,Hibernate Validator 带来的优势有这些:
- 验证逻辑与业务逻辑进行了分离,降低了程序耦合度;
- 统一且规范的验证方式,无需再次编写重复的验证代码。
不过,也带来一些弊端,比如说:
- 需要在请求接口的方法中注入 BindingResult 对象,而这个对应在方法体中并没有用到
- 只能校验一些非常简单的逻辑,涉及到数据查询就无能为力了。
### 二、全局异常处理
使用全局异常处理的优点就是比较灵活,可以处理比较复杂的逻辑校验,在校验失败的时候直接抛出异常,然后进行捕获处理就可以了。
第一步,新建一个自定义异常类 ApiException。
```java
public class ApiException extends RuntimeException {
private IErrorCode errorCode;
public ApiException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ApiException(String message) {
super(message);
}
public ApiException(Throwable cause) {
super(cause);
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
public IErrorCode getErrorCode() {
return errorCode;
}
}
```
第二步,新建一个断言处理类 Asserts,简化抛出 ApiException 的步骤。
```java
public class Asserts {
public static void fail(String message) {
throw new ApiException(message);
}
public static void fail(IErrorCode errorCode) {
throw new ApiException(errorCode);
}
}
```
第三步,新建一全局异常处理类 GlobalExceptionHandler,对异常信息进行解析,并封装到统一的返回对象 ResultObject 中。
```java
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public ResultObject handle(ApiException e) {
if (e.getErrorCode() != null) {
return ResultObject.failed(e.getErrorCode());
}
return ResultObject.failed(e.getMessage());
}
}
```
全局异常处理类用到了两个注解,`@ControllerAdvice` 和 `@ExceptionHandler`。
`@ControllerAdvice` 是一个特殊的 `@Component`(可以通过源码看得到),用于标识一个类,这个类中被以下三种注解标识的方法:`@ExceptionHandler`,`@InitBinder`,`@ModelAttribute`,将作用于所有`@Controller` 类的接口上。
```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
}
```
`@ExceptionHandler` 注解的作用就是标识统一异常处理,它可以指定要统一处理的异常类型,比如说我们自定义的 ApiException。
第四步,在需要校验的地方通过 Asserts 类抛出异常 ApiException。还拿用户登录这个接口来说明吧。
```java
@Controller
@Api(tags="用户")
@RequestMapping("/users")
public class UsersController {
@ApiOperation(value = "登录以后返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
String token = usersService.login(users.getUserLogin(), users.getUserPass());
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ResultObject.success(tokenMap);
}
}
```
该接口需要查询数据库验证密码是否正确,如果密码不正确就抛出校验信息“密码不正确”。
```java
@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements IUsersService {
public String login(String username, String password) {
String token = null;
//密码需要客户端加密后传递
UserDetails userDetails = loadUserByUsername(username);
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
Asserts.fail("密码不正确");
}
// 其他代码省略
return token;
}
}
```
第五步,通过 ApiPost 来测试一下接口,故意把密码输错。

也可以通过 debug 的形式,体验一下整个工作流程。

### 三、总结
实际开发中把两者结合在一起用,就可以弥补彼此的短板了,简单校验用 Hibernate Validator,复杂一点的逻辑校验,比如说需要数据库查询用全局异常处理来实现。
为了把 Spring Boot 逻辑校验这块单独拉出来做一个 demo 的例子,我新建了一个 codingmore-validator 的项目,放在 codingmore-learning 项目下面了,想要参考的,直接戳下面的两个链接。
完整的编程喵整个项目的源码戳第一个,只想看逻辑校验的戳第二个。
>- 编程喵🐱源码地址:[https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more)
>- codingmore-validator: [https://github.com/itwanger/codingmore-learning/tree/main/codingmore-validator](https://github.com/itwanger/codingmore-learning/tree/main/codingmore-validator)

每个类,每个方法基本上都加了注释,可以很容易就看得懂。
