AOP+自定义注解实现全局参数校验
在开发过程中,用户传递的数据不一定合法,虽然可以通过前端进行一些校验,但是为了确保程序的安全性,保证数据的合法,在后台进行数据校验也是十分必要的。
后台的参数校验
在controller方法中校验:
后台的参数是通过controller方法获取的,所以最简单的参数校验的方法,就是在controller方法中进行参数校验。在controller方法中如果进行参数校验会有大量重复、没有太大意义的代码。
使用拦截器、过滤器校验
为了保证controller中的代码有更好的可读性,可以将参数校验的工作交由拦截器(Interceptor)或者过滤器(Filter)来完成,但是此时又存在一个问题:非共性的参数需要每个方法都创建一个与之对应的拦截器(或者过滤器)。
实现对Entity的统一校验
鉴于上述解决方案的缺点,我们可以借助AOP的思想来进行统一的参数校验。思想是通过自定义注解来完成对实体类属性的标注,在AOP中扫描加了自定义注解的属性,对其进行注解属性标注的校验。对于不满足的参数直接抛出自定义异常,交由全局异常处理来处理并返回友好的提示信息。
在介绍此方法之前,我们先来介绍一下使用其会用到的一些内容。
自定义异常
在开发过程中,经常需要抛出一些异常,但是异常中没有状态码,自定义描述等属性。所以可以自定义一个异常。抛出异常时,使用全局异常处理,通过全局异常来处理此异常。
注意:Aspect中的异常只有RuntimeException(及其子类)能被全局异常处理。
所以我们通常将自定义异常定义为运行时异常。
package cn.rayfoo.common.exception;
import lombok.*;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/20 9:26 下午
* @Description: 自定义的异常...
*/
@Getter@Setter@Builder@NoArgsConstructor@AllArgsConstructor
public class MyException extends RuntimeException{
private int code;
private String msg;
}
断言类
在代码的执行过程中,我们经常需要在特定条件下(一般为是否满足某条件)抛出异常,此时需要加入抛异常、返回状态码、错误信息、记录日志等操作,此操作是大量重复的操作,所以借助Junit中Assert的思想,创建了下述的断言工具类,用于在指定条件下抛出一个自定义异常。
package cn.rayfoo.common.exception;
import cn.rayfoo.common.response.HttpStatus;
import lombok.extern.slf4j.Slf4j;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>断言类</p>
* @date 2020/8/7 9:43
*/
@Slf4j
public class MyAssert {
/**
* 如果为空直接抛出异常 类似于断言的思想
* @param status 当status为false 就会抛出异常 不继续执行后续语句
* @param msg 异常描述
*/
public static void assertMethod(boolean status, String msg) throws Exception {
//为false抛出异常
if (!status) {
//记录错误信息
log.error(msg);
//抛出异常
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(msg).build();
}
}
/**
* 如果为空直接抛出异常 类似于断言的思想
* @param status 当status为false 就会抛出异常 不继续执行后续语句
* @param code 状态码
* @param msg 异常描述
*/
public static void assertMethod(boolean status,Integer code, String msg) throws Exception {
//为false抛出异常
if (!status) {
//记录错误信息
log.error(msg);
//抛出异常
throw MyException.builder().code(code).msg(msg).build();
}
}
/**
* 如果为空直接抛出异常 类似于断言的思想
* @param status 当status为false 就会抛出异常 不继续执行后续语句
*/
public static void assertMethod(boolean status) throws Exception {
//为false抛出异常
if (!status) {
//记录错误信息
log.error(HttpStatus.INTERNAL_SERVER_ERROR.name());
//抛出异常
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(HttpStatus.INTERNAL_SERVER_ERROR.name()).build();
}
}
}
当调用断言方法时,只要传递一个boolean表达式,当表达式为false,就会抛出一个异常,提前结束方法。这个异常,通常由全局异常处理类来拦截。
全局异常处理拦截断言抛出的方法
package cn.rayfoo.common.exception;
import cn.rayfoo.common.response.HttpStatus;
import cn.rayfoo.common.response.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.FileNotFoundException;
/**
* @author rayfoo@qq.com
* @version 1.0
* @date 2020/8/5 14:55
* @description 全局异常处理
*/
@ControllerAdvice@Slf4j
public class ServerExceptionResolver {
/**
* 对某种异常进行处理,如果非前后端分离的项目此处可以使用ModelAndView 返回错误页面
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)@ResponseBody
public Result<String> resolveException(Exception ex) {
//打印完整的异常信息
ex.printStackTrace();
//创建result
Result<String> result = new Result<>();
//设置result属性
result.setData(ex.getMessage());
result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
//判断异常类型
if(ex instanceof FileNotFoundException){
log.error("文件问找到异常。。。");
// TODO 自定义一个Status
result.setMsg("文件未找到,请检查文件是否存在!");
}
else if(ex instanceof RuntimeException){
log.error("服务器内部发生了异常");
result.setMsg(HttpStatus.INTERNAL_SERVER_ERROR.name());
}else{
log.error("服务器内部发生了异常");
result.setMsg(HttpStatus.INTERNAL_SERVER_ERROR.name());
}
//.....
return result;
}
/**
* 处理自定义的异常
* @param ex
* @return
*/
@ExceptionHandler(MyException.class)@ResponseBody
public Result<String> resolveMyException(MyException ex){
//打印完整的异常信息
ex.printStackTrace();
//创建result
Result<String> result = new Result<>();
//设置result属性
result.setData(ex.getMsg());
result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
result.setMsg(ex.getMsg());
//保存自定义异常日志
log.error(ex.getMsg());
return result;
}
}
定义Verify注解
准备好上面的内容,我们就可以使用自定义注解+Aspect来完成全局的参数校验了~
此注解用于注解实体类的属性,这个注解中,创建了以下几个属性:
- name:用于描述修饰的字段,当校验失败时,提示用户字段的具体名称。
- maxLength:最大的长度,对字符串长度进行校验,如果是默认值代表不进行长度校验
- minLength:最小的长度,同样进行字符串长度的校验,如果是默认值代表不进行长度校验
- required:是否是必填属性,即进行非空判断
- notNull:进行非空和非空串的判断
- regular:指定用于校验的正则表达式,如果为RegexOption.DEFAULT表示不进行正则校验
package cn.rayfoo.common.annotation;
import cn.rayfoo.common.enums.RegexOption;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:33
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface Verify {
/** 参数名称 */
String name();
/** 参数最大长度 */
int maxLength() default Integer.MAX_VALUE;
/** 是否必填 这里只是判断是否为null */
boolean required() default true;
/** 是否为非空 是否为null和空串都判断 */
boolean notNull() default true;
/** 最小长度 */
int minLength() default Integer.MIN_VALUE;
/** 正则匹配 */
RegexOption regular() default RegexOption.DEFAULT;
}
上面的自定义注解中使用到了RegexOption枚举,此注解只写了常见的正则校验方法,如果需要拓展可以自定添加,下面是此枚举的代码:
package cn.rayfoo.common.enums;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:51
*/
public enum RegexOption {
/**
* 缺省,表示不进行正则校验
*/
DEFAULT(""),
/**
* 邮箱正则
*/
EMAIL_REGEX("^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$"),
/**
* 手机号正则
*/
PHONE_NUMBER_REGEX("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\d{8}$"),
/**
* 身份证正则
*/
IDENTITY_CARD_REGEX("(^\d{18}$)|(^\d{15}$)"),
/**
* URL正则
*/
URL_REGEX("http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"),
/**
* IP地址正则
*/
IP_ADDR_REGEX("(25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]?\d)"),
/**
* 用户名正则
*/
USERNAME_REGEX("^[a-zA-Z]\w{5,20}$"),
/**
* 密码正则
*/
PASSWORD_REGEX("^[a-zA-Z0-9]{6,20}$");
/**
* 正则
*/
private String regex;
/**
* 构造方法
*
* @param regex
*/
private RegexOption(String regex) {
this.regex = regex;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
使用Aspect进行全局参数校验
前面的准备工作做好,就可以进行全局的参数校验了~
package cn.rayfoo.common.aspect;
import cn.rayfoo.common.annotation.Verify;
import cn.rayfoo.common.enums.RegexOption;
import cn.rayfoo.common.exception.MyAssert;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>Controller中的全局参数校验</p>
* @date 2020/8/7 14:03
*/
@Aspect
//@Component
@Slf4j
public class EntityValidatorAspect {
/**
* 定义一个方法,用于声明切入表达式。
*/
@Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
public void validatorPointcut() {
}
@Before("validatorPointcut()")
public void parameterVerify(JoinPoint point) throws Exception {
//迭代所有参数
for (int i = 0; i < point.getArgs().length; i++) {
//切点对象
Object obj = point.getArgs()[i];
Class clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
//需要做校验的参数
if (field.isAnnotationPresent(Verify.class)) {
//获取注解对象
Verify verify = field.getAnnotation(Verify.class);
//取出注解的属性
String name = verify.name();
int maxLength = verify.maxLength();
int minLength = verify.minLength();
boolean required = verify.required();
boolean notNull = verify.notNull();
RegexOption regular = verify.regular();
//属性值
Object fieldObj = field.get(obj);
//是否时必传 断言判断
if (required) {
MyAssert.assertMethod(fieldObj != null, String.format("【%s】为必传参数", name));
}
//字符串的 非空校验
if (notNull) {
MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能为空", name));
}
//是否有最大长度限制 断言判断
if (Integer.MAX_VALUE != maxLength) {
MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最大长度为【%s】", name, maxLength));
}
//是否有最小长度限制 断言判断
if (Integer.MIN_VALUE != minLength) {
MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最小长度为【%s】", name, minLength));
}
//是否有正则校验
if (!"".equals(regular.getRegex())) {
Pattern pattern = Pattern.compile(regular.getRegex());
//断言判断正则
MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("参数【%s】的请求数据不符合规则", name));
}
}
}
}
}
}
上述的校验适用于Controller方法中参数为自定义的实体类,但是对于Map类型、普通类型(包括包装类型)的参数还无法完成校验。后续可以考虑增加对自定义注解的拓展,即可以允许加在方法参数上。
对于Map类型的参数进行校验
上述的校验完成后,又发现了一个问题:如果Controller方法的参数是Map类型,如何完成参数的校验?
经过一番思考,结合上面案例的解决方案,最终也实现了对map的校验,但是要求比较严苛:由于其原理是通过key来匹配校验规则,所以map中的key,必须是后端指定的key才能自动完成校验。
下面介绍以下具体的实现方法:
创建校验枚举
这个枚举是不是很眼熟呀,没错 就是基于上面的注解编写的,增加了一个key属性。通过key属性可以判断map中指定的key进行何种正则校验。
package cn.rayfoo.common.enums;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:51
*/
public enum JSONRegexOption {
/**
* 缺省,表示不进行正则校验
*/
DEFAULT("",""),
/**
* 邮箱正则
*/
EMAIL_REGEX("email","^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$"),
/**
* 手机号正则
*/
PHONE_NUMBER_REGEX("phoneNumber","^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\d{8}$"),
/**
* 身份证正则
*/
IDENTITY_CARD_REGEX("identityCard","(^\d{18}$)|(^\d{15}$)"),
/**
* URL正则
*/
URL_REGEX("url","http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"),
/**
* IP地址正则
*/
IP_ADDR_REGEX("ipAddr","(25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]?\d)"),
/**
* 用户名正则
*/
USERNAME_REGEX("username","^[a-zA-Z]\w{5,20}$"),
/**
* 密码正则
*/
PASSWORD_REGEX("password","^[a-zA-Z0-9]{6,20}$");
/**
* JSON的key
*/
private String key;
/**
* 正则
*/
private String regex;
/**
* 构造方法
*
* @param regex
*/
private JSONRegexOption(String key,String regex) {
this.key = key;
this.regex = regex;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
在Aspect中进行全局校验
经过反复的断点测试发现,Map类型的参数在JoinPoint中获取时是通过java.util.LinkedHashMap类型来接受的。所以我们可以通过判断参数的类型来判断当前参数是否为map,如果为Map通过遍历Map的key来实现全局的校验:
package cn.rayfoo.common.aspect;
import cn.rayfoo.common.enums.JSONRegexOption;
import cn.rayfoo.common.exception.MyAssert;
import cn.rayfoo.common.exception.MyException;
import cn.rayfoo.common.response.HttpStatus;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>Controller中的JSON全局参数校验</p>
* @date 2020/8/7 14:03
*/
@Aspect
@Component
@Slf4j
public class JsonValidatorAspect {
/**
* 定义一个方法,用于声明切入表达式。
*/
@Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
public void validatorPointcut() {
}
@Before("validatorPointcut()")
public void parameterVerify(JoinPoint point) throws Exception {
//迭代所有参数
for (int i = 0; i < point.getArgs().length; i++) {
//切点对象
Object obj = point.getArgs()[i];
//将数据转换为json
Class clazz = obj.getClass();
//如果是map接收参数
if ("java.util.LinkedHashMap".equals(clazz.getName())) {
//获取集合
LinkedHashMap map = (LinkedHashMap) obj;
//获取key列表
Set set = map.keySet();
//迭代key
for (Object key : set) {
//如果有空值 或者空字符串
if (StringUtils.isEmpty(map.get(key))) {
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg("数据中存在空值!").build();
}
//用户名校验
valueValidate(JSONRegexOption.USERNAME_REGEX, map, key, "您输入的用户名不符合规范");
//密码校验
valueValidate(JSONRegexOption.PASSWORD_REGEX, map, key, "您输入的密码不符合规范");
//邮箱校验
valueValidate(JSONRegexOption.EMAIL_REGEX, map, key, "您输入的邮箱不符合规范");
//手机号校验
valueValidate(JSONRegexOption.PHONE_NUMBER_REGEX, map, key, "您输入的手机号不符合规范");
//身份证号校验
valueValidate(JSONRegexOption.IDENTITY_CARD_REGEX, map, key, "您输入的身份证号不符合规范");
//ip校验
valueValidate(JSONRegexOption.IP_ADDR_REGEX, map, key, "您输入的IP不符合规范");
//url校验
valueValidate(JSONRegexOption.URL_REGEX, map, key, "您输入的URL不符合规范");
}
}
}
}
/**
* 正则校验
*
* @param regex 正则
* @param param 需要校验的值
* @return 校验结果
*/
public boolean regexValidate(String regex, String param) {
Pattern pattern = Pattern.compile(regex);
return param.matches(regex);
}
/**
* @param regexOption 校验类型
* @param map 数据集
* @param key 校验的key
* @param msg 如果出错返回的信息
*/
public void valueValidate(JSONRegexOption regexOption, LinkedHashMap map, Object key, String msg) throws Exception {
//密码校验
if (regexOption.getKey().equals(key.toString())) {
//根据key获取值
String value = map.get(key).toString();
//值校验
MyAssert.assertMethod(regexValidate(regexOption.getRegex(), value), msg);
}
}
}
对Map类型参数校验的优化
对于Map类型参数的校验还有优化的办法,能够解决key的硬编码问题。想到了一种解决思路,稍后可以尝试一下。
思路
- 创建一个注解加在方法的参数上,其可以指定一个或一组Entity类的全路径。
- 在Aspect中通过获取此注解获取所有Entity。
- 再使用反射来获取这些Entity中加入注解的属性。
- 通过属性名(匹配key)属性上注解的实例(匹配校验规则)
- 从而实现全局值校验。
对于普通类型(包括包装类型)的优化
对于普通类型(包括包装类型),可以编写一些单独的校验注解。当参数上增加了这些注解,就进行相关的校验。
对于List、Set、List
经过上面的一些解决方案,其实写出这样的校验已经不是什么难题,只需要在Aspect中进行相关的判断即可,具体的实现大家可以多尝试哈~~
有什么更好的解决方案欢迎留言一起交流
来自一小时后的更新。。。。
完善Map类型的校验~
对于上述的想法立马进行了实践,完善了对Map类型参数的校验,再说一遍思路:
首先要在map参数前加上一个自定义注解,此注解只有一个属性,用于声明此map中要校验的数据来自哪些实体类。(实体类需要指定全类名,因为要对其进行反射)
package cn.rayfoo.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/8 19:50
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface VerifyEntity {
/**
* 实体类全类名列表
*/
String[] baseEntityList();
}
在方法上加上注解:
@PutMapping("/updatePhone")
public Result<Object> updatePhone(@RequestBody @VerifyEntity(baseEntityList = {"cn.rayfoo.modules.base.entity.User"}) Map<String, Object> record) {
return null;
}
校验Aspect代码:
package cn.rayfoo.common.aspect;
import cn.hutool.core.util.ArrayUtil;
import cn.rayfoo.common.annotation.Verify;
import cn.rayfoo.common.annotation.VerifyEntity;
import cn.rayfoo.common.exception.MyAssert;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>Controller中的JSON全局参数校验</p>
* @date 2020/8/7 14:03
*/
@Aspect
@Component
@Slf4j
public class JsonValidatorAspectPlus {
/**
* 校验的类型
*/
private static final String LINK_HASH_MAP_TYPE = "java.util.LinkedHashMap";
/**
* 定义一个方法,用于声明切入表达式。
*/
@Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
public void validatorPointcut() {
}
@Before("validatorPointcut()")
public void parameterVerify(JoinPoint point) throws Exception {
//获取参数列表
Object[] args = point.getArgs();
//通过签名 获取方法签名
MethodSignature signature = (MethodSignature) point.getSignature();
//通过方法签名获取执行方法
Method method = signature.getMethod();
//获取参数上的所有注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//获取参数列表
Parameter[] parameters = method.getParameters();
//拆分方法,提高阅读性
isVerifyEntity(parameterAnnotations, args);
}
/**
* 判断是否加了@VerifyEntity注解 加了再进行下一步的操作
* @param parameterAnnotations 所有参数前的注解列表
* @param args 所有的参数列表
*/
private void isVerifyEntity(Annotation[][] parameterAnnotations, Object[] args) throws Exception {
//判断是否加了VerifyEntity注解
for (Annotation[] parameterAnnotation : parameterAnnotations) {
//获取当前参数的位置
int index = ArrayUtil.indexOf(parameterAnnotations, parameterAnnotation);
for (Annotation annotation : parameterAnnotation) {
//获取注解的全类名
String verifyEntityName = VerifyEntity.class.getName();
//获取当前注解的全类名
String name = annotation.annotationType().getName();
//匹配是否相同
if (verifyEntityName.equals(name)) {
//获取此注解修饰的具体的参数
Object param = args[index];
//如果存在此注解,执行方法
isLinkedHashMap(annotation,param);
}
}
}
}
/**
* 判断是否为LinkedHashMap,如果是,进行进一步的操作
* @param annotation 参数上的注解
* @param param 注解所修饰的参数
*/
private void isLinkedHashMap(Annotation annotation,Object param) throws Exception {
//获取注解
VerifyEntity verifyEntity = (VerifyEntity) annotation;
//获取要校验的所有entity
String[] entitys = verifyEntity.baseEntityList();
//如果是map接收参数
if (LINK_HASH_MAP_TYPE.equals(param.getClass().getName())) {
//如果存在Verify注解
hasVerify(entitys, param);
}
}
/**
* 如果EntityList中的实体存在Verify注解
* @param entityList 实体列表
* @param param 加入@verifyEntity的注解 的参数
*/
private void hasVerify(String[] entityList, Object param) throws Exception {
//迭代entityList
for (int i = 0; i < entityList.length; i++) {
Field[] fields = Class.forName(entityList[i]).getDeclaredFields();
//迭代字段
for (Field field : fields) {
//判断是否加入了Verify注解
if (field.isAnnotationPresent(Verify.class)) {
//如果有 获取注解的实例
Verify verify = field.getAnnotation(Verify.class);
//校验
validateMap(param, verify, field.getName());
}
}
}
}
/**
* 真正进行校验的类
* @param param 增加@VerifyEntity注解的参数
* @param verify Verify注解的实例
* @param fieldName 加了Verify的属性name值
*/
public void validateMap(Object param, Verify verify, String fieldName) throws Exception {
//获取集合
LinkedHashMap map = (LinkedHashMap) param;
//获取key列表
Set set = map.keySet();
//迭代key
for (Object key : set) {
//如果key和注解的fieldName一致
if (fieldName.equals(key)) {
//当前值
Object fieldObj = map.get(key);
//获取verify的name
String name = verify.name();
//是否时必传 断言判断
if (verify.required()) {
MyAssert.assertMethod(fieldObj != null, String.format("【%s】为必传参数", name));
}
//字符串的 非空校验
if (verify.notNull()) {
MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能为空", name));
}
//是否有最大长度限制 断言判断
int maxLength = verify.maxLength();
if (Integer.MAX_VALUE != maxLength) {
MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最大长度为【%s】", name, maxLength));
}
//是否有最小长度限制 断言判断
int minLength = verify.minLength();
if (Integer.MIN_VALUE != minLength) {
MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最小长度为【%s】", name, minLength));
}
//是否有正则校验
if (!"".equals(verify.regular().getRegex())) {
//初始化Pattern
Pattern pattern = Pattern.compile(verify.regular().getRegex());
//断言判断正则
MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("参数【%s】的请求数据不符合规则", name));
}
}
}
}
}
此时,解决了Map和Entity两种常见参数的统一校验~
已经解决了常见的参数校验啦~
再次更新,完成普通类型、map、eitity三种校验的整合
-
增强了@verify对于普通类型参数的支持
-
增加了@RequestEntity、@RequestMap注解
-
可以实现对Map、Entity、普通类型(包括包装类型)的全局校验
-
对原有的多个Aspect进行了整合,JSONRegexOption、EntityValidatorAspect、JsonValidatorAspect都可以Deprecated了
-
具有一定的拓展性,如需增加校验规则,只需要拓展RegexOption即可
废话不多说,直接上代码:
适用于普通参数和属性的检验注解:
package cn.rayfoo.common.annotation;
import cn.rayfoo.common.enums.RegexOption;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:33
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface Verify {
/** 参数名称 */
String name();
/** 参数最大长度 */
int maxLength() default Integer.MAX_VALUE;
/** 是否必填 这里只是判断是否为null */
boolean required() default true;
/** 是否为非空 是否为null和空串都判断 */
boolean notNull() default true;
/** 最小长度 */
int minLength() default Integer.MIN_VALUE;
/** 正则匹配 */
RegexOption regular() default RegexOption.DEFAULT;
}
适用于Controller参数中的Map类型:
package cn.rayfoo.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>对Map</p>
* @date 2020/8/8 19:50
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestMap {
/**
* 实体类全类名列表
*/
String[] baseEntityList();
}
适用于Controller方法中的Entity参数:
package cn.rayfoo.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/8 22:43
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestEntity {
String value() default "";
}
适用于@Verify注解的枚举,如果需要新增校验,可以对此枚举进行拓展:
package cn.rayfoo.common.enums;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:51
*/
public enum RegexOption {
/**
* 缺省,表示不进行正则校验
*/
DEFAULT(""),
/**
* 邮箱正则
*/
EMAIL_REGEX("^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$"),
/**
* 手机号正则
*/
PHONE_NUMBER_REGEX("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\d{8}$"),
/**
* 身份证正则
*/
IDENTITY_CARD_REGEX("(^\d{18}$)|(^\d{15}$)"),
/**
* URL正则
*/
URL_REGEX("http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"),
/**
* IP地址正则
*/
IP_ADDR_REGEX("(25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]?\d)"),
/**
* 用户名正则
*/
USERNAME_REGEX("^[a-zA-Z]\w{5,20}$"),
/**
* 密码正则
*/
PASSWORD_REGEX("^[a-zA-Z0-9]{6,20}$");
/**
* 正则
*/
private String regex;
/**
* 构造方法
*
* @param regex
*/
private RegexOption(String regex) {
this.regex = regex;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
Aspect:
package cn.rayfoo.common.aspect;
import cn.hutool.core.util.ArrayUtil;
import cn.rayfoo.common.annotation.RequestEntity;
import cn.rayfoo.common.annotation.RequestMap;
import cn.rayfoo.common.annotation.Verify;
import cn.rayfoo.common.exception.MyAssert;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>Controller中的JSON全局参数校验</p>
* @date 2020/8/7 14:03
*/
@Aspect
@Component
@Slf4j
public class JsonValidatorAspectPlus {
/**
* 校验的类型
*/
private static final String LINK_HASH_MAP_TYPE = "java.util.LinkedHashMap";
/**
* 定义一个方法,用于声明切入表达式。
*/
@Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
public void validatorPointcut() {
}
@Before("validatorPointcut()")
public void parameterVerify(JoinPoint point) throws Exception {
//通过签名 获取方法签名
MethodSignature signature = (MethodSignature) point.getSignature();
//通过方法签名获取执行方法
Method method = signature.getMethod();
//获取参数上的所有注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//获取参数列表
Object[] args = point.getArgs();
//判断是否加了RequestMap注解
for (Annotation[] parameterAnnotation : parameterAnnotations) {
//获取当前参数的位置
int index = ArrayUtil.indexOf(parameterAnnotations, parameterAnnotation);
for (Annotation annotation : parameterAnnotation) {
//获取此注解修饰的具体的参数
Object param = args[index];
//如果有@RequestEntity注解
hasRequestEntity(annotation, param);
//如果有Verify注解 由于是参数上的注解 注意:此处传递的是具体的param 而非args
hasVerify(annotation, param);
//如果有RequestMap注解 由于是参数上的注解 注意:此处传递的是具体的param 而非args
hasRequestMap(annotation, param);
}
// }
}
}
/**
* 如果参数存在RequestEntity注解
*
* @param annotation 参数上的注解
* @param param 具体的参数
*/
private void hasRequestEntity(Annotation annotation, Object param) throws Exception {
//获取注解的全类名
String requestEntityName = RequestEntity.class.getName();
//获取当前注解的全类名
String name = annotation.annotationType().getName();
//匹配是否相同
if (requestEntityName.equals(name)) {
//获取参数的字节码
Class clazz = param.getClass();
//获取当前参数对应类型的所有属性
Field[] fields = clazz.getDeclaredFields();
//遍历属性
for (Field field : fields) {
//获取私有属性值
field.setAccessible(true);
//需要做校验的参数
if (field.isAnnotationPresent(Verify.class)) {
//获取注解对象
Verify verify = field.getAnnotation(Verify.class);
//校验的对象
Object fieldObj = field.get(param);
//校验
validate(verify, fieldObj);
}
}
}
}
/**
* 如果参数上加的是Verify注解
*
* @param annotation 参数上的注解
* @param param 参数
*/
private void hasVerify(Annotation annotation, Object param) throws Exception {
//获取注解的全类名
String verifyName = Verify.class.getName();
//获取当前注解的全类名
String name = annotation.annotationType().getName();
//匹配是否相同
if (verifyName.equals(name)) {
//获取此注解修饰的具体的参数
//获取当前注解的具值
Verify verify = (Verify) annotation;
//进行校验
validate(verify, param);
}
}
/**
* 判断是否加了@RequestMap注解 加了再进行下一步的操作
*
* @param annotation 所有参数前的注解
* @param param 当前参数
*/
private void hasRequestMap(Annotation annotation, Object param) throws Exception {
//获取注解的全类名
String RequestMapName = RequestMap.class.getName();
//获取当前注解的全类名
String name = annotation.annotationType().getName();
//匹配是否相同
if (RequestMapName.equals(name)) {
//如果存在此注解,执行方法
isLinkedHashMap(annotation, param);
}
}
/**
* 判断是否为LinkedHashMap,如果是,进行进一步的操作
*
* @param annotation 参数上的注解
* @param param 注解所修饰的参数
*/
private void isLinkedHashMap(Annotation annotation, Object param) throws Exception {
//获取注解
RequestMap RequestMap = (RequestMap) annotation;
//获取要校验的所有entity
String[] entitys = RequestMap.baseEntityList();
//如果是map接收参数
if (LINK_HASH_MAP_TYPE.equals(param.getClass().getName())) {
//如果存在Verify注解
hasVerify(entitys, param);
}
}
/**
* 如果EntityList中的实体存在Verify注解
*
* @param entityList 实体列表
* @param param 加入@RequestMap的注解 的参数
*/
private void hasVerify(String[] entityList, Object param) throws Exception {
//迭代entityList
for (int i = 0; i < entityList.length; i++) {
//获取所有字段
Field[] fields = Class.forName(entityList[i]).getDeclaredFields();
//迭代字段
for (Field field : fields) {
field.setAccessible(true);
//判断是否加入了Verify注解
if (field.isAnnotationPresent(Verify.class)) {
//如果有 获取注解的实例
Verify verify = field.getAnnotation(Verify.class);
//校验
fieldIsNeedValidate(param, verify, field.getName());
}
}
}
}
/**
* 字段是否需要校验
*
* @param param 增加@RequestMap注解的参数
* @param verify Verify注解的实例
* @param fieldName 加了Verify的属性name值
*/
private void fieldIsNeedValidate(Object param, Verify verify, String fieldName) throws Exception {
//获取集合
LinkedHashMap map = (LinkedHashMap) param;
//获取key列表
Set set = map.keySet();
//迭代key
for (Object key : set) {
//如果key和注解的fieldName一致
if (fieldName.equals(key)) {
//当前值
Object fieldObj = map.get(key);
//真正的进行校验
validate(verify, fieldObj);
}
}
}
/**
* 正则的校验方法
*
* @param verify 校验规则
* @param fieldObj 校验者
*/
private void validate(Verify verify, Object fieldObj) throws Exception {
//获取verify的name
String name = verify.name();
//是否时必传 断言判断
if (verify.required()) {
MyAssert.assertMethod(fieldObj != null, String.format("【%s】为必传参数", name));
}
//字符串的 非空校验
if (verify.notNull()) {
MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能为空", name));
}
//是否有最大长度限制 断言判断
int maxLength = verify.maxLength();
if (Integer.MAX_VALUE != maxLength) {
MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最大长度为【%s】", name, maxLength));
}
//是否有最小长度限制 断言判断
int minLength = verify.minLength();
if (Integer.MIN_VALUE != minLength) {
MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最小长度为【%s】", name, minLength));
}
//是否有正则校验
if (!"".equals(verify.regular().getRegex())) {
//初始化Pattern
Pattern pattern = Pattern.compile(verify.regular().getRegex());
//断言判断正则
MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("参数【%s】的请求数据不符合规则", name));
}
}
}
在controller类中使用上述注解:
@PutMapping("/updatePhone")
public Result<Object> updatePhone(@RequestBody @RequestMap(baseEntityList = {"cn.rayfoo.modules.base.entity.User"}) Map<String, Object> record) {
return null;
}
@PostMapping("/test")
public Result<Object> test(@RequestBody @RequestEntity User user) {
return Result.builder().msg("ok").code(200).data("success").build();
}
@GetMapping("/username")
public Result<Object> usernameTest(@Verify(name = "用户名",regular = RegexOption.USERNAME_REGEX) String username) {
return Result.builder().msg("ok").code(200).data("success").build();
}
对于组合Entity、List这类的数据还需要继续优化,目前已经有一些头绪。后续可能还会更新
思路:
对于组合Entity可以在@Verify增加一个属性 修饰是否该属性是一个Entity,进行递归式判断
对于List可以先迭代list,再在list中的每个Object再进行反射判断