对于研发人员来说,想要写出鲁棒性高的代码,稳定性强的代码,对于参数的校验是必不可少的。防御式编程就是这样一种思想,对用户输入的参数持怀疑态度,尽可能的考虑使用场景,对输入进行校验,这样可以在某些恶意请求,或者异常访问场景下减少系统宕机的可能
觉得有收获的话可以关注微信公众号:程序员HopeX
🍒线上案例
一个平平无常的夜晚,大家准备下班的时候,突然系统出现大量rpc调用超时的异常告警,看监控面板,某个服务出现大量的fullgc,查询日志和对应的服务调用链,发现存在慢sql,且有大量结果集的返回;从慢sql监控日志里面看到有个sql执行时间最大快要达到200多秒,平均执行时间44多秒;发现是执行的查询sql没有带任何查询参数,直接全表扫描,且查询的是一张大表,返回结果700多万行;于是导致查询的服务短时间内频繁fullgc,并因此形成连锁反应,牵一发而动全身,依赖该该服务的其他服务全都不可用,蝴蝶效应引起核心服务的不可用
fullgc
慢sql监控
很多血淋淋的案例都表明参数校验的重要性,如果有提前做好参数校验,查询的时候没有全表查询,全表结果返回,就不会有这次线上事故了;如果有做服务降级,如果一个服务不可用,可以针对该服务进行降级,保证核心服务可用,也不会导致系统的全面崩溃,服务降级是后话,本次主要分享一些常见的参数校验;避免因为一个马掌钉损失了一匹战马,进而损失一个军队,一场战争,一个国家...
🍓参数校验
参数校验分为Controller层入参和Service层校验。Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303 规范,是标准 JSR-303 的一个变种),javax提供了@Valid(标准JSR-303规范),配合 BindingResult 可以直接提供参数验证结果。
springBoot参数校验是基于Validator来做的,首先需要添加hibernate-validator和validation-api依赖,由于spring-boot-starter-web依赖hibernate-validator,而hibernate-validate又依赖validation-api,所以项目中只需要添加spring-boot-starter-web依赖即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.starter.version}</version>
</dependency>
🍇controller层入参校验
在检验 Controller 的入参是否符合规范时,使用 @Validated 或者 @Valid 在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
- 分组校验
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。
@Valid:作为标准JSR-303规范,还不支持分组的功能。 - 嵌套验证
由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
🍟@Validated分组校验
@PostMapping({"/notPass", "batchRepulse"})
public ResponseMap batchRepulse(HttpServletRequest request,@Validated(BatchRepulseGroup.class)@RequestBody CommentReq req) {
log.info("打回重传请求参数:{}", JSON.toJSONString(req));
Long employeeId = Long.valueOf(request.getHeader("employeeId"));
paganiService.batchRepulse(req.getCommentIds(), req.getCancelReason(), employeeId);
return ResponseMap.successResponse();
}
校验分组
/**
* <h3>批量点评分组
*
* @author <a>stephenXu</a>
**/
public interface BatchCommentGroup {
}
/**
* <h3>批量点评打回分组
*
* @author <a>stephenXu</a>
**/
public interface BatchRepulseGroup {
}
/**
* <h3>单个点评分组
*
* @author <a>stephenXu</a>
**/
public interface SingleCommentGroup {
}
校验参数
@Data
public class CommentReq implements Serializable {
private static final long serialVersionUID = 42L;
/**
* 点评id
*/
@NotNull(message = "commentId must not be null!", groups = SingleCommentGroup.class)
private Integer commentId;
/**
* 批量点评id集合
*/
@NotEmpty(message = "commentIds must not be empty!", groups = {BatchCommentGroup.class, BatchRepulseGroup.class})
private List<Integer> commentIds;
/**
* 批量打回的理由
* @see rpc.api.domain.enums.review.CommentCancelReason
*/
@NotNull(message = "cancelReason must not be null!", groups = BatchRepulseGroup.class)
private Integer cancelReason;
/**
* 点评文本内容
*/
private String content;
/**
* 点评语音
*/
private List<CommentContentReq> contents;
}
🍞@Valid校验
@GetMapping("list")
public SimpleResponse<S3CommentListDetailDTO> list(@Valid S3VoiceCommentListReq req) {
//组装请求参数
S3CommentListReq listReq = S3CommentListReq.builder().build();
BeanUtils.copyProperties(req, listReq);
IRpcResult<S3CommentListDetailDTO> dto = remoteClassUserReviewService
.getS3CommentListInfo(listReq);
if (!dto.checkSuccess() || dto.getData() == null) {
return SimpleResponse.failResponse(dto.getMessage());
}
return SimpleResponse.successResponse(dto.getData());
}
校验参数
@Data
@NoArgsConstructor
public class S3VoiceCommentListReq implements Serializable {
private static final long serialVersionUID = 42L;
private static final Integer DEFAULT_PAGE_NO = 1;
private static final Integer DEFAULT_PAGE_SIZE = 10;
private static final Integer MAX_PAGE_SIZE = 40;
/**
* 班级id
*/
@NotNull(message = "classId must not be null!")
private Integer classId;
/**
* 老师id
*/
@NotNull(message = "teacherId must not be null!")
private Integer teacherId;
/**
* 用户id
*/
private Integer userId;
/**
* 课程key
*/
@NotBlank(message = "courseKey must not be blank!")
private String courseKey;
/**
* 页码
*/
private Integer pageNum;
/**
* 每页大小
*/
private Integer pageSize;
/**
* 点评状态 0 未点评
*/
private Integer commentStatus;
/**
* 是否发布
*/
private Boolean isPublish;
/**
* 课时key
*/
private String lessonKey;
/**
* 开始上传日期
*/
private Date uploadDateStart;
/**
* 结束上传日期
*/
private Date uploadDateEnd;
public void setUploadDateStart(Long uploadDateStart) {
if (uploadDateStart != null) {
this.uploadDateStart = new Date(uploadDateStart);
}
}
public void setUploadDateEnd(Long uploadDateEnd) {
if (uploadDateEnd != null) {
this.uploadDateEnd = new Date(uploadDateEnd);
}
}
public void setPageNum(Integer pageNum) {
if (pageNum == null || pageNum < DEFAULT_PAGE_NO) {
this.pageNum = DEFAULT_PAGE_NO;
return;
}
this.pageNum = pageNum;
}
public void setPageSize(Integer pageSize) {
if (pageSize == null || pageSize < DEFAULT_PAGE_SIZE) {
this.pageSize = DEFAULT_PAGE_SIZE;
return;
}
if (pageSize > MAX_PAGE_SIZE) {
this.pageSize = MAX_PAGE_SIZE;
return;
}
this.pageSize = pageSize;
}
}
🍦service层入参校验
service层校验如果有接口需要在接口里面标注需要校验的参数,且实现类里面需要使用@Validated注解标识,让spring的参数校验拦截器MethodValidationInterceptor生效
- 接口
public interface IUserCommentService {
....
/**
* 打回用户作业列表
*
* @param condition 点评条件
* @return List<CancelCommentDTO>
*/
IRpcResult<PageInfoCommentDTO<RepulseCommentDTO>> getRepulseInfo(CommentCondition condition);
/**
* 删除用户上传的作业
*
* @param homeworkDTO 上传作业相关信息
* @return 删除结果
*/
IRpcResult<Boolean> deleteUserUploadHomework(@Valid HomeworkDTO homeworkDTO);
}
- 实现类上要标明@Validated注解,这样Spring默认的参数校验拦截器才会在Aop里面对参数校验进行处理
@Service
@Slf4j
@Validated
public class UserCommentServiceImpl implements IUserCommentService {
...
@Override
public IRpcResult<Boolean> deleteUserUploadHomework(HomeworkDTO homeworkDTO) {
LessonStepByTypeReq req = new LessonStepByTypeReq();
req.setLessonId(homeworkDTO.getLessonId());
req.setType(homeworkDTO.getStepType().intValue());
LessonStepDto lessonStep = lessonStepOuterService.lessonStepByType(req);
HomeworkMsg uploadHomework = HomeworkMsg.builder()
.userId(homeworkDTO.getUserId())
.classId(homeworkDTO.getClassId())
.lessonId(homeworkDTO.getLessonId())
.teacherId(commentBizService.getTeacherId(homeworkDTO.getUserId().intValue()
, homeworkDTO.getClassId().intValue()))
.stepId(Opt.map(lessonStep, LessonStepDto::getId))
.stepType(homeworkDTO.getStepType())
.courseId(null)
.commentType(null)
.deleteTime(new Date())
.build();
Assert.notNull(kafKaService, "kafKaService must not be null!");
kafKaService.sendHomeworkDeleteMsg(uploadHomework);
return DefaultRpcResult.successWithData(Boolean.TRUE);
}
}
springBoot参数校验原理:AOP是关键
Controller层是入参校验PayloadMethodArgumentResolver,ModelAttributeMethodProcessor参数解析器处理的
Service层是在方法校验拦截器MethodValidationInterceptor切面拦截处理的
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@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) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator);
return processor;
}
}
MethodValidationPostProcessor后置处理器里面最终采用MethodValidationInterceptor切面拦截处理
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
@Nullable
private Validator validator;
/**
* Set the 'validated' annotation type.
* The default validated annotation type is the {@link Validated} annotation.
* <p>This setter property exists so that developers can provide their own
* (non-Spring-specific) annotation type to indicate that a class is supposed
* to be validated in the sense of applying method validation.
* @param validatedAnnotationType the desired annotation type
*/
public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
this.validatedAnnotationType = validatedAnnotationType;
}
/**
* Set the JSR-303 Validator to delegate to for validating methods.
* <p>Default is the default ValidatorFactory's default Validator.
*/
public void setValidator(Validator validator) {
// Unwrap to the native Validator with forExecutables support
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
}
else if (validator instanceof SpringValidatorAdapter) {
this.validator = validator.unwrap(Validator.class);
}
else {
this.validator = validator;
}
}
/**
* Set the JSR-303 ValidatorFactory to delegate to for validating methods,
* using its default Validator.
* <p>Default is the default ValidatorFactory's default Validator.
* @see javax.validation.ValidatorFactory#getValidator()
*/
public void setValidatorFactory(ValidatorFactory validatorFactory) {
this.validator = validatorFactory.getValidator();
}
@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
/**
* Create AOP advice for method validation purposes, to be applied
* with a pointcut for the specified 'validated' annotation.
* @param validator the JSR-303 Validator to delegate to
* @return the interceptor to use (typically, but not necessarily,
* a {@link MethodValidationInterceptor} or subclass thereof)
* @since 4.2
*/
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
🍭MethodValidationInterceptor
public class MethodValidationInterceptor implements MethodInterceptor {
private final Validator validator;
/**
* Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
*/
public MethodValidationInterceptor() {
this(Validation.buildDefaultValidatorFactory());
}
/**
* Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
* @param validatorFactory the JSR-303 ValidatorFactory to use
*/
public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
this(validatorFactory.getValidator());
}
/**
* Create a new MethodValidationInterceptor using the given JSR-303 Validator.
* @param validator the JSR-303 Validator to use
*/
public MethodValidationInterceptor(Validator validator) {
this.validator = validator;
}
@Override
@SuppressWarnings("unchecked")
public Object invoke(MethodInvocation invocation) throws Throwable {
// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
Class<?>[] groups = determineValidationGroups(invocation);
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
try {
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
// Let's try to find the bridged method on the implementation class...
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
Object returnValue = invocation.proceed();
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
🍰总结
参数校验非常重要,但参数校验可以不用if-else这种冗长的代码来做,可以交给sprign来处理,使用AOP切面编程,将参数校验和业务逻辑隔离,更优雅,更高效!
觉得有收获的话可以关注微信公众号:程序员HopeX
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。