Spring Boot 构建Rest服务实验手册(四)

在本系列的上一篇文章中, 给大家演示在Spring Boot中如何规范 Restful 服务接口返回数据的格式。本文将继续深入,演示如何对输入的数据的有效性进行验证。

用户输入数据验证是服务端系统必须完成的任务,Spring Boot 内置了对 JSR 303 Bean Validation 的支持,配合面向起码的编程(AOP),使数据验证的任务变得非常简单。

加入验证逻辑

首先,我们用 JSR 303 支持的验证注解来实现一个简单的验证规则: title 不能为空。

要实现这个规则,只需要在实体类相应的属性上加上注解即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Todo {

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;

@NotNull
private String title;

private String desc;

}

可以看到,我们在 Todo 类的 title 属性上加了注解 @NotNull 来限制该属性不能为空。注意,在这里空和空串是两个不同的注解

改完以后,运行程序,用 Postman 中访问这个 api ,在 body 中输入以下参数:

1
{"desc":"desc of task 1"}

得到如下的返回,可以看到,@NotNull 已经起作用了。数据没有插入到数据库中,并且返回了错误信息 - defaultMessage

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
{
"timestamp": "2019-07-16T01:27:17.388+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.todo.title",
"NotNull.title",
"NotNull.java.lang.String",
"NotNull"
],
"arguments": [
{
"codes": [
"todo.title",
"title"
],
"arguments": null,
"defaultMessage": "title",
"code": "title"
}
],
"defaultMessage": "不能为null",
"objectName": "todo",
"field": "title",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"message": "Validation failed for object='todo'. Error count: 1",
"path": "/todo"
}

JSR 303 Bean Validation 中提供了很多现成的验证规则,大家可以参考其官方文档。

规范错误返回格式

现在我们虽然可以方便的使用验证规则来校验数据了, 但返回的错误格式是 Spring Boot 中标准的格式,和我们在上一篇文章中定义的返回格式不同,这样会导致客户端开发时需要针对不同的情况采用不同的代码来处理,不利于系统的维护。所以,我们需要把返回数据的格式进行转换。

在 Spring Boot 中对错误处理进行转换非常简单,只需要在 RestController 类中增加处理方法就可以了,具体来说,可以简单的在 TodoApi 增加一个 handleException 方法,然后用 @ExceptionHandler 注解修饰这个方法,指明其用来处理异常情况。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResult handleException(MethodArgumentNotValidException exception) {

ApiResult.ApiResultBuilder builder = ApiResult.builder().succ(false).build().toBuilder();

String errorMsg = exception.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.findFirst()
.orElse(exception.getMessage());

return builder.msg(errorMsg).build();
}

修改后运行程序,用 Postman 进行访问,可以发现信息已经变成了我们统一的格式。客户端只需要判断 succ 就知道调用是否成功,如果不成功则取 msg 的信息。

1
2
3
4
5
6
{
"succ": false,
"code": null,
"msg": "不能为null",
"data": null
}

使用全局错误处理

在上面的方法中,我们通过在 RestController 中添加方法来规范错误处理信息,考虑如果系统中有很多 RestController 时,这样做就太繁琐,也不利于维护,那有没有全局
统一处理的的方法呢?

当然有,利用 Spring 的 AOP - 面向切面编程 功能,就可以轻松的实现,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException exception, HttpHeaders headers, HttpStatus status, WebRequest request) {

ApiResult.ApiResultBuilder builder = ApiResult.builder().succ(false).build().toBuilder();

String errorMsg = exception.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.findFirst()
.orElse(exception.getMessage());

return handleExceptionInternal(exception, builder.msg(errorMsg).build(), headers, HttpStatus.BAD_REQUEST, request);
}
}

代码中,我们定义一个 RestExceptionHandler 类,该类继承于 ResponseEntityExceptionHandler。 ResponseEntityExceptionHandler 是 Spring Boot 中内置的一个类,提供了处理错误、异常的基本方法和机制,我们可以通过覆盖(Override)它的方法,获得针对特定处理过程的异常处理能力,再通过 @ControllerAdvice 这个 AOP 注解,将类插入到 Rest 请求的过程中去。

有了这个类后,我们可以删除刚才在 RestController 中加入的 handleException 了。

修改后再运行程序,用 Postman 进行访问,可以看到返回和上一节一样的结果。

处理业务逻辑错误

以上我们是通过 JSR 303 的注解来进行的数据验证,这对于一些比较简单的数据验证是可行的,但对于比较复杂的验证场景(比如需要查数据库,根据查询结果判断是否正确),JSR303就有点力不从心了,这时就需要在业务对象中进行验证。在我们的案例中,应该在 biz 包下的 TodoBiz 类中进行数据验证。举一个例子,假设 title 属性有一个限制,只不能为 “tom”。那我们在 TodoBiz 的业务方法 addTodo 中加入验证如下:

1
2
3
4
5
6
7
8
9
@Transactional
public void addTodo(Todo todo) {

if (!"tom".equals(todo.getTitle())) {
todoRepository.save(todo);
} else {
throw new BizException("Title can not be tom");
}
}

这里,使用了我们自定义的异常类 BizException 来抛出异常给展现层 - RestController。 BizException 的定义如下:

1
2
3
4
5
6
public class BizException extends RuntimeException {

public BizException(String msg) {
super(msg, null);
}
}

注意,我们继承了 RuntimeException,这样做有两点好处:

  1. 不需要在使用业务类的代码中(在例子中就是 Api 类)被强制使用 try … catch 结构,简化代码。
  2. 可以触发 Spring Boot 的事务管理机制,回滚(rollback)事务。

然后我们再在 RestExceptionHandler 中加入方法类统一处理 BizException。

1
2
3
4
5
6
7
8
@ExceptionHandler({BizException.class})
public ResponseEntity<Object> handleBizException(BizException e, WebRequest request) {

ApiResult.ApiResultBuilder builder = ApiResult.builder().succ(false).build().toBuilder();

return new ResponseEntity<Object>(builder.msg(e.getMessage()).build(), new HttpHeaders(), HttpStatus.OK);

}

经过这样改造后,我们就可以进一步的简化 Api 的代码,去掉 try … catch 结构了。新的代码如下:

TodoApi.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    @PostMapping("/todo")
public ApiResult addTodo(@Valid @RequestBody Todo todo) {

ApiResult.ApiResultBuilder builder = ApiResult.builder().succ(false).build().toBuilder();

// try {
todoBiz.addTodo(todo);

builder.succ(true);
// } catch(BizException e) {
// builder.msg(e.getMessage());
// }

return builder.build();
}

改完代码后,运行程序,用 Postman 访问,使用以下参数:

1
{"title":"tom", "desc":"desc of task 1"}

可以得到如下结果:

1
2
3
4
5
6
{
"succ": false,
"code": null,
"msg": "Title can not be tom",
"data": null
}

下一步

下一篇文章中,将介绍如何使用 Swagger 为 API 自动生成文档和测试页面。

本文标题:Spring Boot 构建Rest服务实验手册(四)

文章作者:晨星

发布时间:2019年07月16日 - 11:07

最后更新:2020年09月16日 - 08:09

原始链接:https://www.mls-tech.info/java/springboot-practice-manul-4/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。