背景

在开发中经常需要写一些字段校验的代码,比如非空,长度限制,邮箱格式验证等等,导致充满了if-else 的代码,不仅相当冗长,而且很让人抓狂。

hibernate validator官方文档)提供了一套比较完善、便捷的验证实现方式。它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。在Spring Boot 火热的现在,该工具已经包含在spring-boot-starter-web中,不需额外引入其他包。

一、快速入门

1.1 在UserDTO中声明要检查的参数

校验说明见代码中注释

@Data
public class UserDTO { /**
性别(不校验)
*/
private String sex; /**
用户名(校验:不能为空,不能超过20个字符串)
*/
@NotBlank(message = "用户名不能为空")
@Length(max = 20, message = "用户名不能超过20个字符")
private String userName; /**
* 手机号(校验:不能为空且按照正则校验格式)
*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
private String mobile; /**
邮箱(校验:不能唯恐且校验邮箱格式)
*/
@NotBlank(message = "联系邮箱不能为空")
@Email(message = "邮箱格式不对")
private String email;
}

1.2 接口处声明要检查的参数

需要在Controller层的入参位置用@Validated 注解声明

@RestController
@RequestMapping("/demo")
public class ValidatorDemoController { /**
* 注解参数校验案例
* @param userDTO
* @return
*/
@PostMapping("/test")
public HttpResult test(@Validated UserDTO userDTO) {
return HttpResult.success(userDTO);
}
}

这里的HttpResult 是Van 自己封装的一个结果集,详见文末Github地址的源码。

1.3 Web全局异常捕获

@ValidSpring Boot 中进行绑定参数校验时会抛出异常,需要在Spring Boot 中处理。

