总概
A、技术栈
- 开发语言:Java 1.8
- 数据库:MySQL、Redis、MongoDB、Elasticsearch
- 微服务框架:Spring Cloud Alibaba
- 微服务网关:Spring Cloud Gateway
- 服务注册和配置中心:Nacos
- 分布式事务:Seata
- 链路追踪框架:Sleuth
- 服务降级与熔断:Sentinel
- ORM框架:MyBatis-Plus
- 分布式任务调度平台:XXL-JOB
- 消息中间件:RocketMQ
- 分布式锁:Redisson
- 权限:OAuth2
- DevOps:Jenkins、Docker、K8S
B、源码地址
C、本节实现目标
- 实现【手机号码】自定义校验注解
- 实现【证件号码】自定义校验注解
D、系列
- 微服务开发系列 第一篇:项目搭建
- 微服务开发系列 第二篇:Nacos
- 微服务开发系列 第三篇:OpenFeign
- 微服务开发系列 第四篇:分页查询
- 微服务开发系列 第五篇:Redis
- 微服务开发系列 第六篇:Redisson
- 微服务开发系列 第七篇:RocketMQ
- 微服务开发系列 第八篇:Elasticsearch
- 微服务开发系列 第九篇:OAuth2
- 微服务开发系列 第十篇:Gateway
- 微服务开发系列 第n篇:AOP请求日志监控
- 微服务开发系列 第n篇:自定义校验注解
一、Spring中的校验注解
在Spring的使用过程中,有一些现成的注解可以使用
- @AssertFalse:该值必须为False
- @AssertTrue:该值必须为True
- @DecimalMax(value,inclusive):被注释的元素必须是一个数字,其值必须小于等于指定的最大值 ,inclusive表示是否包含该值
- @DecimalMin(value,inclusive):被注释的元素必须是一个数字,其值必须大于等于指定的最小值 ,inclusive表示是否包含该值
- @Digits:限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
- @Email:该值必须为邮箱格式
- @Future:被注释的元素必须是一个将来的日期
- @FutureOrPresent:被注释的元素必须是一个现在或将来的日期
- @Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @Negative:该值必须小于0
- @NegativeOrZero:该值必须小于等于0
- @NotBlank:该值不为空字符串,例如“ ”
- @NotEmpty:该值不为空字符串,例如”“
- @NotNull:该值不为Null
- @Null:该值必须为Null
- @Past:被注释的元素必须是一个过去的日期
- @PastOrPresent:被注释的元素必须是一个过去或现在的日期
- @Pattern(regexp):匹配正则
- @Positive:该值必须大于0
- @PositiveOrZero:该值必须大于等于0
- @Size(min,max):数组大小必须在[min,max]这个区间
二、实现自定义注解
自定义注解实现代码放在mall-core里,理由是:自定义注解代码实现后基本上是不会再修改调整的,放在mall-core里可以增强mall-core模块的功能,mall-core可以被任何项目引用去重复使用。即使以后要新增新的自定义注解,继续往mall-core里增加即可,mall-core只是被定义为相对稳定的模块,而不是不能被修改增强。
而mall-common是公共的模块,只是为了将各个服务都需要用到的类、工具提取出来,是可能会经常被修改的,因此没有将自定义注解实现代码放到mall-common里。

