使用 Preconditions 验证入参
在开发中,我们常常会使用 Preconditions 来验证接口的入参是否符合要求,代码如下
@Component
public class FlowCmdServiceImpl implements FlowCmdService {
@Override
public void withdraw(@Valid FlowWithdrawCmd cmd) {
Preconditions.checkArgument(cmd != null,"请求不能为空");
Preconditions.checkArgument(StringUtils.isNoneBlank(cmd.getFlowReqId()),"reqId不能为空");
Preconditions.checkArgument(StringUtils.isNoneBlank(cmd.getOperator()),"operator不能为空");
//验证通过,处理业务
}
}
如果需要验证的数据很多,会浪费我们大量的精力处理参数的验证,而且会严重污染我们的代码。我们希望把重心放在处理业务逻辑上,一些参数的验证可以从业务代码中剥离处理,统一处理。
如果让我们自己设计一个这样的参数验证功能,该如何实现呢?我们猜测一下:
- 首先我们需要定义一个注解类 @Valid 来标识验证的范围。
- 还需要一些注解来标示验证的规则(如:@NotNull不能为空,@Min 最小数字等)
- 不能做侵入式的,那肯定需要使用动态代理来统一处理。
在spring boot中使用validation验证Controller
spring 已经为我们提供了一套完整的validation,我们先看看它的使用,在说说它的原理和我们猜测的是否一致。
引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
定义要验证的请求类
/**
* 工作流撤回命令
* @author xiyangyang
* @date 2021-09-28
*/
@Data
public class FlowWithdrawCmd extends Command {
/**
* 工作流申请ID
*/
@NotBlank(message = "申请单ID不能为空")
private String flowReqId;
/**
* 操作人ERP
*/
@NotBlank(message = "操作人不能为空")
private String operator;
}
定义一个申请单 Controller类
请注意@Validated 和 @Valid 注解的使用
@Validated
@RestController
public class FlowController {
@Resource
private FlowCmdService flowCmdService;
@Resource
private FlowQueryService flowReqQueryService;
@PostMapping("/flow/withdraw")
public Response<FlowDTO> withdraw(@Valid FlowWithdrawCmd cmd){
flowCmdService.withdraw(cmd);
return Response.success();
}
@RequestMapping("/flow/queryByReqId")
public String queryByReqId(@NotBlank(message = "reqId is not null") String reqId){
return null;
}
}
发送申请单查询请求:http://localhost:8080/flowreq/queryByReqId?reqId= 异常信息如下:
javax.validation.ConstraintViolationException: queryByReqId.reqId: reqId is not null
定义统一异常处理类
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理Validated校验异常
* <p>
* 注: 常见的ConstraintViolationException异常, 也属于ValidationException异常
*
* @param e
* 捕获到的异常
* @return 返回给前端的data
*/
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
public Response handleParameterVerificationException(Exception e) {
log.error(" handleParameterVerificationException has been invoked", e);
Response response = null;
if (e instanceof BindException) {
FieldError fieldError = ((BindException) e).getFieldError();
if (fieldError != null) {
response = Response.failure("-1",fieldError.getDefaultMessage());
}
} else if (e instanceof MethodArgumentNotValidException) {
BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
response = Response.failure("-1",fieldError.getDefaultMessage());
}
} else if (e instanceof ConstraintViolationException) {
response = Response.failure("-1",e.getMessage());
} else {
response = Response.failure("-1","处理参数时异常");
}
return response;
}
}
写MVC的单元测试类
public class FlowControllerTest extends BootBaseTest {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@Before
public void initMokcMvc() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public void testWithdraw() throws Exception {
MockHttpServletRequestBuilder servlet = MockMvcRequestBuilders
.post("/flow/withdraw")
.contentType("application/json;charset=UTF-8")
.content(JSON.toJSONString(new FlowWithdrawCmd()));
servlet.characterEncoding("utf-8");
MvcResult result = mockMvc.perform(servlet).andReturn();
System.out.println(result.getResponse().getContentAsString(Charset.forName("utf-8")));
}
}
运行测试类
{"success":false,"code":"-1","message":"申请单ID不能为空"}
运行的结果和我们期望是一致的,拦截了请求参数的验证信息。
在Service层使用validation
在服务层使用和在controller 层有 什么不一样吗?
定义服务接口
注意需要在方法需要验证的参数前 添加注解 @Valid
public interface FlowProvider {
/**
* 取消申请单(移除申请单)
*/
Response cancel(@Valid @NotNull(message = "req is not null") FlowCancelCmd cmd);
}
接口实现类定义
接口实现类需要和接口定义一致 ,注意@Valid 和 @NotNull 。实现类上需求添加@Validated。使用的@Validated校验参数接口参数和实现类参数要保持一直,不然会报错。
@Validated
@Component
public class FlowProviderImpl implements FlowProvider {
/**
* 取消申请单
*/
@Override
@ResponseHandler
public Response cancel(@Valid @NotNull(message = "req is not null") FlowCancelCmd cmd) {
FlowReq flowReq = flowReqRepo.getByReqId4Update(cmd.getReqId());
flowReq.cancel();
return Response.success();
}
}
添加注册的Bean
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.addProperty(HibernateValidatorConfiguration.FAIL_FAST, "true")
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
methodValidationPostProcessor.setValidator(validator);
return methodValidationPostProcessor;
}
定义ResponseHandler 注解
/**
* RPC response 注解处理
*
* @author yangyanping
* @date 2021-11-04
*/
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseHandler {
}
定义异常处理 拦截器
@Slf4j
@Aspect
@Component
public class ResponseAspect {
@Pointcut("@annotation(com.flow.interfaces.provider.aop.ResponseHandler)")
public void aspect() {
}
@Around("aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("ResultAspect#around");
try {
Object result = joinPoint.proceed();
if (result == null) {
return Response.success();
}
if (!(result instanceof Response)) {
return Response.success(result);
}
return result;
} catch (IllegalArgumentException | ComponentBusinessException e) {
log.error("ResultAspect#around.ComponentBusinessException", e);
return Response.failure("-1", e.getMessage());
} catch (ConstraintViolationException e) {
log.error("ResultAspect#around.Exception", e);
Set<ConstraintViolation<?>> set = e.getConstraintViolations();
for (ConstraintViolation<?> violation : set) {
return Response.failure("-1", violation.getMessage());
}
return Response.failure("-1", "操作异常");
} catch (Exception e) {
log.error("ResultAspect#around.Exception", e);
return Response.failure("-1", "操作异常");
}
}
}
单元测试
public class FlowProviderTest extends BootBaseTest {
@Resource
private FlowProvider flowProvider;
@Test
public void testWithdraw(){
FlowWithdrawReq flowWithdrawReq = new FlowWithdrawReq();
Response response = flowProvider.withdraw(flowWithdrawReq);
System.out.println(JSON.toJSONString(response));
}
}
程序输出:{"code":-1,"message":"操作人不能为空"}
嵌套类的验证实例定义
@Data
public class User {
@NotBlank(message = "name不能为空")
private String name;
@Min(value = 0,message = "age最小值为0")
private int age;
@Valid
@NotNull(message = "地址不能为空")
private List<AddressInfo> addressInfoList;
@Data
public static class AddressInfo{
@NotBlank(message = "city不能为空")
private String city;
@NotBlank(message = "address不能为空")
private String address;
}
}
Validation实现原理
spring-boot-autoconfigure的配置
文件META-INF/spring-factories 里面配置文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.4.3</version>
</dependency>
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
ValidationAutoConfiguration类定义
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({ExecutableValidator.class})
@ConditionalOnResource(
resources = {"classpath:META-INF/services/javax.validation.spi.ValidationProvider"}
)
@Import({PrimaryDefaultValidatorPostProcessor.class})
public class ValidationAutoConfiguration {
public ValidationAutoConfiguration() {
}
@Bean
@Role(2)
@ConditionalOnMissingBean({Validator.class})
public static LocalValidatorFactoryBean defaultValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(excludeFilters.orderedStream());
boolean proxyTargetClass = (Boolean)environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator);
return processor;
}
}
MethodValidationPostProcessor 类的层次结构
BeanPostProcessor
该接口我们也叫后置处理器,作用是在Bean对象在实例化和依赖注入完毕后,在显示调用初始化方法的前后添加我们自己的逻辑。注意是Bean实例化完毕后及依赖注入完成后触发的。接口的源码如下
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
方法 | 描述 |
postProcessBeforeInitialization | 实例化、依赖注入完毕, 在调用显示的初始化之前完成一些定制的初始化任务 |
postProcessAfterInitialization | 实例化、依赖注入、初始化完毕时执行 |
MethodValidationPostProcessor
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
@Nullable
private Validator validator;
public MethodValidationPostProcessor() {
}
public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
this.validatedAnnotationType = validatedAnnotationType;
}
public void setValidator(Validator validator) {
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean)validator).getValidator();
} else if (validator instanceof SpringValidatorAdapter) {
this.validator = (Validator)validator.unwrap(Validator.class);
} else {
this.validator = validator;
}
}
public void setValidatorFactory(ValidatorFactory validatorFactory) {
this.validator = validatorFactory.getValidator();
}
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, this.createMethodValidationAdvice(this.validator));
}
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor();
}
}
MethodValidationInterceptor
public class MethodValidationInterceptor implements MethodInterceptor {
private final Validator validator;
public MethodValidationInterceptor() {
this(Validation.buildDefaultValidatorFactory());
}
public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
this(validatorFactory.getValidator());
}
public MethodValidationInterceptor(Validator validator) {
this.validator = validator;
}
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
if (this.isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
} else {
Class<?>[] groups = this.determineValidationGroups(invocation);
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
Set result;
try {
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
} catch (IllegalArgumentException var8) {
methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), target.getClass()));
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
} else {
Object returnValue = invocation.proceed();
result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
} else {
return returnValue;
}
}
}
}
private boolean isFactoryBeanMetadataMethod(Method method) {
Class<?> clazz = method.getDeclaringClass();
if (clazz.isInterface()) {
return (clazz == FactoryBean.class || clazz == SmartFactoryBean.class) && !method.getName().equals("getObject");
} else {
Class<?> factoryBeanType = null;
if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
factoryBeanType = SmartFactoryBean.class;
} else if (FactoryBean.class.isAssignableFrom(clazz)) {
factoryBeanType = FactoryBean.class;
}
return factoryBeanType != null && !method.getName().equals("getObject") && ClassUtils.hasMethod(factoryBeanType, method);
}
}
protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
Validated validatedAnn = (Validated)AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
if (validatedAnn == null) {
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
validatedAnn = (Validated)AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
}
return validatedAnn != null ? validatedAnn.value() : new Class[0];
}
}
Validated 注解类定义
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
验证类分析
核心的maven
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.7.Final</version>
<scope>compile</scope>
</dependency>
NotBlankValidator实现类
package org.hibernate.validator.internal.constraintvalidators.bv;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraints.NotBlank;
public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
public NotBlankValidator() {
}
public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
if (charSequence == null) {
return false;
} else {
return charSequence.toString().trim().length() > 0;
}
}
}
常用验证注解
注解 | 描述 |
邮箱验证 | |
@Max | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Min | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Negative | 带注释的元素必须是一个严格的负数(0为无效值) |
@NegativeOrZero | 带注释的元素必须是一个严格的负数(包含0) |
@NotBlank | 同StringUtils.isNotBlank |
@NotEmpty | 同StringUtils.isNotEmpty |
@NotNull | 不能是Null |
@Szie | 带注释的元素大小必须介于指定边界(包括)之间 |
参考:
解决@Validated注解无效,嵌套对象属性的@NotBlank无效问题 - 路饭网
SpringBoot使用Validation校验参数_JustryDeng-CSDN博客_validation
SpringBoot validator 完美实现+统一封装错误提示_小单的博客专栏-CSDN博客_springboot validator