@RestControllerAdvice
@Slf4j
public class WebExceptionHandler { /**
* 方法参数校验
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
public HttpResult handleMethodArgumentNotValidException(BindException e) {
log.error(e.getMessage(), e);
return HttpResult.failure(400,e.getBindingResult().getFieldError().getDefaultMessage());
} @ExceptionHandler(Exception.class)
public HttpResult handleException(Exception e) {
log.error(e.getMessage(), e);
return HttpResult.failure(400, "系统繁忙,请稍后再试");
}
}

1.4 测试

测试工具采用的postman

  • 请求方式:POST
  • 请求地址:localhost:8080/demo/test
  • 请求参数:
userName:Van
mobile:17098705205
email:123
  • 返回结果:
{
"success": false,
"code": 400,
"data": null,
"message": "邮箱格式不对"
}
  • 说明
  1. 更多注解,请各位自行尝试;
  2. 测试结果证明:参数校验生效,且按照我们设定的结果集返回异常信息。

1.5 常见的校验注解

  1. @Null:被注释的元素必须为 null
  2. @NotNull:被注释的元素必须不为 null
  3. @AssertTrue:被注释的元素必须为 true
  4. @AssertFalse:被注释的元素必须为 false
  5. @Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  6. @Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  7. @DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  8. @DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  9. @Size(max=, min=):被注释的元素的大小必须在指定的范围内
  10. @Digits (integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内
  11. @Past:被注释的元素必须是一个过去的日期

    @Future:被注释的元素必须是一个将来的日期
  12. @Pattern(regex=,flag=):被注释的元素必须符合指定的正则表达式
  13. @NotBlank(message =):验证字符串非null,且长度必须大于0
  14. @Email:被注释的元素必须是电子邮箱地址
  15. @Length(min=,max=):被注释的字符串的大小必须在指定的范围内
  16. @NotEmpty:被注释的字符串的必须非空
  17. @Range(min=,max=,message=):被注释的元素必须在合适的范围内

二、自定义注解校验

hibernate validator 自带的注解可以搞定简单的参数校验,加上正则的写法,能解决绝大多数参数校验情况。但是,有些情况,比如:校验是否登录,就需要我们自定义注解校验了。为了方便测试,我这里以身份证校验为例完成自定义校验的过程。

2.1 身份证校验工具类

public class IdCardValidatorUtils {

    protected String codeAndCity[][] = {{"11", "北京"}, {"12", "天津"},
{"13", "河北"}, {"14", "山西"}, {"15", "内蒙古"}, {"21", "辽宁"},
{"22", "吉林"}, {"23", "黑龙江"}, {"31", "上海"}, {"32", "江苏"},
{"33", "浙江"}, {"34", "安徽"}, {"35", "福建"}, {"36", "江西"},
{"37", "山东"}, {"41", "河南"}, {"42", "湖北"}, {"43", "湖南"},
{"44", "广东"}, {"45", "广西"}, {"46", "海南"}, {"50", "重庆"},
{"51", "四川"}, {"52", "贵州"}, {"53", "云南"}, {"54", "西藏"},
{"61", "陕西"}, {"62", "甘肃"}, {"63", "青海"}, {"64", "宁夏"},
{"65", "新疆"}, {"71", "台湾"}, {"81", "香港"}, {"82", "澳门"},
{"91", "国外"}}; private String cityCode[] = {"11", "12", "13", "14", "15", "21", "22",
"23", "31", "32", "33", "34", "35", "36", "37", "41", "42", "43",
"44", "45", "46", "50", "51", "52", "53", "54", "61", "62", "63",
"64", "65", "71", "81", "82", "91"}; // 每位加权因子
private static int power[] = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; // 第18位校检码
private String verifyCode[] = {"1", "0", "X", "9", "8", "7", "6", "5",
"4", "3", "2"}; /**
* 验证所有的身份证的合法性
*
* @param idcard
* @return
*/
public static boolean isValidatedAllIdcard(String idcard) {
if (idcard.length() == 15) {
idcard = convertIdcarBy15bit(idcard);
}
return isValidate18Idcard(idcard);
} /**
* 将15位的身份证转成18位身份证
*
* @param idcard
* @return
*/
public static String convertIdcarBy15bit(String idcard) {
String idcard17 = null;
// 非15位身份证
if (idcard.length() != 15) {
return null;
} if (isDigital(idcard)) {
// 获取出生年月日
String birthday = idcard.substring(6, 12);
Date birthdate = null;
try {
birthdate = new SimpleDateFormat("yyMMdd").parse(birthday);
} catch (ParseException e) {
e.printStackTrace();
}
Calendar cday = Calendar.getInstance();
cday.setTime(birthdate);
String year = String.valueOf(cday.get(Calendar.YEAR)); idcard17 = idcard.substring(0, 6) + year + idcard.substring(8); char c[] = idcard17.toCharArray();
String checkCode = ""; if (null != c) {
int bit[] = new int[idcard17.length()]; // 将字符数组转为整型数组
bit = converCharToInt(c);
int sum17 = 0;
sum17 = getPowerSum(bit); // 获取和值与11取模得到余数进行校验码
checkCode = getCheckCodeBySum(sum17);
// 获取不到校验位
if (null == checkCode) {
return null;
} // 将前17位与第18位校验码拼接
idcard17 += checkCode;
}
} else { // 身份证包含数字
return null;
}
return idcard17;
} /**
* @param idCard
* @return
*/
public static boolean isValidate18Idcard(String idCard) {
// 非18位为假
if (idCard.length() != 18) {
return false;
}
// 获取前17位
String idcard17 = idCard.substring(0, 17);
// 获取第18位
String idcard18Code = idCard.substring(17, 18);
char c[] = null;
String checkCode = "";
// 是否都为数字
if (isDigital(idcard17)) {
c = idcard17.toCharArray();
} else {
return false;
} if (null != c) {
int bit[] = new int[idcard17.length()];
bit = converCharToInt(c);
int sum17 = 0;
sum17 = getPowerSum(bit); // 将和值与11取模得到余数进行校验码判断
checkCode = getCheckCodeBySum(sum17);
if (null == checkCode) {
return false;
}
// 将身份证的第18位与算出来的校码进行匹配,不相等就为假
if (!idcard18Code.equalsIgnoreCase(checkCode)) {
return false;
}
}
return true;
} /**
* 18位身份证号码的基本数字和位数验校
*
* @param idCard
* @return
*/
public boolean is18Idcard(String idCard) {
return Pattern.matches("^[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]{1})$", idCard);
} /**
* 数字验证
*
* @param str
* @return
*/
public static boolean isDigital(String str) {
return str == null || "".equals(str) ? false : str.matches("^[0-9]*$");
} /**
* 将身份证的每位和对应位的加权因子相乘之后,再得到和值
*
* @param bit
* @return
*/
public static int getPowerSum(int[] bit) { int sum = 0; if (power.length != bit.length) {
return sum;
} for (int i = 0; i < bit.length; i++) {
for (int j = 0; j < power.length; j++) {
if (i == j) {
sum = sum + bit[i] * power[j];
}
}
}
return sum;
} /**
* 将和值与11取模得到余数进行校验码判断
*
* @param sum17
* @return 校验位
*/
public static String getCheckCodeBySum(int sum17) {
String checkCode = null;
switch (sum17 % 11) {
case 10:
checkCode = "2";
break;
case 9:
checkCode = "3";
break;
case 8:
checkCode = "4";
break;
case 7:
checkCode = "5";
break;
case 6:
checkCode = "6";
break;
case 5:
checkCode = "7";
break;
case 4:
checkCode = "8";
break;
case 3:
checkCode = "9";
break;
case 2:
checkCode = "x";
break;
case 1:
checkCode = "0";
break;
case 0:
checkCode = "1";
break;
}
return checkCode;
} /**
* 将字符数组转为整型数组
*
* @param c
* @return
* @throws NumberFormatException
*/
public static int[] converCharToInt(char[] c) throws NumberFormatException {
int[] a = new int[c.length];
int k = 0;
for (char temp : c) {
a[k++] = Integer.parseInt(String.valueOf(temp));
}
return a;
} public static void main(String[] args) {
String idCardForFalse = "350583199108290106";
String idCardForTrue = "350583197106150219";
if (IdCardValidatorUtils.isValidatedAllIdcard(idCardForTrue)) {
System.out.println("身份证校验正确");
} else {
System.out.println("身份证校验错误!");
}
}
}

