文章目录
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 包。只要进行简单的代码编写工作就行。
- 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/