2.1 【手机号码】自定义注解类
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* @author Alan Chen
* @description 手机号码校验注解
* @date 2023/04/27
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {MobileValidator.class})
public @interface Mobile {
boolean required() default true;
String message() default "参数不正确";
String regExp() default MobileRegExp.MOBILE_REG_EXP;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
该自定义注解类中用到了四种元注解,最后一个注解@Constraint表示校验此注解的校验器类,可以多个。值得一提的是除了自定义的message、require属性外,下面的groups和payload也是必须添加的。
2.2 手机号码校验正则表达式
/**
* @author Alan Chen
* @description 手机号码校验正则表达式
* @date 2023/04/27
*/
public class MobileRegExp {
/**
* 中国大陆、澳门、香港和台湾:
* ^ 表示匹配字符串的开始位置。
* 1 表示手机号码开头必须是数字 1(适用于中国大陆)。
* [3-9] 表示第二个数字必须是 3、4、5、6、7、8、9 中的任意一个(适用于中国大陆)。
* [5689] 表示手机号码开头必须是数字 5、6、8、9 中的任意一个(适用于澳门和香港)。
* 09 表示手机号码开头必须是数字 09(适用于台湾)。
* \d 表示任意数字。
* {7} 或 {8} 或 {9} 表示前面的数字必须重复出现 7 次(适用于澳门和香港)或 8 次(适用于台湾)或 9 次(适用于中国大陆)。
* | 表示逻辑或。
* () 表示分组,用于将三个表达式组合在一起。
* $ 表示匹配字符串的结束位置。
*/
public final static String MOBILE_REG_EXP = "^(1[3-9]\\d{9}|[5689]\\d{7}|09\\d{8})$";
/**
* 中国-大陆:
* ^ 表示匹配字符串的开始位置。
* 1 表示手机号码开头必须是数字 1。
* [3-9] 表示第二个数字必须是 3、4、5、6、7、8、9 中的任意一个。
* \d 表示任意数字。
* {9}表示前面的数字必须出现9次。
* $ 表示匹配字符串的结束位置。
*/
public final static String MOBILE_REG_EXP_ZH_CN = "^1[3-9]\\d{9}$";
/**
* 中国-澳门:
* 澳门手机号码格式为8位数字,以6开头
* ^ 表示匹配字符串的开始位置。
* 6 表示手机号码开头必须是数字 6。
* \d 表示任意数字。
* {7} 表示前面的数字必须重复出现 7 次。
* $ 表示匹配字符串的结束位置。
*/
public final static String MOBILE_REG_EXP_ZH_MO = "^6\\d{7}$";
/**
* 中国-香港:
* 香港手机号码格式为8位数字,以5、6、8、9开头
* ^ 表示匹配字符串的开始位置。
* [5689] 表示手机号码开头必须是数字 5、6、8、9 中的任意一个。
* \d 表示任意数字。
* {7} 表示前面的数字必须重复出现 7 次。
* $ 表示匹配字符串的结束位置。
*/
public final static String MOBILE_REG_EXP_ZH_HK = "^[5689]\\d{7}$";
/**
* 中国-台湾:
* 台湾地区的手机号码开头一般是09,接下来是八位数字
* ^ 表示匹配字符串的开始位置。
* 09 表示手机号码开头必须是数字 09。
* \d 表示任意数字。
* {8} 表示前面的数字必须重复出现 8 次。
* $ 表示匹配字符串的结束位置。
*/
public final static String MOBILE_REG_EXP_ZH_TW = "^09\\d{8}$";
}
2.3 手机号码校验器
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
/**
* @author Alan Chen
* @description 手机号码校验器
* @date 2023/04/27
*/
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private boolean require = false;
private String regExp;
@Override
public void initialize(Mobile mobile) {
require = mobile.required();
regExp = mobile.regExp();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (require == false) {
return true;
}
return regExpMatch(value);
}
private boolean regExpMatch(String value) {
if (StringUtils.isEmpty(value)) {
return false;
}
return Pattern.compile(regExp).matcher(value).matches();
}
}
校验类需要实现ConstraintValidator接口。接口使用了泛型,需要指定两个参数,第一个自定义注解类,第二个为需要校验的数据类型。实现接口后要override两个方法,分别为initialize方法和isValid方法。其中initialize为初始化方法,可以在里面做一些初始化操作,isValid方法就是我们最终需要的校验方法了。可以在该方法中实现具体的校验步骤。
2.4 group分组接口实现类
我们可能会将PersonEditVO对象用在不同的接口中接收参数,比如在新增和修改接口中。在新增接口中,需要校验mobile,在修改接口中不需要校验mobile。那注解中的groups字段就派上用场了。groups和@Validated配合能控制哪些注解需不需要开启校验。
我们首先定义4个groups分组接口AddAction、EditAction、UpdateAction、DeleteAction,并且继承Default接口。当然也可以不继承Default接口,因为使用注解时不显示指定groups的值,则默认为groups = {Default.class}。所以继承了Default接口,在用@Validated(AddAction.class)时,也会校验groups = {Default.class}的注解。
import javax.validation.groups.Default;
public interface AddAction extends Default {
}
public interface EditAction extends Default {
}
public interface UpdateAction extends Default {
}
public interface DeleteAction extends Default {
}
2.5 VO参数类
import com.ac.core.validation.action.AddAction;
import com.ac.core.validation.action.EditAction;
import com.ac.core.validation.validator.idcard.IdNo;
import com.ac.core.validation.validator.mobile.Mobile;
import com.ac.core.validation.validator.mobile.MobileRegExp;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class PersonEditVO {
@NotNull(message = "ID不能为空", groups = EditAction.class)
@ApiModelProperty("ID")
private Long id;
@NotBlank(message = "用户姓名不能为空", groups = AddAction.class)
@Length(max = 5, message = "姓名最长5个字")
@ApiModelProperty("用户姓名")
private String memberName;
@NotBlank(message = "手机号不能为空", groups = AddAction.class)
@Mobile(message = "手机号格式不正确", regExp = MobileRegExp.MOBILE_REG_EXP_ZH_CN, groups = {AddAction.class, EditAction.class})
@ApiModelProperty("手机号")
private String mobile;
@NotBlank(message = "证件号不能为空")
@IdNo(message = "证件号格式不正确")
@ApiModelProperty("证件号(身份证/港澳通行证/台湾通行证/护照)")
private String idNo;
}
2.6 controller接口
import com.ac.core.validation.action.AddAction;
import com.ac.core.validation.action.EditAction;
import com.ac.member.vo.PersonEditVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Api(tags = "Validation校验测试")
@RestController
@RequestMapping("validation")
public class ValidationController {
@ApiOperation(value = "新增")
@PostMapping
public boolean add(@RequestBody @Validated(AddAction.class) PersonEditVO vo) {
log.info("add,vo={}", vo);
return true;
}
@ApiOperation(value = "修改")
@PutMapping
public boolean update(@RequestBody @Validated(EditAction.class) PersonEditVO vo) {
log.info("update,vo={}", vo);
return true;
}
}
三、异常拦截
如果参数校验不通过,会抛出MethodArgumentNotValidException异常,我们全局处理下然后返回给接口。
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
@ControllerAdvice
@Slf4j
public class ValidExceptionHandler {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public Object errorHandler(HttpServletRequest request, MethodArgumentNotValidException e) {
List<ObjectError> errors = e.getBindingResult().getAllErrors();
if (CollectionUtil.isEmpty(errors)) {
return errors;
}
String message = errors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(";"));
return message;
}
}
四、测试


网友评论