受其他项目启发,想做一个通过aop和自定义注解实现的入参校验。看了下网上的资料都不太符合我的想法,我是想着只在方法上加注解,把需要校验的字段传入注解中,而不是在要校验的每个字段上面加注解,因为我觉得后面这种形式对代码侵入性太大了,对于需要校验的字段,每个上面都要加上一个甚至几个的注解,所以我想写一个只在方法上对入参的校验,下面就上代码吧。
- 自定义注解还是用的aop来实现的,所以第一步还是要引入aop的依赖的。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 定义注解,我这里定义了4个注解,感觉应该可以应对大部分的校验了,至少能应对我们公司的业务需要了。这四个注解主要实现了:非空校验、字段长度校验、对日期格式的校验、身份证号校验、手机号校验、邮箱校验和正则校验。下面是这4个注解的代码:
/**
* @Author: TheBigBlue
* @Description: 参数校验,校验不为空、身份证号、手机号、Email
* @Date: 2019/6/18
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamValidate {
/**
* 需要校验非空的字段
*/
String[] notNull() default {};
/**
* 需要校验身份证的字段
*/
String[] idNo() default {};
/**
* 需要校验手机号的字段
*/
String[] phone() default {};
/**
* 需要校验邮箱的字段
*/
String[] email() default {};
}
/**
* @Author: TheBigBlue
* @Description: 校验字段长度
* @Date: 2019/6/18
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LengthValidate {
/**
* 需要校验的字段
*/
String[] value();
/**
* 最小长度
*/
int minLength() default 1;
/**
* 最大长度
*/
int maxLength() default 30;
}
/**
* @Author: TheBigBlue
* @Description:
* @Date: 2019/6/18
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DateValidate {
/**
* 需要校验的字段
*/
String[] value();
/**
* 校验的日期格式
*/
String pattern() default "yyyy-MM-dd HH:mm:ss";
}
/**
* @Author: TheBigBlue
* @Description: 正则校验
* @Date: 2019/6/18
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RegexValidate {
/**
* 需要校验的字段
*/
String[] value();
/**
* 自定义的正则表达式
*/
String regex();
}
- 接下来是写切面和确认切点,使用环绕通知或前置通知,对方法之前增强,获取注解中对应的字段,校验。下面是切面的代码,其中主要还用到了另外4个工具类来分别实现上面4个注解的校验。
主要思路是对所有类所有方法织入,如果该方法无参数,则直接执行该方法,如果该方法有参数,则获取该方法的请求参数和参数值并映射为map对应,然后获取该方法的注解。如果注解包含上面那几种校验的注解,则对应校验之,如果校验失败,则返回统一的错误信息,如果校验成功,参数没有问题,则执行该方法。
/**
* @Author: TheBigBlue
* @Description: 参数校验切面
* @Date: 2019/6/17
*/
@Component
@Aspect
@Order(1002)
public class ParamValidateAspect {
//对包下所有类的所有方法增强
private final String executeExpr = "execution(* com.caxs.app..*.*(..))";
/**
* @Author: TheBigBlue
* @Description: 拦截请求,校验参数
* @Date: 2019/6/20
* @Param joinPoint:
* @Return:
**/
@Around(executeExpr)
public Object paramValidate(ProceedingJoinPoint joinPoint) throws Throwable {
//请求参数值
Object[] args = joinPoint.getArgs();
//请求的方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
JsonResponse resp = JsonResponse.success();
if (!ObjectIsNullUtil.isNullOrEmpty(args)) {
List<Object> filteredArgs = Arrays.stream(args)
//空值转为空串,防止后面get报错
.map(arg -> arg == null ? "" : arg)
.collect(Collectors.toList());
//获取参数名称
LocalVariableTableParameterNameDiscoverer paramNames = new LocalVariableTableParameterNameDiscoverer();
String[] params = paramNames.getParameterNames(method);
//拼接请求参数和参数值
Map<String, Object> rqsParams = IntStream.range(0, filteredArgs.size())
.boxed()
.collect(Collectors.toMap(j -> params[j], j -> filteredArgs.get(j)));
Annotation[] annos = method.getDeclaredAnnotations();
//如果注解不为空,则检查是否有参数校验注解
if (!ObjectIsNullUtil.isNullOrEmpty(annos)) {
for (Annotation anno : annos) {
if (anno instanceof DateValidate) {
//校验日期
resp = ValidateDateUtil.validateDate(anno, rqsParams);
} else if (anno instanceof LengthValidate) {
//校验字段长度
resp = ValidateLengthUtil.validateLength(anno, rqsParams);
} else if (anno instanceof ParamValidate) {
//校验不为空、身份证号、手机号、Email
resp = ValidateParamUtil.validateParam(anno, rqsParams);
} else if (anno instanceof RegexValidate) {
//校验正则
resp = ValidateRegexUtil.validateRegex(anno, rqsParams);
}
//有校验不通过,返回校验失败
if (!ApiEnum.RSLT_CDE_000000.getCode().equalsIgnoreCase(resp.getCode())) {
return resp;
}
}
}
}
return joinPoint.proceed();
}
}
- 上面获取到对应的校验注解调用对应的工具类进行对应的校验,然后下面是四个工具类和一个通用工具类的代码:
先是一个通用工具类,主要提供:反射获取该属性的属性值,获取所有需要校验的字段的属性名和属性值
/**
* @Author: TheBigBlue
* @Description:
* @Date: 2019/6/19
*/
public class CommonAspectUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(CommonAspectUtil.class);
/**
* @Author: TheBigBlue
* @Description: 根据对象和属性名获取属性值
* @Date: 2019/6/19
* @Param targetObj:
* @Param fileName:
* @Return:
**/
public static Object getFieldByObjectAndFileName(Object targetObj, String fileName) throws Exception {
Method methdo = targetObj.getClass().getMethod(getGetterNameByFieldName(fileName));
return methdo.invoke(targetObj);
}
/**
* @Author: TheBigBlue
* @Description: 根据属性名得到该属性的getter方法名
* @Date: 2019/6/19
* @Param fieldName:
* @Return:
**/
public static String getGetterNameByFieldName(String fieldName) {
return "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
}
/**
* @Author: TheBigBlue
* @Description: 获取所有需要校验的字段的属性名和属性值
* @Date: 2019/6/19
* @Param fieldNames: 需要校验的字段数组
* @Param rqsParams: 属性名和属性值map
* @Return:
**/
public static List<FieldInfo> getFieldInfoList(String[] fieldNames, Map<String, Object> rqsParams) {
List<FieldInfo> fieldInfoList = new ArrayList<>();
for (String fieldName : fieldNames) {
try {
if (fieldName.indexOf(".") > 0) {
//只校验对应对象中的属性
String[] split = fieldName.split("\\.");
//根据对象和属性名获取属性值
Object fieldValue = getFieldByObjectAndFileName(rqsParams.get(split[0]), split[1]);
fieldInfoList.add(new FieldInfo(split[1], fieldValue));
} else if (fieldName.indexOf(":all") > 0) {
//TODO 所有对象中属性都校验
} else {
//校验单属性日期格式
fieldInfoList.add(new FieldInfo(fieldName, rqsParams.get(fieldName)));
}
} catch (Exception e) {
LOGGER.error("获取" + fieldName + "属性信息错误!", e);
}
}
return fieldInfoList;
}
}
然后是日期校验工具类。
/**
* @Author: TheBigBlue
* @Description: 校验日期
* @Date: 2019/6/19
*/
public class ValidateDateUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ValidateDateUtil.class);
/**
* @Author: TheBigBlue
* @Description: 校验日期格式
* @Date: 2019/6/19
* @Param anno: 注解对象
* @Param rqsParams: 请求参数和参数值
* @Return:
**/
public static JsonResponse validateDate(Annotation anno, Map<String, Object> rqsParams) {
DateValidate dateValidateAnno = (DateValidate) anno;
//日期格式
String pattern = dateValidateAnno.pattern();
//需要校验的字段
String[] fieldNames = dateValidateAnno.value();
//获取所有需要校验的字段的属性名和属性值
List<FieldInfo> fieldInfoList = CommonAspectUtil.getFieldInfoList(fieldNames, rqsParams);
for (FieldInfo fieldInfo : fieldInfoList) {
//校验每个属性,有一个不符合则返回
JsonResponse fail = validate(fieldInfo.getFieldName(), fieldInfo.getFieldValue(), pattern);
if (!ApiEnum.RSLT_CDE_000000.getCode().equalsIgnoreCase(fail.getCode())) {
return fail;
}
}
return JsonResponse.success();
}
/**
* @Author: TheBigBlue
* @Description: 校验日期格式
* @Date: 2019/6/19
* @Param fieldName: 属性名称
* @Param fieldValue: 属性值
* @Param pattern: 日期格式
* @Return:
**/
private static JsonResponse validate(String fieldName, Object fieldValue, String pattern) {
if (ObjectIsNullUtil.isNullOrEmpty(fieldValue)) {
LOGGER.info(fieldName + "参数为空!");
return JsonResponse.fail(fieldName + "参数为空!");
} else {
//校验日期格式
try {
String dateStr = (String) fieldValue;
SimpleDateFormat format = new SimpleDateFormat(pattern);
// 设置lenient为false.否则SimpleDateFormat会比较宽松地验证日期
format.setLenient(false);
format.parse((String) fieldValue);
//保证格式完全匹配
if (dateStr.length() != pattern.length()) {
throw new BusinessException();
}
return JsonResponse.success();
} catch (Exception e) {
LOGGER.error(fieldName + "日期格式错误。", e);
return JsonResponse.fail(fieldName + "日期格式错误!");
}
}
}
}
然后是字段长度校验工具类。
/**
* @Author: TheBigBlue
* @Description: 校验字段长度
* @Date: 2019/6/19
*/
public class ValidateLengthUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ValidateLengthUtil.class);
/**
* @Author: TheBigBlue
* @Description: 校验字段长度
* @Date: 2019/6/19
* @Param anno: 注解对象
* @Param rqsParams: 请求参数和参数值
* @Return:
**/
public static JsonResponse validateLength(Annotation anno, Map<String, Object> rqsParams) {
LengthValidate lengthValidateAnno = (LengthValidate) anno;
//需要校验的字段
String[] fieldNames = lengthValidateAnno.value();
//最小长度
int minLength = lengthValidateAnno.minLength();
//最大长度
int maxLength = lengthValidateAnno.maxLength();
//获取所有需要校验的字段的属性名和属性值
List<FieldInfo> fieldInfoList = CommonAspectUtil.getFieldInfoList(fieldNames, rqsParams);
for (FieldInfo fieldInfo : fieldInfoList) {
//校验每个属性,有一个不符合则返回
JsonResponse fail = valiedate(fieldInfo.getFieldName(), fieldInfo.getFieldValue(), minLength, maxLength);
if (!ApiEnum.RSLT_CDE_000000.getCode().equalsIgnoreCase(fail.getCode())) {
return fail;
}
}
return JsonResponse.success();
}
/**
* @Author: TheBigBlue
* @Description:
* @Date: 2019/6/19 校验字段长度
* @Param fieldName: 属性名称
* @Param fieldValue: 属性值
* @Param minValue: 最小长度
* @Param maxValue: 最大长度
* @Return:
**/
private static JsonResponse valiedate(String fieldName, Object fieldValue, int minValue, int maxValue) {
if (ObjectIsNullUtil.isNullOrEmpty(fieldValue)) {
LOGGER.info(fieldName + "参数为空!");
return JsonResponse.fail(fieldName + "参数为空!");
} else {
//校验字段长度
try {
int length = ((String) fieldValue).length();
if (length < minValue) {
return JsonResponse.fail(fieldName + "参数长度过短,不得少于" + minValue);
} else if (length > maxValue) {
return JsonResponse.fail(fieldName + "参数长度过长,不得大于" + maxValue);
}
} catch (Exception e) {
LOGGER.error(fieldName + "校验字段长度错误。");
return JsonResponse.fail(fieldName + "校验字段长度错误。");
}
}
return JsonResponse.success();
}
}
然后是正则格式校验工具类。
/**
* @Author: TheBigBlue
* @Description: 校验正则
* @Date: 2019/6/19
*/
public class ValidateRegexUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ValidateRegexUtil.class);
/**
* @Author: TheBigBlue
* @Description: 校验正则
* @Date: 2019/6/19
* @Param anno: 注解对象
* @Param rqsParams: 请求参数和参数值
* @Return:
**/
public static JsonResponse validateRegex(Annotation anno, Map<String, Object> rqsParams) {
RegexValidate regexValidateAnno = (RegexValidate) anno;
//需要校验的字段
String[] fieldNames = regexValidateAnno.value();
//正则表达式
String regex = regexValidateAnno.regex();
//获取所有需要校验的字段的属性名和属性值
List<FieldInfo> fieldInfoList = CommonAspectUtil.getFieldInfoList(fieldNames, rqsParams);
for (FieldInfo fieldInfo : fieldInfoList) {
//校验每个属性,有一个不符合则返回
JsonResponse fail = validate(fieldInfo.getFieldName(), fieldInfo.getFieldValue(), regex);
if (!ApiEnum.RSLT_CDE_000000.getCode().equalsIgnoreCase(fail.getCode())) {
LOGGER.info("正则格式错误!");
return fail;
}
}
return JsonResponse.success();
}
/**
* @Author: TheBigBlue
* @Description: 校验正则格式
* @Date: 2019/6/19
* @Param fieldName: 属性名称
* @Param fieldValue: 属性值
* @Param regex: 正则格式
* @Return:
**/
public static JsonResponse validate(String fieldName, Object fieldValue, String regex) {
if (ObjectIsNullUtil.isNullOrEmpty(fieldValue)) {
LOGGER.info(fieldName + "参数为空!");
return JsonResponse.fail(fieldName + "参数为空!");
} else {
//校验正则格式
try {
Pattern pattern = Pattern.compile(regex);
if (!pattern.matcher((String) fieldValue).matches()) {
LOGGER.info(fieldName + "正则格式错误。");
return JsonResponse.fail(fieldName + "正则格式错误!");
}
return JsonResponse.success();
} catch (Exception e) {
LOGGER.error(fieldName + "正则格式错误。", e);
return JsonResponse.fail(fieldName + "正则格式错误!");
}
}
}
}
然后是非空校验、身份证校验、手机号校验、邮箱校验工具类,其实后面三个就是正则校验,只不过正则表达式是已经写好的。
/**
* @Author: TheBigBlue
* @Description: 校验不为空、身份证号、手机号、Email
* @Date: 2019/6/19
*/
public class ValidateParamUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ValidateParamUtil.class);
private static final String NOT_NULL = "不为空";
private static final String ID_NO = "身份证号";
private static final String PHONE = "手机号";
private static final String EMAIL = "邮箱";
private static final String ID_NO_REGEX = "(^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$)|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$)";
private static final String PHONE_REGEX = "^((13[0-9])|(15[^4])|(18[0,2,3,5-9])|(17[0-8])|(147))\\d{8}$";
private static final String EMIAL_REGEX = "^([\\w-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([\\w-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$";
/**
* @Author: TheBigBlue
* @Description: 校验不为空、身份证号、手机号、Email
* @Date: 2019/6/19
* @Param anno: 注解对象
* @Param rqsParams: 请求参数和参数值
* @Return:
**/
public static JsonResponse validateParam(Annotation anno, Map<String, Object> rqsParams) {
ParamValidate paramValidateAnno = (ParamValidate) anno;
//需要校验非空的字段
String[] notNullFieldNames = paramValidateAnno.notNull();
JsonResponse notNullFail = chooseTyp(notNullFieldNames, rqsParams, NOT_NULL);
if (!ApiEnum.RSLT_CDE_000000.getCode().equalsIgnoreCase(notNullFail.getCode())) {
return notNullFail;
}
//需要校验身份证的字段
String[] idNoFieldNames = paramValidateAnno.idNo();
JsonResponse idNoFail = chooseTyp(idNoFieldNames, rqsParams, ID_NO);
if (!ApiEnum.RSLT_CDE_000000.getCode().equalsIgnoreCase(idNoFail.getCode())) {
return idNoFail;
}
//需要校验手机号的字段
String[] phoneFieldNames = paramValidateAnno.phone();
JsonResponse phoneFail = chooseTyp(phoneFieldNames, rqsParams, PHONE);
if (!ApiEnum.RSLT_CDE_000000.getCode().equalsIgnoreCase(phoneFail.getCode())) {
return phoneFail;
}
//需要校验Email的字段
String[] emailFieldNames = paramValidateAnno.email();
JsonResponse emailFail = chooseTyp(emailFieldNames, rqsParams, EMAIL);
if (!ApiEnum.RSLT_CDE_000000.getCode().equalsIgnoreCase(emailFail.getCode())) {
return emailFail;
}
return JsonResponse.success();
}
/**
* @Author: TheBigBlue
* @Description: 校验不同类型
* @Date: 2019/6/19
* @Param fieldNames: 需要校验的字段
* @Param rqsParams: 请求参数和参数值
* @Param typ: 校验类型
* @Return:
**/
public static JsonResponse chooseTyp(String[] fieldNames, Map<String, Object> rqsParams, String typ) {
if (!ObjectIsNullUtil.isNullOrEmpty(fieldNames)) {
//获取所有需要校验的字段的属性名和属性值
List<FieldInfo> fieldInfoList = CommonAspectUtil.getFieldInfoList(fieldNames, rqsParams);
for (FieldInfo fieldInfo : fieldInfoList) {
//校验每个属性,有一个不符合则返回
JsonResponse fail = validate(fieldInfo.getFieldName(), fieldInfo.getFieldValue(), typ);
if (!fail.getCode().equalsIgnoreCase(ApiEnum.RSLT_CDE_000000.getCode())) {
return fail;
}
}
}
return JsonResponse.success();
}
/**
* @Author: TheBigBlue
* @Description: 校验日期格式
* @Date: 2019/6/19
* @Param fieldName: 属性名称
* @Param fieldValue: 属性值
* @Param typ: 校验类型
* @Return:
**/
private static JsonResponse validate(String fieldName, Object fieldValue, String typ) {
if (ObjectIsNullUtil.isNullOrEmpty(fieldValue)) {
//校验不为空
LOGGER.info(fieldName + "参数为空!");
return JsonResponse.fail(fieldName + "参数为空!");
} else {
switch (typ) {
//校验非空,上面已拦截
case NOT_NULL:
return JsonResponse.success();
//校验身份证号
case ID_NO:
return validateRegex(fieldName, fieldValue, ID_NO, ID_NO_REGEX);
//校验手机号
case PHONE:
return validateRegex(fieldName, fieldValue, PHONE, PHONE_REGEX);
//校验Email
case EMAIL:
return validateRegex(fieldName, fieldValue, EMAIL, EMIAL_REGEX);
//其他情况暂不支持
default:
return JsonResponse.fail("不支持的校验类型!");
}
}
}
/**
* @Author: TheBigBlue
* @Description: 校验正则格式
* @Date: 2019/6/19
* @Param fieldName: 属性名称
* @Param fieldValue: 属性值
* @Param regex: 正则格式
* @Return:
**/
public static JsonResponse validateRegex(String fieldName, Object fieldValue, String typ, String regex) {
//校验日期格式
try {
Pattern pattern = Pattern.compile(regex);
if (!pattern.matcher((String) fieldValue).matches()) {
LOGGER.info(fieldName + typ + "格式错误。");
return JsonResponse.fail(fieldName + typ + "格式错误。");
}
return JsonResponse.success();
} catch (Exception e) {
LOGGER.error(fieldName + typ + "格式错误。", e);
return JsonResponse.fail(fieldName + typ + "格式错误。");
}
}
}
- 然后就可以使用自定义注解了,类似于下面这两种格式,即目前上面的代码支持直接写参数名称,或者是一个对象里的属性,用.来表示,同时这些注解都是默认实现了非空校验的,即如果一个字段要校验非空和身份证,只需要写一个校验身份证的就行。另外,我本来想支持第三种形式的,就是字段:all的格式,就是不管你这个对象或者map套map多少层,只要是这个字段,我都校验,但因为时间原因,就先不写了,这个基本够我用了。
- 最后,在我们相互交流相互学习中,也欢迎各位大神如果有更好的工具和开源项目能够分享给我们大家,谢谢各位。