通常我们在使用spring框架编写接口时,对于部分接口的参数我们要进行判空或者格式校验来避免程序出现异常。那是我们一般都是使用if-else逐个对参数进行校验。这种方法按逻辑来说也是没有问题的,同样也能实现预期效果。但是,这样的代码从可读性以及美观程序来看,是非常糟糕的。那么,我们就可以使用@valid注解来帮助我们优雅的校验参数。
注意:
实体类中添加 @Valid 相关验证注解,并在注解中添加出错时的响应消息。
import javax.validation.Valid; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @Data public class User { @NotBlank(message = "姓名不能为空") private String username; @NotBlank(message = "密码不能为空") @Length(min = 6, max = 16, message = "密码长度为6-16位") private String password; @Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手机号格式不正确") private String phone; // 嵌套必须加 @Valid,否则嵌套中的验证不生效 @Valid @NotNull(message = "userinfo不能为空") private UserInfo userInfo; }
如果是嵌套的实体对象,并且也要校验该对象,则需要在最外层属性上添加 @Valid 注解
此处只列出Validator提供的大部分验证约束注解,请参考hibernate validator官方文档了解其他验证约束注解和进行自定义的验证约束注解定义。
@Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Validated和@Valid的区别和使用,包括嵌套检验可以参考: https://blog.csdn.net/qq_27680317/article/details/79970590 https://blog.csdn.net/qq_45151158/article/details/112349233?spm=1001.2014.3001.5501
对于GET请求的参数可以使用@validated注解配合上面相应的注解进行校验或者按照原先if-else方式进行效验.
而对于POST请求,大部分是以表单数据即以实体对象为参数,可以使用@Valid注解方式进行效验(可以简单概括一下,如果接口使用实体类接收参数,那么要用@Valid注解该对象,并且在对象的各属性上添加上方表格里的注解;如果接口直接使用某个字段来接收参数,那么在该字段前添加表格里的注解即可,可参考下文代码)。如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。
第一种(适用于生产):在Controller方法参数前加@Valid注解——校验不通过时直接抛异常,get请求直接在平面参数前添加相应的校验规则注解,使用这种的话一般结合统一异常处理进行处理,后面会主要介绍这种方式,可以直接看六部分。
第二种:在Controller方法参数前加@Valid注解,参数后面定义一个BindingResult类型参数——执行时会将校验结果放进bindingResult里面,用户自行判断并处理。
/** * 将校验结果放进BindingResult里面,用户自行判断并处理 * * @param userInfo * @param bindingResult * @return */ @PostMapping("/testBindingResult") public String testBindingResult(@RequestBody @Valid UserInfo userInfo, BindingResult bindingResult) { // 参数校验 if (bindingResult.hasErrors()) { String messages = bindingResult.getAllErrors() .stream() .map(ObjectError::getDefaultMessage) .reduce((m1, m2) -> m1 + ";" + m2) .orElse("参数输入有误!"); //这里可以抛出自定义异常,或者进行其他操作 throw new IllegalArgumentException(messages); } return "操作成功!"; }
这里我们是直接抛出了异常,如果没有进行全局异常处理的话,接口将会返回如下信息:
第三种:用户手动调用对应API执行校验——Validation.buildDefault ValidatorFactory().getValidator().validate(xxx)
这种方法适用于校验任意一个有valid注解的实体类,并不仅仅是只能校验接口中的参数;
这里我提取出一个工具类,如下
import org.springframework.util.CollectionUtils; import javax.validation.ConstraintViolation; import javax.validation.Valid; import javax.validation.Validation; import java.util.Set; /** * 手动调用api方法校验对象 */ public class MyValidationUtils { public static void validate(@Valid Object value) { Set> validateSet = Validation.buildDefaultValidatorFactory() .getValidator() .validate(value); if (!CollectionUtils.isEmpty(validateSet)) { String messages = validateSet.stream() .map(ConstraintViolation::getMessage) // 归约实现字符串合并 .reduce((m1, m2) -> m1 + ";" + m2) .orElse("参数输入有误!"); throw new IllegalArgumentException(messages); } } }
1、安装依赖:
org.springframework.boot spring-boot-starter-validation
继承RuntimeException,要知道,spring 对于 RuntimeException 异常才会进行事务回滚,所以要继承RuntimeException。
@Data @EqualsAndHashCode(callSuper = true) public class MyException extends RuntimeException { private Integer code; public MyException(ResultEnum resultEnum) { super(resultEnum.getMsg()); this.code = resultEnum.getCode(); } public MyException(Integer code, String msg) { super(msg); this.code = code; } }
3、定义三个异常拦截器
@RestControllerAdvice是帮助我们把信息转成json格式返回
@ResponseBody是将方法中的字符串转成json格式同一返回,一般该方法返回值为Object
三个异常拦截器都使用order控制顺序,小的排在前面。 注意:过滤器中的异常无法被拦截
ValidationExceptionHandle.java,验异常统一拦截返回,定义在最前面
@RestControllerAdvice @Order(80) @Slf4j public class ValidationExceptionHandle extends ResponseEntityExceptionHandler { @Override protected ResponseEntity
OtherExceptionHandle.java,其他异常拦截,用于项目中其他异常的拦截返回。
@ControllerAdvice @Order(90) public class OtherExceptionHandle { private Logger logger = LoggerFactory.getLogger(getClass()); /** * 无 * @param e * @return */ @ExceptionHandler(ValidationException.class) @ResponseBody public Object handleValidationException(ValidationException e) { logger.error(e.getMessage(), e); return e; } /** * 违反约束异常处理 * @param e * @return */ @ExceptionHandler(ConstraintViolationException.class) @ResponseBody public Object handConstraintViolationException(ConstraintViolationException e) { logger.error(e.getMessage(), e); return e; } }
FinalExceptionHandle.java,最终异常拦截,最后一道防线。
@RestControllerAdvice @Order(100) public class FinalExceptionHandle{ private final Logger logger = LoggerFactory.getLogger(getClass()); @ExceptionHandler(value = Exception.class) @ResponseBody public Object handle(Exception e) { logger.error(e.getMessage(), e); Mapmap = new HashMap<>(); if (e instanceof MyException) { MyException myException = (MyException) e; map.put("code",500); map.put("msg",myException.getMessage()); return map; } else { e.printStackTrace(); map.put("code",500); map.put("msg","出错啦"); return map; } } }
在基于Spring框架的项目中,可以通过在ApplicationContext-MVC.xml(即SpringMVC配置)文件中配置 ExceptionResolver 的bean ,来配置 全局捕获异常处理 类,然后自定义异常处理类处理。注意如果是spring配置文件中定义过的ExceptionResolver 类,不需要添加@Component。如果是SpringBoot 则需要。这是因为springboot没有自定义配置全局异常捕获类,所以需要添加@Component,来标识该类为Bean。
异常处理可以分为三种:
第一种是进入@Controller标识的方法前 产生的异常,例如URL地址错误。这种异常处理需要 异常处理类通过实现 ErrorController 来处理。 第二种是进入Controller时,但还未进行逻辑处理时 产生的异常,例如传入参数数据类型错误。这种异常处理需要用@ControllerAdvice标识并处理,建议继承 ResponseEntityExceptionHandler 来处理,该父类包括了很多已经被@ExceptionHandler 注解标识的方法,包括一些参数转换,请求方法不支持等类型等等。 第三种时进入Controller,进行逻辑处理时产生的异常,例如NullPointerException异常等。这种异常处理也可以用@ControllerAdvice来标识并进行处理,也建议继承ResponseEntityExceptionHandler 处理, 这里我们可以用@ExceptionHandler 自定义捕获的异常并处理。 以上三种情况都是restful的情况,结果会返回一个Json。
BindException:作用于 @Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效。
/** * BindException异常处理 *BindException: 作用于@Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效
* * @param e BindException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) public Result> bindExceptionHandler(BindException e) { String msg = e.getBindingResult().getFieldErrors() .stream() .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage())) .reduce((x, y) -> String.format("%s; %s", x, y)) .orElse("参数输入有误"); log.error("BindException异常,参数校验异常:{}", msg); return Result.verifyError(msg); }
MethodArgumentNotValidException:作用于 @Validated @Valid 注解,前端提交的方式为json格式有效,出现异常时会被该异常类处理。
/** * MethodArgumentNotValidException-Spring封装的参数验证异常处理 *MethodArgumentNotValidException:作用于 @Validated @Valid 注解,接收参数加上@RequestBody注解(json格式)才会有这种异常。
* * @param e MethodArgumentNotValidException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors() .stream() .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage())) .reduce((x, y) -> String.format("%s; %s", x, y)) .orElse("参数输入有误"); log.error("MethodArgumentNotValidException异常,参数校验异常:{}", msg); return Result.verifyError(msg); }
ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。
/** * ConstraintViolationException-jsr规范中的验证异常,嵌套检验问题 *ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。
*注:Controller类上必须添加@Validated注解,否则接口单个参数校验无效
* * @param e ConstraintViolationException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = ConstraintViolationException.class) public Result> constraintViolationExceptionHandler(ConstraintViolationException e) { String msg = e.getConstraintViolations() .stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")); log.error("ConstraintViolationException,参数校验异常:{}", msg); return Result.verifyError(msg); }
注:Controller类上必须添加@Validated注解,否则接口参数校验无效
4、统一异常处理完整代码
Spring validation入参验证框架,一般在Controller类加上@Validated注解(可检验集合参数),接口方法对应的dto加上@Valid注解,然后直接对以上三个异常进行全局捕获处理即可。
ValidationExceptionHandle.java完整代码:
package com.tangsm.spring.boot.validation.handler; import com.tangsm.spring.boot.validation.domain.vo.Result; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.util.stream.Collectors; /** * 参数校验通用异常处理 * * @author tangsm */ @Order(80) @RestControllerAdvice public class ValidationExceptionHandle { private static final Logger log = LoggerFactory.getLogger(ValidationExceptionHandle.class); /** * BindException异常处理 *BindException: 作用于@Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效
* * @param e BindException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) public Result> bindExceptionHandler(BindException e) { String msg = e.getBindingResult().getFieldErrors() .stream() .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage())) .reduce((x, y) -> String.format("%s; %s", x, y)) .orElse("参数输入有误"); log.error("BindException异常,参数校验异常:{}", msg); return Result.verifyError(msg); } /** * MethodArgumentNotValidException-Spring封装的参数验证异常处理 *MethodArgumentNotValidException:作用于 @Validated @Valid 注解,接收参数加上@RequestBody注解(json格式)才会有这种异常。
* * @param e MethodArgumentNotValidException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors() .stream() .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage())) .reduce((x, y) -> String.format("%s; %s", x, y)) .orElse("参数输入有误"); log.error("MethodArgumentNotValidException异常,参数校验异常:{}", msg); return Result.verifyError(msg); } /** * ConstraintViolationException-jsr规范中的验证异常,嵌套检验问题 *ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。
*注:Controller类上必须添加@Validated注解,否则接口单个参数校验无效
* * @param e ConstraintViolationException异常信息 * @return 响应数据 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = ConstraintViolationException.class) public Result> constraintViolationExceptionHandler(ConstraintViolationException e) { String msg = e.getConstraintViolations() .stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")); log.error("ConstraintViolationException,参数校验异常:{}", msg); return Result.verifyError(msg); } }
参考:
springboot全局的异常拦截处理_springboot 全局异常拦截_L-960的博客-CSDN博客
原创:全局异常捕获BindException、MethodArgumentNotValidException和ConstraintViolationException @Validated@Valid_bindexception没有getbindingresult方法_HD243608836的博客-CSDN博客
高效使用hibernate-validator校验框架 - hjzqyx - 博客园 (cnblogs.com)
SpringBoot 参数校验的方法 - 木白的菜园 - 博客园 (cnblogs.com)