Spring 中 Hibernate Validation 的使用( AND OR )

4 篇文章 0 订阅
2 篇文章 0 订阅

Hibernate Validation 的使用

前言

工作上有个需求,与地方进行数据对接,对其上传的JSON数据进行验证。目前使用的方法是使用一个工具类,对各个字段的值进行正则校验。这种方式需要对每个字段都调用相应的工具类方法,而实际对接的数据集有几十种类型,每个数据集又都有几十个字段,所以每开发一个数据集接口,就要写很多代码,每个字段都要调用一次或多次工具类方法,代码行数太多不说,也很容易看眼花,造成验证流程错误。

想法

之前考虑过把这些验证都弄成可配置的项,实现在后台页面上直接配置相关字段的验证,这样就不需要开发新代码,也很省事。但是考虑了一段时间还是放弃了,主要在于每个字段的验证都存在着好几种验证条件,AND、OR、With Some Condition 这些可能性导致了无法通过一个简单的配置来完成字段值的验证。

做不到在页面上配置,只能想着简化代码了。现在Spring 框架的后台验证的方式是通过 Hibernate Validator 的方式来进行验证。抱着注解再怎么写也比每个字段写验证代码强的想法,我把 Hibernate Validator 6.0 的 Reference 看了一遍,找到了自己目前可以使用的一些功能方法。

实践

因为项目上使用的是 Springboot 框架,配置验证相关的类非常简单,pom.xml 中的 spring-boot-starter-web dependency 中,已经自动引入了 Hibernate-Validator 相关 jar 包。只要进行简单的代码编写工作就行。

  1. Controller
    在 ValidController中,只需要对方法中传入的参数对象添加 @Validated 注解,紧跟着参数添加 BindingResult 参数就可以对验证对象中的字段进行验证,并返回验证错误的信息到 BindingResult 中。

ValidController

@RestController
@RequestMapping("/valid")
public class ValidController {
    @PostMapping("/market")
    public ResultBean validMarketData(@Validated MarketData marketData, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResultBean.error(bindingResult.getAllErrors());
        }
        return ResultBean.success();
    }
}

定义统一的返回信息
ResultBean

@Getter
@Setter
public class ResultBean {
    private String resutlCode;
    private String resultMsg;
    private Object resutlData;

    private ResultBean() {
        this.resutlCode = "2000";
        this.resultMsg = "success";
    }
    private ResultBean(String resultCode,String resultMsg) {
        this.resutlCode = resultCode;
        this.resultMsg = resultMsg;
    }
    private ResultBean(String resultCode, String resultMsg, Object resultData) {
        this.resutlCode = resultCode;
        this.resultMsg = resultMsg;
        this.resutlData = resultData;
    }
    public static ResultBean success(){
        return new ResultBean();
    }
    public static ResultBean success(Object object){
        ResultBean result = new ResultBean();
        result.setResutlData(object);
        return result;
    }
    public static ResultBean error(String resultCode,String resultMsg,Object resultData){
        ResultBean result = new ResultBean(resultCode, resultMsg, resultData);
        return result;
    }
    public static ResultBean error(Object resultData){
        ResultBean result = new ResultBean("99999", "未知错误", resultData);
        return result;
    }
    public static ResultBean error(String resultCode,String resultMsg){
        ResultBean result = new ResultBean(resultCode, resultMsg);
        return result;
    }
}

需要验证的对象类
MarketData

@Getter
@Setter
@ToString
public class MarketData {

    @NotNull
    @LegalEntity
    private String representId;

    @NotBlank
    public String marketId;

    @NotBlank
    public String marketName;

    @NotNull
    @Digits(integer = 8,fraction = 2)
    private BigDecimal inPrice;

    @NotNull
    @Digits(integer = 10,fraction = 2)
    private BigDecimal inAmount;

    @NotBlank
    private String productCode;

    @NotNull
    @UpperReaches
    private Integer upperReaches;

    @Length(max = 15)
    private String providerCode;

    @Length(max = 20)
    private String providerName;

    @AssertTrue(message = "上游建立台账则必填供应商信息")
    private boolean isValid() {
        if (upperReaches != null
               && UpperReachesEnum.YES.getValue().equals(upperReaches) && StringUtils.isBlank(providerCode)) {
            return false;
        }

        return true;
    }
    public MarketData() {
    }
    public MarketData(String representId, String marketId, String marketName, BigDecimal inPrice, BigDecimal inAmount, String productCode, Integer upperReaches, String providerCode, String providerName) {
        this.representId = representId;
        this.marketId = marketId;
        this.marketName = marketName;
        this.inPrice = inPrice;
        this.inAmount = inAmount;
        this.productCode = productCode;
        this.upperReaches = upperReaches;
        this.providerCode = providerCode;
        this.providerName = providerName;
    }
}

