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"
}
}