pom.xml
spring-boot可引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
也可引入
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.22.Final</version>
</dependency>
注解使用示例
package com.ccjt.eip.web.vo;
import com.alibaba.fastjson.annotation.JSONField;
import com.ccjt.eip.web.annotation.JgCodeConstraint;
import com.ccjt.eip.web.annotation.Table;
import com.ccjt.eip.web.annotation.TableField;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/*
*
* @author
* @date 2021-06-18T14:55:26.438
*/
@Setter
@Getter
@Table(name="project_attractinfo")
public class ProjectAttractinfo implements Serializable{
//
@TableField(name="id",increment = true)
private Integer id;
//
@TableField(name="create_time")
private Date createTime = new Date();
//
@TableField(name="update_time")
private Date updateTime = new Date();
//
@TableField(name="showorder")
private Integer showorder = 1;
//
@TableField(name="isdelete")
private Integer isdelete;
//项目唯一id
@TableField(name="project_id")
@JSONField(name = "projectId")
@NotBlank(message = "projectId不能为空")
@Length(max = 50, message = "projectId超长")
private String projectId;
//项目编号(此编号为对方系统产生的编号,例如:中航资产平台产生的项目编号)
@TableField(name="project_number")
@JSONField(name = "projectNumber")
@NotBlank(message = "projectNumber不能为空")
@Length(max = 50, message = "projectNumber超长")
private String projectNumber;
//项目名称
@TableField(name="project_name")
@JSONField(name = "projectName")
@NotBlank(message = "projectName不能为空")
@Length(max = 200, message = "projectName超长")
private String projectName;
//项目招商链接
@TableField(name="link_url")
@JSONField(name = "linkUrl")
@NotBlank(message = "linkUrl不能为空")
@Pattern(regexp = "(http|https)://([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?$", message = "linkUrl不是个可用的链接")
private String linkUrl;
//项目招商内容
@TableField(name="content")
@NotBlank(message = "content不能为空")
private String content;
//区划
@TableField(name="area_code")
@JSONField(name = "areaCode")
@NotBlank(message = "areaCode不能为空")
@Length(max = 200, message = "areaCode超长")
private String areaCode;
//备注
@TableField(name="beizhu")
@Length(max = 200, message = "beizhu超长")
private String beizhu;
//交易机构编号(详见数字字典3.1),多个机构以逗号拼接(该字段用于后期扩展用,比如要将此招商信息也同步推送至平台上的交易机构自己的网站上,要推送的哪些交易机构就以此字段来识别)
@TableField(name="jgcode")
@JSONField(name = "jgcode")
@Length(max = 200, message = "jgcode超长")
@JgCodeConstraint
private String jgcode;
//交易机构名称,多个机构以逗号拼接,同上
@TableField(name="jgname")
@JSONField(name = "jgname")
@Length(max = 200, message = "jgname超长")
private String jgname;
//招商信息发布机构(或平台)代码。中航资产交易平台在易交易平台中的机构编号(测试环境编号联调时提供,正式环境编号正式上线时提供)
@TableField(name="public_jgcode")
@JSONField(name = "fbJgcode")
@NotBlank(message = "fbJgcode不能为空")
@Length(max = 50, message = "fbJgcode超长")
private String publicJgcode;
//招商信息发布机构(平台)名称
@TableField(name="public_jgname")
@JSONField(name = "fbJgname")
@NotBlank(message = "fbJgname不能为空")
@Length(max = 50, message = "fbJgname超长")
private String publicJgname;
//数据时间戳(当前时间)
@TableField(name="data_timestamp")
@JSONField(name = "datatimestamp")
@NotNull(message = "dataTimestamp不能为空")
private Date dataTimestamp;
//系统标识
@TableField(name = "syscode")
private String sysCode;
@JSONField(name = "biaoDi")
private List<@Valid ProjectAttractinfoSection> biaodis;
}
以上涉及到的默认支持的验证注解:@NotNull @NotBlank @Length @Pattern
@NotEmpty 用在集合类上面 ,用于确保集合不为null且长度不为0
@NotBlank 用在String上面 ,用于确保不为null且不为空字符串
@NotNull 用在基本类型上,例如date
自定义拓展验证注解:@JgCodeConstraint
其他默认注解不再赘述,自行百度。
拓展验证注解的使用方式
1.建立注解
import com.ccjt.eip.web.validator.JgCodeConstraintValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Constraint(validatedBy = {JgCodeConstraintValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JgCodeConstraint {
String message() default "非法的jgcode值";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2.建议验证实现
import com.ccjt.eip.web.annotation.JgCodeConstraint;
import com.ccjt.eip.web.constants.DicConstants;
import com.google.common.base.Splitter;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Iterator;
/**
* jgcode合法性验证
* @author
* @date 2021/6/23 0023 18:20
*/
public class JgCodeConstraintValidator implements ConstraintValidator<JgCodeConstraint, String> {
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
Iterator<String> jgcodes = Splitter.on(",").split(s).iterator();
while (jgcodes.hasNext()){
String tmpCode = jgcodes.next();
if(!DicConstants.tradeOrgCodes.contains(tmpCode)){
//区划不在可用范围内,返回验证失败
return false;
}
}
return true;
}
@Override
public void initialize(JgCodeConstraint constraintAnnotation) {
}
}
完成以上就可以注解到想要验证的字段上。
以上默认注解和拓展注解都只能对单个字段进行校验。
如果涉及到A字段的值影响到B字段的验证,框架支持采用分组触发的模式。
分组校验
1.建立分组验证实现类
import com.ccjt.eip.web.domain.constant.Constant;
import com.ccjt.eip.web.vo.NccqBidnotice;
import com.google.common.collect.Lists;
import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider;
import java.util.List;
import java.util.Objects;
/**
* 成交公告信息动态验证
*/
public class BidnoticeGroupSequenceProvider implements DefaultGroupSequenceProvider<NccqBidnotice> {
@Override
public List<Class<?>> getValidationGroups(NccqBidnotice notice) {
List<Class<?>> defaultGroupSequence = Lists.newArrayList();
defaultGroupSequence.add(NccqBidnotice.class);
if (!Objects.isNull(notice)) {
//交易方式为“网络竞价”时动态验证
if (Objects.equals(notice.getTransactionMode(), Constant.TRANSACTION_MODE_NETWORK_BIDDING)) {
defaultGroupSequence.add(NccqBidnotice.WhenTransactionModeIsNetworkGroup.class);
}
}
return defaultGroupSequence;
}
}
2.建立接口。接口代表一种验证策略。
/**
* 定义交易方式为“网络竞价”分组
*/
public interface WhenTransactionModeIsNetworkGroup {
}
接口类会被标记到具体的字段上,用于表示哪些字段受此验证策略的影响。
以下方代码为例,给bean添加@GroupSequenceProvider(BidnoticeGroupSequenceProvider.class)来启用分组验证。
给字段的原生验证注解设置groups = {WhenTransactionModeIsNetworkGroup.class}。
验证bean时会进入BidnoticeGroupSequenceProvider,满足条件后会触发带有groups = {WhenTransactionModeIsNetworkGroup.class}的全部注解的验证。
以此实现当交易方式为网络竞价时,才会验证竞价规则、竞价开始时间、竞价结束时间的非空性。
@Setter
@Getter
@Table(name = "nccq_bidnotice")
@GroupSequenceProvider(BidnoticeGroupSequenceProvider.class)
public class NccqBidnotice implements Serializable {
//交易方式
@TableField(name = "transaction_mode")
@NotBlank(message = "交易方式不能为空")
@Length(max = 50, message = "长度不能超过50位")
private String transactionMode;
//竞价规则
@TableField(name = "biddingrules")
@NotBlank(groups = {WhenTransactionModeIsNetworkGroup.class}, message = "交易方式为'网络竞价'时,竞价规则不能为空")
@Length(max = 50, message = "长度不能超过50位")
private String biddingrules;
//竞价开始时间
@TableField(name = "offer_begin_date")
@NotNull(groups = {WhenTransactionModeIsNetworkGroup.class}, message = "交易方式为'网络竞价'时,竞价开始时间不能为空")
private Date offerBeginDate;
/**
* 竞价结束时间
*/
@TableField(name = "offer_end_date")
@NotNull(groups = {WhenTransactionModeIsNetworkGroup.class}, message = "交易方式为'网络竞价'时,竞价结束时间不能为空")
private Date offerEndDate;
/**
* 定义交易方式为“网络竞价”分组
*/
public interface WhenTransactionModeIsNetworkGroup {
}
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
使用方法
以上工作完成后,开始进行具体的使用:原则上可以使用@Valid注解给想要验证的bean进行注解,即可触发验证,例如
但实际使用中发现,因框架版本等问题,容易出现@Valid失效的问题。所以才用自定义注解来手动触发。
1.编写注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Validator {
boolean value() default true;
}
2.编写切面
import com.alibaba.fastjson.JSONObject;
import com.ccjt.eip.web.annotation.Validator;
import com.ccjt.eip.web.validator.BeanValidator;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Aspect
@Component
@Slf4j
@Order(1)
public class ValidateAspect {
@Resource
private BeanValidator beanValidator;
@Pointcut("@annotation(com.ccjt.eip.web.annotation.Validator)")
public void doValidate() {
}
@Before(value = "doValidate()")
public void doBefore(JoinPoint jp) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Validator annotation = getAnnotation(jp);
if (needValidate(annotation, request)) {
for (int i = 0; i < jp.getArgs().length; i++) {
beanValidator.validate(jp.getArgs()[i]);
}
// String args = serializeObjs(jp.getArgs());
// NccqBidnotice bidNotice = JSONObject.parseObject(args, NccqBidnotice.class);
// String validResult = validatorService.valid(bidNotice);
// if (StringUtils.isNotBlank(validResult)) {
// CustomExceptionThrower.throwException(CommonCode.INVALID_PARAMS, validResult);
// }
}
}
private Validator getAnnotation(JoinPoint jp) {
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
Validator validatorAnnotation = method.getAnnotation(Validator.class);
return validatorAnnotation;
}
private boolean needValidate(Validator validatorAnnotation, HttpServletRequest request) {
if (validatorAnnotation != null) {
return validatorAnnotation.value();
}
return false;
}
private static final ObjectMapper mapper;
static {
mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
}
private static String serializeObjs(Object[] objs) {
StringBuilder sb = new StringBuilder();
for (Object obj : objs) {
if (obj instanceof MultipartHttpServletRequest) {
continue;
}
sb.append(serializeObj(obj));
}
return sb.toString();
}
private static String serializeObj(Object obj) {
if (obj == null) {
return null;
}
try {
String json = mapper.writeValueAsString(obj);
json = json.replaceAll("______.*?______", "");
return json;
} catch (JsonProcessingException e) {
return null;
}
}
}
以上切面引用了BeanValidator ,该类其实是手动触发框架验证的工具类。只是我们通过注解形式来调用该类的验证方法。
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidationException;
import javax.validation.Validator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 自定义验证方法
* @author
* @date 2021/6/23 0023 11:30
*/
@Service
public class BeanValidator {
public <T> void validate(T object) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<T>> constraintViolations = validator.validate(object);
if (CollectionUtils.isEmpty(constraintViolations)) {
return;
}
throw new ValidationException(convertErrorMsg(constraintViolations));
}
public <T> void validate(T object, Class<?>... group) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<T>> constraintViolations = validator.validate(object, group);
if (CollectionUtils.isEmpty(constraintViolations)) {
return;
}
throw new ValidationException(convertErrorMsg(constraintViolations));
}
public <T> String convertErrorMsg(Set<ConstraintViolation<T>> set) {
Map<String, StringBuilder> errorMap = new HashMap<String, StringBuilder>();
for (ConstraintViolation<T> cv : set) {
String property = cv.getPropertyPath().toString();
if (errorMap.get(property) != null) {
StringBuilder sb = errorMap.get(property);
sb.append("," + cv.getMessage());
} else {
StringBuilder sb = new StringBuilder();
sb.append(cv.getMessage());
errorMap.put(property, sb);
}
}
return JSON.toJSONString(errorMap);
}
}
3.使用,给方法加上注解
@Validator
public ResultDto addProjectAttractinfo(@Valid ProjectAttractinfo projectAttractinfo){...}
至此,只要方法的参数中包含的bean的字段上有类似@NotNull等这些字段验证的注解,就会被触发。如果验证不过就会抛出异常throw new ValidationException(详见BeanValidator 类的验证方法)
为了能全局统一返回异常信息,编写了全局的异常捕获类
import com.ccjt.eip.web.dto.ResultDto;
import com.ccjt.eip.web.enums.RetCode;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ValidationException;
/**
* 全局异常
* @author
* @date 2021/6/23 0023 13:28
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResultDto validationExceptionHandler(ValidationException e) {
return new ResultDto(RetCode.Failed, e.getMessage());
}
}
以上就是整体的使用过程。
注意事项
1.嵌套bean的验证,可以在内部bean的前面也加上@Valid。如果是集合可以这样加,这样当我们验证ProjectAttractinfo时,内部的biaodis只要不为空就会递归进去验证。
public class ProjectAttractinfo implements Serializable{
private List<@Valid ProjectAttractinfoSection> biaodis;
}
2.由于我们最初的验证是通过@Validator来触发的,所以其实,以下方法参数前的@Valid是可以省略的,但为了方便理解,建议还是加上@Valid。
@Validator
public ResultDto addProjectAttractinfo(@Valid ProjectAttractinfo projectAttractinfo){...}
3.实际业务场景如果涉及到A字段的值影响到B字段的值。或者A字段的值需要与某张表的某个字段进行比较等关联性验证时,需要自行编写验证代码。而且此类的验证需要放在@Valid验证结束后再执行。因为@Valid会进行基础单字段的校验,包括非空。然后才能确保A字段一定有值。如果先进行我们自定义的关联校验,又没有判断非空的话,则可能触发NullException。
以下提供一种处理方案:
拓展注解,以支持关联验证
1.拓展Validator注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Validator {
boolean value() default true;
//用于设置拓展验证。在基础的验证后执行。详见 ValidateAspect
String[] moreValidations() default {};
}
2.moreValidations是一个数组。用于设置想要触发的附加验证方法,设置示例如下。此处注意moreValidations的数量只能小于等于被注解的方法的参数的个数。
也就是说第一个参数的附加验证一定是moreValidations[0]对应的方法。第二个参数对应的附加验证一定是moreValidations[1]对应的方法。如果只要附加验证第二个参数,那就将第一个参数设为空字符串"",如上。
@Validator(moreValidations = {"", "com.ccjt.eip.web.backend.validate.morevalidation.ZzkgListValidation.basicInfoValidation"})
public ResultDto saveBasicInfo(@Valid ProjectDto projectDto,@Valid SectionDto sectionDto) {...}
3.编写附加验证。所有方法的异常一定要处理掉,不能抛出。因为最终通过反射调用方法使用invoke方法时,发生任何异常,java都会抛出InvocationTargetException。这会导致我们编写的全局异常处理捕获不到ValidationException。
为了解决此问题,必须规范要求所有附加验证的方法不能throw任何expection。只能返回ResultDto。在切面中如果遇到ResultDto不为success,就会转化为ValidationException抛出去,从而被全局异常处理捕获。具体处理见 4
/**
* 增资扩股挂牌阶段拓展验证
* @author
* @date 2021/6/24 0024 16:21
*/
public class ZzkgListValidation {
public ResultDto basicInfoValidation(Object o){
ProjectDto projectDto = Convert.convert(ProjectDto.class, o);
return new ResultDto(RetCode.SUCCESS, "到此一游" + projectDto.getSharding());
}
}
4.切面拓展ValidateAspect
@Before(value = "doValidate()")
public void doBefore(JoinPoint jp) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Validator annotation = getAnnotation(jp);
if (needValidate(annotation, request)) {
for (int i = 0; i < jp.getArgs().length; i++) {
beanValidator.validate(jp.getArgs()[i]);
}
//判断是否有额外需要进行的校验
final String[] moreValidations = annotation.moreValidations();
if(moreValidations.length > jp.getArgs().length){
//待执行的验证方法的数量 > 参数数量时,说明设置有误(为减少注解参数的长度,不允许给一个参数设置2个及以上的验证方法)
throw new ValidationException("拓展验证方法数量过多");
}
for (int i = 0; i < moreValidations.length; i++) {
if(Strings.isNotBlank(moreValidations[i])){
int lastSplitTagIndex = moreValidations[i].lastIndexOf(".");
if(lastSplitTagIndex > 0){
try {
String className = moreValidations[i].substring(0, lastSplitTagIndex);
String methodName = moreValidations[i].substring(lastSplitTagIndex + 1);
//反射执行
Class<?> clazz = Class.forName(className);
final Method method = ReflectUtil.getMethod(clazz, methodName, Object.class);
try {
ResultDto resultDto = (ResultDto) method.invoke(clazz.newInstance(), jp.getArgs()[i]);
if(!RetCode.SUCCESS.equals(resultDto.getCode())){
//增强校验返回结果不为SUCCESS,就抛出全局的验证异常
throw new ValidationException(JSON.toJSONString(resultDto));
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e){
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}else{
throw new ValidationException("拓展验证方法设置格式有误");
}
}
}
}
}