Custom Annotation

其中@LegalEntity 以及 @UpperReaches 是自定义注解。@AssertTrue 注解的 isValid() 方法是自定义验证的方法,多个字段之间的验证关系可以在这里面进行,这是一种“不那么优雅”的方法,不过当前工作中只有极少多字段关系验证,所以在这里使用。

@LegalEntity
@ConstraintComposition(CompositionType.OR)
@SocialCreditCode
@BusinessLicenseNumber
@IdCardNumber
@PassportNumber
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE,TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface LegalEntity {

    String message() default "请输入正确的统一社会信用代码/工商注册登记号/身份证号码/护照号";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

自定义注解中,有一些注解及属性是作为验证注解所必须要有的,其中的 @Constraint(validatedBy={xxx.class}) 指定该注解验证实现的验证类。 message()、groups()、payload() 是必要的属性。

LegalEntity 注解类中,使用了多个其他自定义的注解,包括 @SocialCreditCode、@BusinessLicenseNumber、@IdCardNumber、@PassportNumber,由于此处法人主体(LegalEntity)编码存在多种可能,这些自定义注解之间是 “或(OR)”的关系,因此使用了 @ConstraintComposition(CompositionType.OR) 注解来指定各个自定义注解之间“OR”的关系。

使用了组合约束注解,默认情况下会返回组合中各个约束的错误信息,如果希望该注解仅返回一条验证失败信息,可以使用 @ReportAsSingleViolation 注解,会在验证失败后返回 message() 的默认信息。

组合约束中各个注解其实也是 @Pattern 注解的低级实现。只需要存在约束注解的几个必要属性,再添加 @Pattern 注解,即可实现各种自定义正则的约束注解。

@SocialCreditCode
@Pattern(regexp = "[^_IOZSVa-z\\W]{2}\\d{6}[^_IOZSVa-z\\W]{10}")
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE,TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface SocialCreditCode {

    String message() default "请输入正确的统一社会信用代码";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

@BusinessLicenseNumber

@Pattern(regexp = "^[a-zA-Z0-9]{10,20}$")
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE,TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface BusinessLicenseNumber {

    String message() default "请输入正确的营业执照号";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

@IdCardNumber

@Pattern(regexp = "^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}(\\d|x|X)$")
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE,TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface IdCardNumber {

    String message() default "请输入正确的身份证号码";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}
@UpperReaches

@UpperReaches 注解不是利用正则实现的,它实现的是一个是否选项,用1、2代码替代。这样的注解需要有自定义的验证类。

@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE,TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {UpperReachesValidator.class})
public @interface UpperReaches {

    String message() default "请输入正确的是否建立电子台账代码";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

注解对应的验证类是一个实现了 ConstraintValidator<A extends Annotation, T> 接口的类,A 即注解类型,T 即注解可以修饰的属性类型。

public class UpperReachesValidator implements ConstraintValidator<UpperReaches, Integer> {

    @Override
    public void initialize(UpperReaches constraintAnnotation) {
        //这里可以获取到注解中的信息
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        if (UpperReachesEnum.YES.getValue().equals(upperReaches) && StringUtils.isBlank(providerCode)) {
            return false;
        }
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate("这里可以写自定定义的验证失败的消息")
                .addConstraintViolation();
        return false;
    }
}

其中的枚举类 UpperReachesEnum 用于定义 是否 选项。

@Getter
public enum UpperReachesEnum {
    YES(1), NO(2)
    ;
    private Integer value;
    UpperReachesEnum(Integer value) {
        this.value = value;
    }

}

Test

使用 Apache HttpClient 来进行测试,添加相关 maven 依赖

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
</dependency>
<!-- 对象转Json -->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.47</version>
</dependency>

进行JUnit 单元测试

	@Test
    public void testValidator() throws IOException {

        String url = "http://localhost:8080/valid/market";
        String requestBody = getRequestBody();

        HttpClient client = HttpClientBuilder.create().build();
        HttpPost request = new HttpPost(url);

        request.addHeader("User-Agent", USER_AGENT);
        request.addHeader("Accept", "application/json");
        request.addHeader("Content-Type", "application/json");
        
        request.setEntity(new StringEntity(requestBody,"UTF-8"));

        HttpResponse response = client.execute(request);
        
        System.out.println("响应编码 : "
                + response.getStatusLine().getStatusCode());
        BufferedReader rd = new BufferedReader(
                new InputStreamReader(response.getEntity().getContent()));
        StringBuffer result = new StringBuffer();
        String line = "";
        while ((line = rd.readLine()) != null) {
            result.append(line);
        }
        System.out.println("响应内容 : " + result);
 
        ((CloseableHttpClient) client).close();
//        Assert.assertFalse(result.toString().contains("false"));
    }

    private String getRequestBody() {
        return JSONObject.toJSONString(new MarketData("dke", "dfae", "faefjaejflajfkelafefe",
                new BigDecimal(124.323), new BigDecimal(2324.34535), "difajeiafe",
                3, "dfjaie", "fjaeifjiafeawfefe"));
    }

最后获取到的响应信息:

{
"resutlCode": "99999",
"resultMsg": "未知错误",
"resutlData": [
{
"codes": [
"UpperReaches.marketData.upperReaches",
"UpperReaches.upperReaches",
"UpperReaches.java.lang.Integer",
"UpperReaches"
],
"arguments": [
{
"codes": [
"marketData.upperReaches",
"upperReaches"
],
"arguments": null,
"defaultMessage": "upperReaches",
"code": "upperReaches"
}
],
"defaultMessage": "这里可以写自定定义的验证失败的消息",
"objectName": "marketData",
"field": "upperReaches",
"rejectedValue": 3,
"bindingFailure": false,
"code": "UpperReaches"
},
{
"codes": [
"Digits.marketData.inPrice",
"Digits.inPrice",
"Digits.java.math.BigDecimal",
"Digits"
],
"arguments": [
{
"codes": [
"marketData.inPrice",
"inPrice"
],
"arguments": null,
"defaultMessage": "inPrice",
"code": "inPrice"
},
2,
8
],
"defaultMessage": "数字的值超出了允许范围(只允许在8位整数和2位小数范围内)",
"objectName": "marketData",
"field": "inPrice",
"rejectedValue": 124.3229999999999932924765744246542453765869140625,
"bindingFailure": false,
"code": "Digits"
},
{
"codes": [
"LegalEntity.marketData.representId",
"LegalEntity.representId",
"LegalEntity.java.lang.String",
"LegalEntity"
],
"arguments": [
{
"codes": [
"marketData.representId",
"representId"
],
"arguments": null,
"defaultMessage": "representId",
"code": "representId"
}
],
"defaultMessage": "请输入正确的统一社会信用代码/工商注册登记号/身份证号码/护照号",
"objectName": "marketData",
"field": "representId",
"rejectedValue": "dke",
"bindingFailure": false,
"code": "LegalEntity"
},
{
"codes": [
"Digits.marketData.inAmount",
"Digits.inAmount",
"Digits.java.math.BigDecimal",
"Digits"
],
"arguments": [
{
"codes": [
"marketData.inAmount",
"inAmount"
],
"arguments": null,
"defaultMessage": "inAmount",
"code": "inAmount"
},
2,
10
],
"defaultMessage": "数字的值超出了允许范围(只允许在10位整数和2位小数范围内)",
"objectName": "marketData",
"field": "inAmount",
"rejectedValue": 2324.34535000000005311449058353900909423828125,
"bindingFailure": false,
"code": "Digits"
}
]
}

这里面 price 和 amount 值的转换出了点问题,应该使用 new BigDecimal(String.valueOf(12.324242)) 这种方式来来进行转换。

响应信息的其他内容和我们需要的一致。也可以在进行验证的时候可以在 BindingResult 结果中获取我们需要信息进行返回。

后记

在学习使用 Hibernate Validator 的过程中,也考虑过性能问题,找到了几篇文章,说Hibernate Validator 6.0的性能是之前版本的三到四倍,而在实际验证过程中,也不可能有很多的对象,比如说几万个对象要同时进行验证。这样的情况非常极端,而且不应该出现。

当然,使用 Validator 肯定要比写正则对字符串进行校验要慢,但是确实能够精简很多代码,能帮助提高编程效率,而且更容易维护,这是一个取舍的问题。在我看来,代码的可维护性比起牺牲的那点性能要重要的多,这是工作上的问题,是需要看实际的应用情况来决定的。

参考:
http://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints-simple
https://beanvalidation.org/2.0/spec/#constraintmetadata-crossparameterdescriptor
https://code.i-harness.com/zh-CN/q/1e1ac5
http://in.relation.to/2017/10/31/bean-validation-benchmark-revisited/

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值