2.2 自定义注解

@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityCardNumberValidator.class)
public @interface IdentityCardNumber { String message() default "身份证号码格式不正确"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};
}

仔细的你会发现,相对于一般的自定义注解,该注解:

@Constraint(validatedBy = IdentityCardNumberValidator.class),该注解的作用就是调用身份证校验的工具。

2.3 在UserDTO 需要校验的字段添加声明

/**
* 身份证号(校验:自定义注解校验)
*/
@IdentityCardNumber
private String idNumber;

2.4 控制层接口

@RestController
@RequestMapping("/custom")
public class ValidatorCustomController { /**
* 自定义注解参数校验案例
* @param userDTO
* @return
*/
@PostMapping("/test")
public HttpResult test(@Validated UserDTO userDTO) {
return HttpResult.success(userDTO);
} }

2.5 自定义注解的测试

  • 请求方式:POST
  • 请求地址:localhost:8080/private/test
  • 请求参数:
userName:Van
mobile:17098705205
email:110@qq.com
idNumber:350583199108290106
  • 返回结果:
{
"success": false,
"code": 400,
"data": null,
"message": "身份证号码格式不正确"
}

三、分组校验

除了上述的校验外,可能还有这种需求:

在创建用户信息时,不需要校验userId;但在更新用户信息时,需要校验userId,而用户名,邮箱等两种情况都得校验。这种情况,就可以分组校验来解决了。

3.1 定义分组接口

  • Create.java
import javax.validation.groups.Default;

public interface Create extends Default {
}
  • Update.java
import javax.validation.groups.Default;

public interface Update extends Default {
}

3.2 在UserDTO 需要校验的字段添加声明

/**
* 用户id(只有在有Update分组中校验非空)
*/
@NotNull(message = "id 不能为空", groups = Update.class)
private Long userId;

3.3 控制层入参位置进行声明

@RestController
@RequestMapping("/groups")
public class ValidatorGroupsController { /**
* 更新数据,需要传入userID
* @param userDTO
* @return
*/
@PostMapping("/update")
public HttpResult updateData(@Validated(Update.class)UserDTO userDTO) {
return HttpResult.success(userDTO);
}
/**
* 新增数据,不需要传入userID
* @param userDTO
* @return
*/
@PostMapping("/create")
public HttpResult createData(@Validated(Create.class)UserDTO userDTO) {
return HttpResult.success(userDTO);
}
}

3.4 分组校验的测试-新增测试

  • 请求方式:POST
  • 请求地址:localhost:8080/groups/create
  • 请求参数:
userName:Van
mobile:17098705205
email:110@qq.com
idNumber:350583197106150219
userId:
  • 返回结果:
{
"success": true,
"code": 200,
"data": {
"userId": null,
"sex": null,
"userName": "Van",
"mobile": "17098705205",
"email": "110@qq.com",
"idNumber": "350583197106150219",
"passWord": null
},
"message": null
}

请求成功,说明新增请求,不检验userId ,即userId 可以为空。

3.5 分组校验的测试-更新测试

  • 请求方式:POST
  • 请求地址:localhost:8080/groups/update
  • 请求参数:同上(3.4)
  • 返回结果:
{
"success": false,
"code": 400,
"data": null,
"message": "id 不能为空"
}

请求失败,说明更新请求,检验userId ,即userId 不能为空。

结合 3.4 与 3.5 的测试结果,说明分组校验成功。

四、总结

希望大家写的每一行代码都是业务需要,而不是无聊且无穷无尽的参数校验。

Github 示例代码

