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" } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程