JSR303在项目controller及service层中的应用

一、前言

项目中我们经常要对接口的入参做校验,如果在代码中写判断逻辑,不仅不美观且代码冗余,我们可以使用JSR303标注注解的方式来解决这样的问题。不仅在controller层可以使用JSR303做校验,service层也可以使用JSR303做校验。

二、版本问题

实战中,使用springboot2.7.2版本时需要导入以下依赖

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

当使用springboot2.2.5.RELEASE则不需要另外导入依赖

结论:不同的springboot版本对validation-api jar的集成情况不同,实战中需要注意版本问题。

本文采用springboot2.2.5.RELEASE

三、controller层接口入参校验

1、接口入参为一个实体json

实体代码

@Data
public class User {
private Integer id;
@NotBlank private String name;
@NotNull private Integer age;
}

接口代码

@PostMapping("/save")
public CommonRes save(@Valid @RequestBody User user) {
CommonRes commonRes = new CommonRes();
System.out.println("user = " + user);
commonRes.setCode(200);
commonRes.setData(user);
return commonRes;
}

请求接口

{
"name":"",
"age":""
}

接口响应结果

{
"timestamp": "2022-09-28T05:11:03.192+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.user.age",
"NotNull.age",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"user.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
}
],
"defaultMessage": "不能为null",
"objectName": "user",
"field": "age",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotBlank.user.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"user.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "不能为空",
"objectName": "user",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
},
{
"codes": [
"NotNull.user.age",
"NotNull.age",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"user.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
}
],
"defaultMessage": "不能为null",
"objectName": "user",
"field": "age",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"message": "Validation failed for object='user'. Error count: 3",
"path": "/user/save"
}

结果信息冗余,并不是我们想要的格式

后台日志