如何从 if-else 的参数校验中解放出来?的更多相关文章

  1. spring注解式参数校验

    很痛苦遇到大量的参数进行校验,在业务中还要抛出异常或者返回异常时的校验信息,在代码中相当冗长,今天我们就来学习spring注解式参数校验. 其实就是:hibernate的validator. 开始啦. ...

  2. 接口参数校验之@Valid与BindingResult

    接口方法往往需要对入参做一些校验,从而判断入参是否合格,而javax.validation包为我们提供了一些常用的参数校验注解,使用起来很方便. 下面这个示例是检验入参对象中的password是否为空 ...

  3. SpringBoot中通过实现WebMvcConfigurer完成参数校验

    在Spring5.0和SpringBoot2.0中废弃了WebMvcConfigurerAdapter类. 现有两种解决方案 1 直接实现WebMvcConfigurer (官方推荐)2 直接继承We ...

  4. 为List<T>中的T进行参数校验

    1.现在前端发送了一个POST请求,他的Data是一个数组,而不是对象(jsonObj的值两侧是中括号). var jsonObj = [{'id':11, 'name':'叵'}, {'id':12 ...

  5. 全栈之路-小程序API-SpringBoot项目中参数校验机制与LomBok工具集使用

    参数校验机制在web开发中是非常重要的,每当看到现在所在公司的校验代码,我都有头疼,每一个接口都是重新写参数的校验,有些复杂的接口,参数的校验甚至占了整个接口代码量的挺大一部分的,看着我都有些头疼,我 ...

  6. 测试开发专题:如何在spring-boot中进行参数校验

    上文我们讨论了spring-boot如何去获取前端传递过来的参数,那传递过来总不能直接使用,需要对这些参数进行校验,符合程序的要求才会进行下一步的处理,所以本篇文章我们主要讨论spring-boot中 ...

  7. java自定义注解实现前后台参数校验

    2016.07.26 qq:992591601,欢迎交流 首先介绍些基本概念: Annotations(also known as metadata)provide a formalized way ...

  8. jQuery form插件的使用--用 formData 参数校验表单,验证后提交(简单验证).

    Form Plugin API 里提供了很多有用的方法可以让你轻松的处理表单里的数据和表单的提交过程. 测试环境:部署到Tomcat中的web项目. 一.引入依赖js <script src=& ...

  9. SpringMVC参数校验(针对`@RequestBody`返回`400`)

    SpringMVC参数校验(针对@RequestBody返回400) 前言 习惯别人帮忙做事的结果是自己不会做事了.一直以来,spring帮我解决了程序运行中的各种问题,我只要关心我的业务逻辑,设计好 ...

随机推荐

  1. Spring IoC源码解析之invokeBeanFactoryPostProcessors

    一.Bean工厂的后置处理器 Bean工厂的后置处理器:BeanFactoryPostProcessor(触发时机:bean定义注册之后bean实例化之前)和BeanDefinitionRegistr ...

  2. Spring系列(二):Spring IoC应用

    一.Spring IoC的核心概念 IoC(Inversion of Control  控制反转),详细的概念见Spring系列(一):Spring核心概念 二.Spring IoC的应用 1.定义B ...

  3. 消息中间件-activemq实战之整合Spring(四)

    前面的理论准备已经很充分,这一节我们来实战:将activemq整合到Spring框架才行中,因为Spring已经集成了JMS,这也为我们配置activermq带来了方便. 1. Spring对jms的 ...

  4. scrapy 自学入门demo分享

    [toc] 本文基于python 3.7.0,win10平台: 2018-08 完整项目代码:https://github.com/NameHewei/python-scrapy 安装 安装pytho ...

  5. DMP大数据营销

    一.下载大数据营销APP 使用手机浏览器扫描二维码 二.使用大数据营销APP 1.打开app,如果手机没有打开蓝牙和GPS定位app会自动提示让您打开,若app没有提示请手动去打开蓝牙和GPS 2.搜 ...

  6. memcached中hash表相关操作

      以下转自http://blog.csdn.net/luotuo44/article/details/42773231 memcached源码中assoc.c文件里面的代码是构造一个哈希表.memc ...

  7. 状压DP概念 及例题(洛谷 P1896 互不侵犯)

    状压DP 就是状态压缩DP.所谓状态压缩,就是将一些复杂的状态压缩起来,一般来说是压缩为一个二进制数,用01来表示某一元素的状态. 比如一排灯泡(5个) 我们可以用一串二进制01串来表示他们的状态 1 ...

  8. spark源码阅读---Utils.getCallSite

    1 作用 当该方法在spark内部代码中调用时,会返回当前调用spark代码的用户类的名称,以及其所调用的spark方法.所谓用户类,就是我们这些用户使用spark api的类. 2 内部实现 2.1 ...

  9. Spring Cloud版本

    Spring Cloud版本 Spring Cloud版本演进情况如下: 版本名称 版本 Finchley snapshot版 Edgware snapshot版 Dalston SR1 当前最新稳定 ...

  10. 基于Spring Boot自建分布式基础应用

    目前刚入职了一家公司,要求替换当前系统(单体应用)以满足每日十万单量和一定系统用户负载以及保证开发质量和效率.由我来设计一套基础架构和建设基础开发测试运维环境,github地址. 出于本公司开发现状及 ...