一、为什么使用 Validation 来验证参数
通常我们在使用spring框架编写接口时,对于部分接口的参数我们要进行判空或者格式校验来避免程序出现异常。那是我们一般都是使用if-else逐个对参数进行校验。这种方法按逻辑来说也是没有问题的,同样也能实现预期效果。但是,这样的代码从可读性以及美观程序来看,是非常糟糕的。那么,我们就可以使用@valid注解来帮助我们优雅的校验参数。
二、如何使用Validation相关注解进行参数校验
- ①为实体类中的参数或者对象添加相应的注解;②在控制器层进行注解声明,或者手动调用校验方法进行校验;③对异常进行处理;
三、Validation类的相关注解及描述
验证注解 | 验证的数据类型 | 说明 |
---|---|---|
@AssertFalse | Boolean,boolean | 验证注解的元素值是false |
@AssertTrue | Boolean,boolean | 验证注解的元素值是true |
@NotNull | 任意类型 | 验证注解的元素值不是null |
@Null | 任意类型 | 验证注解的元素值是null |
@Min(value=值) | BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@Min指定的value值 |
@Max(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value=值) | 和@Min要求一样 | 验证注解的元素值大于等于@ DecimalMin指定的value值 |
@DecimalMax(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@ DecimalMax指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) | 和@Min要求一样 | 验证注解的元素值的整数位数和小数位数上限 |
@Size(min=下限, max=上限) | 字符串、Collection、Map、数组等 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Past | java.util.Date,java.util.Calendar;Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
@Future | 与@Past要求一样 | 验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank | CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min=下限, max=上限) | CharSequence子类型 | 验证注解的元素值长度在min和max区间内 |
@NotEmpty | CharSequence子类型、Collection、Map、数组 | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 | 验证注解的元素值在最小值和最大值之间 |
@Email(regexp=正则表达式,flag=标志的模式) | CharSequence子类型(如String) | 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式 |
@Pattern(regexp=正则表达式,flag=标志的模式) | String,任何CharSequence的子类型 | 验证注解的元素值与指定的正则表达式匹配 |
@Valid | 任何非原子类型 | 指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证 |
此处只列出Validator提供的大部分验证约束注解,请参考hibernate validator官方文档了解其他验证约束注解和进行自定义的验证约束注解定义。
四、使用 Validation API 进行参数效验步骤
整个过程如下图所示,用户访问接口,然后进行参数效验。 对于GET请求的参数可以使用@validated注解配合上面相应的注解进行校验或者按照原先if-else方式进行效验。而对于POST请求,大部分是以表单数据即以实体对象为参数,可以使用@Valid注解方式进行效验(可以简单概括一下,如果接口使用实体类接收参数,那么要用@Valid注解该对象,并且在对象的各属性上添加上方表格里的注解;如果接口直接使用某个字段来接收参数,那么在该字段前添加表格里的注解即可,可参考下文代码)。如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。
五、 Spring Validation的三种校验方式
第一种:在Controller方法参数前加@Valid注解——校验不通过时直接抛异常,get请求直接在平面参数前添加相应的校验规则注解,使用这种的话一般结合统一异常处理进行处理;
第二种:在Controller方法参数前加@Valid注解,参数后面定义一个BindingResult类型参数——执行时会将校验结果放进bindingResult里面,用户自行判断并处理。
/**
* 将校验结果放进BindingResult里面,用户自行判断并处理
*
* @param userInfo
* @param bindingResult
* @return
*/
@PostMapping("/testBindingResult")
public String testBindingResult(@RequestBody @Valid UserInfo userInfo, BindingResult bindingResult) {
// 参数校验
if (bindingResult.hasErrors()) {
String messages = bindingResult.getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.reduce((m1, m2) -> m1 + ";" + m2)
.orElse("参数输入有误!");
//这里可以抛出自定义异常,或者进行其他操作
throw new IllegalArgumentException(messages);
}
return "操作成功!";
}
这里我们是直接抛出了异常,如果没有进行全局异常处理的话,接口将会返回如下信息:
- 第三种:用户手动调用对应API执行校验——Validation.buildDefault ValidatorFactory().getValidator().validate(xxx)
这种方法适用于校验任意一个有valid注解的实体类,并不仅仅是只能校验接口中的参数;
这里我提取出一个工具类,如下:MyValidationUtils.class
import org.springframework.util.CollectionUtils;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import java.util.Set;
/**
* 手动调用api方法校验对象
*/
public class MyValidationUtils {
public static void validate(@Valid Object user) {
Set<ConstraintViolation<@Valid Object>> validateSet = Validation.buildDefaultValidatorFactory()
.getValidator()
.validate(user, new Class[0]);
if (!CollectionUtils.isEmpty(validateSet)) {
String messages = validateSet.stream()
.map(ConstraintViolation::getMessage)
.reduce((m1, m2) -> m1 + ";" + m2)
.orElse("参数输入有误!");
throw new IllegalArgumentException(messages);
}
}
}
五、springboot项目中实战演练
spring-boot-starter-web依赖已经集成相关jar,无需额外引入。
- 1.对实体类的变量进行注解标注
实体类中添加 @Valid 相关验证注解,并在注解中添加出错时的响应消息。
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@Data
public class User {
@NotBlank(message = "姓名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Length(min = 6, max = 16, message = "密码长度为6-16位")
private String password;
@Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手机号格式不正确")
private String phone;
// 嵌套必须加 @Valid,否则嵌套中的验证不生效
@Valid
@NotNull(message = "userinfo不能为空")
private UserInfo userInfo;
}
如果是嵌套的实体对象,并且也要校验该对象,则需要在最外层属性上添加 @Valid 注解
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
@Data
public class UserInfo {
@NotBlank(message = "年龄不为空")
@Max(value = 18, message = "不能超过18岁")
private String age;
@NotBlank(message = "性别不能为空")
private String gender;
}
六、统一接口返回结果
前端接口请求后台端,后端将返回结果统一封装。提高交互的规范性及通用性,也提高了前后端联调效率。前端根据规范格式返回结构体进行统一映射处理,就避免一个接口一个返回格式的问题。
1、统一封装结果包含如下参数
- 状态码:code
- 状态信息:status
- 返回信息:message
- 数据:data
2、统一封装结果包含如下方法
- 全参数方法
- 成功返回(无参)
- 成功返回(枚举)
- 成功返回(状态码+返回信息)
- 成功返回(返回信息 + 数据)
- 成功返回(状态码+返回信息+数据)
- 成功返回(数据)
- 成功返回(返回信息)
- 失败返回(无参)
- 失败返回(枚举)
- 失败返回(状态码+返回信息)
- 失败返回(返回信息+数据)
- 失败返回(状态码+返回信息+数据)
- 失败返回(数据)
- 失败返回(返回信息)
3、ResponseResult封装返回结果代码
@Data
@ApiModel("统一结果集处理器")
public class ResponseResult<T> {
/**
* 状态码
*/
@ApiModelProperty(value = "状态码")
private Integer code;
/**
* 状态信息
*/
@ApiModelProperty(value = "状态信息")
private Boolean status;
/**
* 返回信息
*/
@ApiModelProperty(value = "返回信息")
private String message;
/**
* 数据
*/
@ApiModelProperty(value = "数据")
private T data;
/**
* 全参数方法
*
* @param code 状态码
* @param status 状态
* @param message 返回信息
* @param data 返回数据
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
private static <T> ResponseResult<T> response(Integer code, Boolean status, String message, T data) {
ResponseResult<T> responseResult = new ResponseResult<>();
responseResult.setCode(code);
responseResult.setStatus(status);
responseResult.setMessage(message);
responseResult.setData(data);
return responseResult;
}
/**
* 全参数方法
*
* @param code 状态码
* @param status 状态
* @param message 返回信息
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
private static <T> ResponseResult<T> response(Integer code, Boolean status, String message) {
ResponseResult<T> responseResult = new ResponseResult<>();
responseResult.setCode(code);
responseResult.setStatus(status);
responseResult.setMessage(message);
return responseResult;
}
/**
* 成功返回(无参)
*
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> success() {
return response(HttpStatusEnum.SUCCESS.getCode(), true, HttpStatusEnum.SUCCESS.getMessage(), null);
}
/**
* 成功返回(枚举参数)
*
* @param httpResponseEnum 枚举参数
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> success(HttpStatusEnum httpResponseEnum) {
return response(httpResponseEnum.getCode(), true, httpResponseEnum.getMessage());
}
/**
* 成功返回(状态码+返回信息)
*
* @param code 状态码
* @param message 返回信息
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> success(Integer code, String message) {
return response(code, true, message);
}
/**
* 成功返回(返回信息 + 数据)
*
* @param message 返回信息
* @param data 数据
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> success(String message, T data) {
return response(HttpStatusEnum.SUCCESS.getCode(), true, message, data);
}
/**
* 成功返回(状态码+返回信息+数据)
*
* @param code 状态码
* @param message 返回信息
* @param data 数据
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> success(Integer code, String message, T data) {
return response(code, true, message, data);
}
/**
* 成功返回(数据)
*
* @param data 数据
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> success(T data) {
return response(HttpStatusEnum.SUCCESS.getCode(), true, HttpStatusEnum.SUCCESS.getMessage(), data);
}
/**
* 成功返回(返回信息)
*
* @param message 返回信息
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> success(String message) {
return response(HttpStatusEnum.SUCCESS.getCode(), true, message, null);
}
/**
* 失败返回(无参)
*
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> fail() {
return response(HttpStatusEnum.ERROR.getCode(), false, HttpStatusEnum.ERROR.getMessage(), null);
}
/**
* 失败返回(枚举)
*
* @param httpResponseEnum 枚举
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> fail(HttpStatusEnum httpResponseEnum) {
return response(httpResponseEnum.getCode(), false, httpResponseEnum.getMessage());
}
/**
* 失败返回(状态码+返回信息)
*
* @param code 状态码
* @param message 返回信息
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> fail(Integer code, String message) {
return response(code, false, message);
}
/**
* 失败返回(返回信息+数据)
*
* @param message 返回信息
* @param data 数据
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> fail(String message, T data) {
return response(HttpStatusEnum.ERROR.getCode(), false, message, data);
}
/**
* 失败返回(状态码+返回信息+数据)
*
* @param code 状态码
* @param message 返回消息
* @param data 数据
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> fail(Integer code, String message, T data) {
return response(code, false, message, data);
}
/**
* 失败返回(数据)
*
* @param data 数据
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> fail(T data) {
return response(HttpStatusEnum.ERROR.getCode(), false, HttpStatusEnum.ERROR.getMessage(), data);
}
/**
* 失败返回(返回信息)
*
* @param message 返回信息
* @param <T> 泛型
* @return {@link ResponseResult<T>}
*/
public static <T> ResponseResult<T> fail(String message) {
return response(HttpStatusEnum.ERROR.getCode(), false, message, null);
}
}
4、HttpStatusEnum返回结果代码
/**
* Http状态返回枚举
*
**/
@Getter
public enum HttpStatusEnum {
/**
* 操作成功
*/
SUCCESS(200, "操作成功"),
/**
* 对象创建成功
*/
CREATED(201, "对象创建成功"),
/**
* 请求已经被接受
*/
ACCEPTED(202, "请求已经被接受"),
/**
* 操作已经执行成功,但是没有返回数据
*/
NO_CONTENT(204, "操作已经执行成功,但是没有返回数据"),
/**
* 资源已被移除
*/
MOVED_PERM(301, "资源已被移除"),
/**
* 重定向
*/
SEE_OTHER(303, "重定向"),
/**
* 资源没有被修改
*/
NOT_MODIFIED(304, "资源没有被修改"),
/**
* 参数列表错误(缺少,格式不匹配)
*/
BAD_REQUEST(400, "参数列表错误(缺少,格式不匹配)"),
/**
* 未授权
*/
UNAUTHORIZED(401, "未授权"),
/**
* 访问受限,授权过期
*/
FORBIDDEN(403, "访问受限,授权过期"),
/**
* 资源,服务未找到
*/
NOT_FOUND(404, "资源,服务未找!"),
/**
* 不允许的http方法
*/
BAD_METHOD(405, "不允许的http方法"),
/**
* 资源冲突,或者资源被锁
*/
CONFLICT(409, "资源冲突,或者资源被锁"),
/**
* 不支持的数据,媒体类型
*/
UNSUPPORTED_TYPE(415, "不支持的数据,媒体类型"),
/**
* 系统内部错误
*/
ERROR(500, "系统内部错误"),
/**
* 接口未实现
*/
NOT_IMPLEMENTED(501, "接口未实现"),
/**
* 系统警告消息
*/
WARN(601,"系统警告消息");
private final Integer code;
private final String message;
HttpStatusEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
七、配置全局异常处理
在使用上方统一返回结果的加持下,规范的同时也不可避免程序异常情况。因此我们必须提前定义一个统一全局异常来捕获这些异常信息,并将其当作一种结果返回给控制层,友好的处理异常信息。
1.全局异常处理注解 @RestControllerAdvice
- @RestControllerAdvice注解是Spring MVC和Spring Boot应用程序中用于定义全局异常处理类的注解,它是@ControllerAdvice注解的特殊版本,是一个组合注解,由@ControllerAdvice、@ResponseBody组成。
- @ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个组件,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法。
@RestControllerAdvice的特点
- 注解@RestControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。
- @RestControllerAdvice注解将作用于所有注解了@RequestMapping的控制器的方法上。
@ExceptionHandler:用于指定异常处理方法,与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。 - @InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。
- @ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对
2.@ExceptionHandler常用异常拦截
- 权限校验异常:AccessDeniedException(spring-security中异常)
- 请求方式不支持:HttpRequestMethodNotSupportedException
- 业务异常:ServiceException(自己业务定义异常)
- 拦截未知的运行时异常:RuntimeException
- 系统异常:Exception
- 自定义验证异常:BindException
- 自定义验证异常:MethodArgumentNotValidException
3.全局异常处理代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 权限校验异常
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(AccessDeniedException.class)
public ResponseResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return ResponseResult.fail(HttpStatusEnum.FORBIDDEN.getCode(), HttpStatusEnum.FORBIDDEN.getMessage());
}
/**
* 业务异常
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BusinessException.class)
public ResponseResult<?> businessException(BusinessException e) {
if (Objects.isNull(e.getCode())) {
return RespResult.err(e.getMessage());
}
return ResponseResult.fail(e.getCode(), e.getMessage());
}
/**
* BindException异常处理
* <p>BindException: 作用于@Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效</p>
*
* @param e BindException异常信息
* @return 响应数据
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
public ResponseResult<?> bindExceptionHandler(BindException e) {
String msg = e.getBindingResult().getFieldErrors()
.stream()
.map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage()))
.reduce((x, y) -> String.format("%s; %s", x, y))
.orElse("参数输入有误");
log.error("BindException异常,参数校验异常:{}", msg);
return ResponseResult.fail(msg);
}
/**
* MethodArgumentNotValidException-Spring封装的参数验证异常处理
* <p>MethodArgumentNotValidException:作用于 @Validated @Valid 注解,接收参数加上@RequestBody注解(json格式)才会有这种异常。</p>
*
* @param e MethodArgumentNotValidException异常信息
* @return 响应数据
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseResult<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors()
.stream()
.map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage()))
.reduce((x, y) -> String.format("%s; %s", x, y))
.orElse("参数输入有误");
log.error("MethodArgumentNotValidException异常,参数校验异常:{}", msg);
return ResponseResult.fail(msg);
}
/**
* ConstraintViolationException-jsr规范中的验证异常,嵌套检验问题
* <p>ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。</p>
* <p>注:Controller类上必须添加@Validated注解,否则接口单个参数校验无效</p>
*
* @param e ConstraintViolationException异常信息
* @return 响应数据
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseResult<?> constraintViolationExceptionHandler(ConstraintViolationException e) {
String msg = e.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
log.error("ConstraintViolationException,参数校验异常:{}", msg);
return ResponseResult.fail(msg);
}
/**
* 请求参数不全-缺少servlet请求参数抛出的异
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ResponseResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) {
log.error("MissingServletRequestParameterException,参数校验异常:{}", e.getMessage());
return ResponseResult.fail(e.getMessage());
}
/**
* http请求的方法不正确
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) {
log.error("http请求的方法不正确:【"+e.getMessage()+"】");
return ResponseResult.fail(e.getMethod() + "请求");
}
/**
* 请求参数不能正确读取解析时,抛出的异常,比如传入和接受的参数类型不一致
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({HttpMessageNotReadableException.class})
public ResponseResult<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.error("[handleHttpMessageNotReadableException] 参数解析失败:", e);
return ResponseResult.fail(e.getMessage());
}
/**
* 方法请求参数类型不匹配异常
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({MethodArgumentTypeMismatchException.class})
public ResponseResult<?> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.error("[handleMethodArgumentTypeMismatchException] 方法参数类型不匹配异常: ", e);
return ResponseResult.fail(e.getMessage());
}
/**
* javax.validation 下校验参数时抛出的异常
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ValidationException.class)
public ResponseResult<?> handleValidationException(ValidationException e) {
log.error("[handleValidationException] 参数验证失败:", e);
return ResponseResult.fail(e.getMessage());
}
/**
* 不支持当前媒体类型抛出的异常
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
@ExceptionHandler({HttpMediaTypeNotSupportedException.class})
public ResponseResult<?> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
log.error("[handleHttpMediaTypeNotSupportedException] 不支持当前媒体类型: ", e);
return ResponseResult.fail(e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ResponseResult<?> exceptionHandler(Exception e) {
log.error("其他异常", e);
return ResponseResult.fail(ResultEnum.INTERNAL_SERVER_ERROR.code, ResultEnum.INTERNAL_SERVER_ERROR.message);
}
}
4.使用自定义参数注解
- 1.我们这里创建一个身份证校验注解
@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 {};
}
这个注解是作用在Field字段上,运行时生效,触发的是IdentityCardNumber这个验证类。
message 定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制
groups 这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作
payload 主要是针对bean的,使用不多。
2.自定义Validator
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> {
@Override
public void initialize(IdentityCardNumber identityCardNumber) {
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
return IdCardValidatorUtils.isValidate18Idcard(o.toString());
}
}
校验工具类IdCardValidatorUtils.class
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Pattern;
/**
* 根据〖中华人民共和国国家标准GB11643-1999〗中有关公民身份号码的规定,
* 公民身份号码是特征组合码,由十七位数字本体码和一位数字校验码组成。
* 排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。
* 顺序码: 表示在同一地址码所标识的区域范围内,对同年、同月、同 日出生的人编定的顺序号,顺序码的奇数分配给男性,偶数分配 给女性。
* 1.前1、2位数字表示:所在省份的代码;
* 2.第3、4位数字表示:所在城市的代码;
* 3.第5、6位数字表示:所在区县的代码;
* 4.第7~14位数字表示:出生年、月、日;
* 5.第15、16位数字表示:所在地的派出所的代码;
* 6.第17位数字表示性别:奇数表示男性,偶数表示女性;
* 7.第18位数字是校检码:也有的说是个人信息码,一般是随计算机的随机产生,用来检验身份证的正确性。
* 校检码可以是0~9的数字,有时也用x表示。
* <p>
* 2、第十八位数字(校验码)的计算方法
* 1.将前面的身份证号码17位数分别乘以不同的系数。从第一位到第十七位的系数分别为:7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2
* 2.将这17位数字和系数相乘的结果相加。
* 3.用加出来和除以11,看余数是多少?
* 4.余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字。其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2。
* 5.通过上面得知如果余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2。
*/
public class IdCardValidatorUtils {
/**
* 省,直辖市代码表: { 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:"国外"}
*/
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;
}
}
- 使用自定义的注解
@NotBlank(message = "身份证号不能为空")
@IdentityCardNumber(message = "身份证信息有误,请核对后提交")
private String clientCardNo;
参考:
https://zhuanlan.zhihu.com/p/656464576