2022-09-28 13:11:03.190 WARN 21711 --- [io-10092-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.jsr303demo.util.CommonRes com.example.jsr303demo.controller.UserController.save(com.example.jsr303demo.entity.User) with 3 errors: [Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [不能为null]] [Field error in object 'user' on field 'name': rejected value []; codes [NotBlank.user.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [不能为空]] [Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [不能为null]] ]

可添加一个全局异常处理,专门捕获并处理这个异常

@Slf4j
@RestControllerAdvice
public class MyExceptionControllerAdvice {
/**
* MethodArgumentNotValidException 异常捕获处理
*
* @param e
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public CommonRes methodArgumentNotValidExceptionHandle(MethodArgumentNotValidException e) {
CommonRes commonRes = new CommonRes();
log.error("数据校验异常{},异常类型:{}", e.getMessage(), e.getClass());
Map<String, String> errorMap = new HashMap<>();
e.getBindingResult()
.getFieldErrors()
.forEach(
item -> {
errorMap.put(item.getField(), item.getDefaultMessage());
});
commonRes.setCode(400);
commonRes.setMsg("数据校验异常 MethodArgumentNotValidException");
commonRes.setData(errorMap);
return commonRes;
}
}

再次请求结果为

{
"code": 400,
"msg": "数据校验异常 MethodArgumentNotValidException",
"data": {
"name": "不能为空",
"age": "不能为null"
}
}

2、接口入参是非json实体

去掉@RequestBody注解

@PostMapping("/save")
public CommonRes save(@Valid User user) {
CommonRes commonRes = new CommonRes();
System.out.println("user = " + user);
commonRes.setCode(200);
commonRes.setData(user);
return commonRes;
}

结果为

{
"timestamp": "2022-09-28T05:18:55.149+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.user.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"user.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "不能为空",
"objectName": "user",
"field": "name",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotBlank"
},
{
"codes": [
"NotNull.user.age",
"NotNull.age",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"user.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
}
],
"defaultMessage": "不能为null",
"objectName": "user",
"field": "age",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotNull.user.age",
"NotNull.age",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"user.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
}
],
"defaultMessage": "不能为null",
"objectName": "user",
"field": "age",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"message": "Validation failed for object='user'. Error count: 3",
"path": "/user/save"
}

后台日志

2022-09-28 13:18:55.149 WARN 22953 --- [io-10092-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors
Field error in object 'user' on field 'name': rejected value [null]; codes [NotBlank.user.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [不能为空]
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [不能为null]
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [不能为null]]

 

没有得到我们想要的结果,是因为此时抛出的异常不是MethodArgumentNotValidException,而是BindException

因此我们需要添加BindException的全局异常处理

/**
* 只打了@Valid在参数前,没有@RequestBody 验证错误后返回的异常
*
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
public CommonRes bindExceptionHandle(BindException e) {
CommonRes commonRes = new CommonRes();
log.error("数据校验异常{},异常类型:{}", e.getMessage(), e.getClass());
Map<String, String> errorMap = new HashMap<>();
e.getBindingResult()
.getFieldErrors()
.forEach(
item -> {
errorMap.put(item.getField(), item.getDefaultMessage());
});
commonRes.setCode(400);
commonRes.setMsg("数据校验异常 bindExceptionHandle");
commonRes.setData(errorMap);
return commonRes;
}

再次请求结果为

{
"code": 400,
"msg": "数据校验异常 bindExceptionHandle",
"data": {
"name": "不能为空",
"age": "不能为null"
}
}

3、接口的入参不是一个实体

@GetMapping("/getUserByParam")
public CommonRes getUserByParam(@NotNull Integer id) {
CommonRes commonRes = new CommonRes();
System.out.println("id = " + id);
commonRes.setCode(200);
commonRes.setData(id);
return commonRes;
}

此时请求id为空

localhost:10092/user/getUserByParam?id=

@NotNull并不起作用

在类上加入@Validated注解才会校验生效

@Validated
@RestController
@RequestMapping("/user")
public class UserController {
// ...
}

再次请求结果为

{
"timestamp": "2022-09-28T05:30:11.616+0000",
"status": 500,
"error": "Internal Server Error",
"message": "getUserByParam.id: 不能为null",
"path": "/user/getUserByParam"
}

后台日志抛出异常

2022-09-28 13:30:11.614 ERROR 24560 --- [io-10092-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: getUserByParam.id: 不能为null] with root cause
javax.validation.ConstraintViolationException: getUserByParam.id: 不能为null
...

添加ConstraintViolationException全局异常捕获

/**
* constraintViolationExceptionHandle 验证错误后返回的异常
*
* @param e
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
public CommonRes constraintViolationExceptionHandle(ConstraintViolationException e) {
CommonRes commonRes = new CommonRes();
log.error("数据校验异常{},异常类型:{}", e.getMessage(), e.getClass());
Map<String, String> errorMap = new HashMap<>();
for (ConstraintViolation<?> constraintViolation : e.getConstraintViolations()) {
errorMap.put(
constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage());
}
commonRes.setCode(400);
commonRes.setMsg("数据校验异常 constraintViolationExceptionHandle");
commonRes.setData(errorMap);
return commonRes;
}

再次请求结果为

{
"code": 400,
"msg": "数据校验异常 constraintViolationExceptionHandle",
"data": {
"getUserByParam.id": "不能为null"
}
}

以上基本涵盖了入参为实体json、非json实体、非实体非json的三种异常处理

同理其他校验异常也可添加异常处理捕获,进而统一返回校验错误的接口返回信息

四、service层方法入参校验

service入参也可使用JSR303校验统一返回校验结果json

如代码如下

@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Override
public void save(User user) {
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
Set<ConstraintViolation<Object>> set = validator.validate(user);
for (ConstraintViolation<Object> constraintViolation : set) {
log.error(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage());
}
}
}

user字段为空时

控制台打印结果为

2022-09-28 13:49:09.107 ERROR 27242 --- [io-10092-exec-4] c.e.j.service.impl.UserServiceImpl : name:不能为空
2022-09-28 13:49:09.107 ERROR 27242 --- [io-10092-exec-4] c.e.j.service.impl.UserServiceImpl : age:不能为null

我们不妨将此段代码封装成工具类,在需要进行入参校验的实收调用一下即可

先定义一个自定义异常,用于此段逻辑

public class MyValidationException extends Exception {
public MyValidationException() {
super();
}
public MyValidationException(String message) {
super(message);
}
}

工具类抛出自定义异常

@Slf4j
public class MyValidationUtil {
public static void myServiceValidate(Object o) throws Exception {
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
Set<ConstraintViolation<Object>> set = validator.validate(o);
Map<String, String> errorMap = new HashMap<>();
if (set != null && set.size() > 0) {
for (ConstraintViolation<Object> constraintViolation : set) {
log.error(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage());
errorMap.put(
constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage());
}
ObjectMapper objectMapper = new ObjectMapper();
throw new MyValidationException(objectMapper.writeValueAsString(errorMap));
}
}
}

添加全局异常捕获MyValidationException

@ExceptionHandler(value = MyValidationException.class)
public CommonRes handleServiceValidationException(MyValidationException e) {
CommonRes commonRes = new CommonRes();
log.error("数据校验异常:{}", e.getMessage());
Map errorMap = null;
if (!StringUtils.isEmpty(e.getMessage())) {
ObjectMapper objectMapper = new ObjectMapper();
try {
errorMap = objectMapper.readValue(e.getMessage(), Map.class);
} catch (JsonProcessingException ex) {
ex.printStackTrace();
}
}
commonRes.setCode(400);
commonRes.setMsg("数据校验异常 service");
commonRes.setData(errorMap);
return commonRes;
}

再次请求service方法,结果如下

{
"code": 400,
"msg": "数据校验异常 service",
"data": {
"name": "不能为空",
"age": "不能为null"
}
}
posted @   大列巴同学  阅读(10)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程
点击右上角即可分享
微信分享提示
目 录 X
一、前言
二、版本问题
三、controller层接口入参校验
四、service层方法入参校验