服务端字段校验@Valid @NotNull @Length...
  Xq2GMhzBO7Vw 2023年11月22日 34 0

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("拓展验证方法设置格式有误");
                    }
                }
            }
        }
    }

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月22日 0

暂无评论

推荐阅读
  2Vtxr3XfwhHq   2024年05月17日   53   0   0 Java
  Tnh5bgG19sRf   2024年05月20日   107   0   0 Java
  8s1LUHPryisj   2024年05月17日   46   0   0 Java
  aRSRdgycpgWt   2024年05月17日   47   0   0 Java
Xq2GMhzBO7Vw