前言
在项目练习中。对前端传递的参数后端会进行一个校验,一般情况下都是使用if对参数进行校验太麻烦,重复的代码太多,这个时候我们就需要使用aop。
在Spring Boot中,参数校验通常使用javax.validation.constraints包中的注解,这里我们就使用aop进行一个参数校验
一、导入aop的包到项目中
在项目中导入一个aop的包,版本根据你的sprigboot版本来
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
二、aop实现
在项目目录中新建一个aspect包,在后在这个包文件夹下新建一个切面类,切面类的名字可以自定义,我这边的名字就叫做OperationAspect
我们需要将创建的类定义成一个切面类,并且需要将其交给spring管理,我们需要在这个类上添加一些注解来实现 @Aspect @Component
@Aspect
@Component("operationAspect")
public class OperationAspect {
}
我们需要设置切入点,来告诉程序我们从哪个地方进行切入,就好比一个西瓜横着还是竖着切,需要告诉程序,切入点设置完成后,我们使用自定义注解的形式来使用
@Pointcut("@annotation()")
private void pointcut() {
}
创建一个annotation的包,在里面定义一个annotation的类,之后使用注解来定义切入点,这个自定义注解还需要设置俩个注解,一个用来告诉程序在什么地方使用,一个什么时候运行,在里面自定义我们的方法和默认参数
@Target({ElementType.METHOD}),@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GlobalInterceptor {
// 是否校验参数
boolean checkParams() default true;
}
将这个注解的包名和方法添加到我上面切入点空缺的地方
@Aspect
@Component("operationAspect")
public class OperationAspect {
@Pointcut("@annotation(com.easyjob.annotation.GlobalInterceptor)")
private void pointcut() {
}
}
这样我们的一个aop就创建成功了,在需要使用的地方加上我们自定义的注解就可以了,接下来我们就可以来写aop需要处理的逻辑了
三、 实现aop参数校验
我们使用前置通知来实现一些我们需要实现的功能,我们通过joinPoint来获取,我们的方法名和方法上的其他参数,在通过获取的method来拿到方法上的注解,在通过判断注解上checkParams是否是需要校验,通过定义的validateParams来校验参数,这个方法传递俩个参数,我们获取的method,和方法上面的参数arguments
private Logger logger = LoggerFactory.getLogger(OperationAspect.class);
// 前置通知
@Before("@annotation(com.easyjob.annotation.GlobalInterceptor)")
public void interceptorDo(JoinPoint joinPoint) {
// 获取参数
Object[] arguments = joinPoint.getArgs();
//TODO 使用反射来获取方法的参数名和其他信息 例如方法名、参数和返回值等
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取方法上的注解
GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
if (interceptor == null) {
return;
}
// 参数校验
if (interceptor.checkParams()) {
validateParams(method, arguments);
}
}
// 参数校验
private void validateParams(Method method, Object[] arguments) {
}
我们在定义一个VerifyParams注解来校验数据的最小最大长度和是否需要正则校验
如果需要其他条件可以自行添加,这里我们的正则校验使用的是一个枚举类,这个类里面定义了一些正则规则,如果需要其他的可以自行查找添加
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyParams {
// 最小长度
int min() default -1;
// 最大长度
int max() default -1;
// 是否必填
boolean required() default false;
// 校验正则
VerifyRegexEnum regex() default VerifyRegexEnum.NO;
}
正则枚举类
public enum VerifyRegexEnum {
NO("", "不校验"),
IP("([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])(\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])){3}", "IP地址"),
POSITIVE_INTEGER("^[0-9]*[1-9][0-9]*$", "正整数"),
NUMBER_LETTER_UNDER_LINE("^\w+$", "由数字、26个英文字母或者下划线组成的字符串"),
EMAIL("^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$", "邮箱"),
PHONE("(1[0-9])\d{9}$", "手机号码"),
COMMON("^[a-zA-Z0-9_\u4e00-\u9fa5]+$", "数字,字母,中文,下划线"),
PASSWORD("^(?=.*\d)(?=.*[a-zA-Z])[\da-zA-Z~!@#$%^&*_]{8,}$", "只能是数字,字母,特殊字符 8-18位"),
ACCOUNT("^[0-9a-zA-Z_]{1,}$", "字母开头,由数字、英文字母或者下划线组成"),
MONEY("^[0-9]+(.[0-9]{1,2})?$", "金额");
private String regex;
private String desc;
VerifyRegexEnum(String regex, String desc) {
this.regex = regex;
this.desc = desc;
}
public String getRegex() {
return regex;
}
public String getDesc() {
return desc;
}
}
编写回到我们定义的validateParams方法里面来编写代码
private void validateParams(Method method, Object[] arguments) {
//method.getParameters() 它提供了关于参数的类型、名称和其他属性的信息。类型是一个 Parameter[]数组
Parameter[] parameters = method.getParameters();
// 获取方法上的参数
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
//这个对象的值跟我们的方法参数是一样的
Object value = arguments[i];
// 获取参数上的注解
VerifyParams verifyParams = parameter.getAnnotation(VerifyParams.class);
if(verifyParams==null){
continue;
}
}
}
Parameter[] parameters = method.getParameters();这个地方获取我们的参数名称,循环遍历出我们的参数 这个地方的parameters跟我们的arguments是一一对应的
校验的过程中,我们要根据类型来进行一个校验,如果传递的是一个对象要怎么校验如果是基础数据类型就直接校验,定义基本数据类型
添加基础数据类型,判断是否是对象,对象有对象的判断方法
private static final String[] TYPE_BASE = {"java.lang.String", "java.lang.Integer", "java.lang.Long"};
我们将处理参数的逻辑在单独抽出,在这个方法中处理校验参数的逻辑,让一个方法里面的代码尽量简洁,看起来
也舒服
新建一个方法,这个方法用来真正处理校验字段的逻辑,这里我们还需要校验正则方法,使用我们使用了一个正则校验的工具类VerifyUtils,有了这个工具类后我们就可以开始校验正则了
public class VerifyUtils {
/**
* 验证字符串是否符合指定的正则表达式
*
* @param regs 正则表达式
* @param value 待验证的字符串
* @return 如果字符串符合正则表达式,返回 true,否则返回 false
*/
public static boolean verify(String regs, String value) {
// 如果值为空,则返回 false
if (StringTools.isEmpty(value)) {
return false;
}
// 编译正则表达式
Pattern pattern = Pattern.compile(regs);
// 创建匹配器
Matcher matcher = pattern.matcher(value);
// 返回匹配结果
return matcher.matches();
}
/**
* 验证字符串是否符合指定的 VerifyRegexEnum 枚举类型中的正则表达式
*
* @param regs 正则表达式枚举
* @param value 待验证的字符串
* @return 如果字符串符合正则表达式,返回 true,否则返回 false
*/
public static boolean verify(VerifyRegexEnum regs, String value) {
// 调用 verify(String regs, String value) 方法进行验证
return verify(regs.getRegex(), value);
}
}
在代码中添加好校验正则的代码,如果有误抛出异常提醒
/**
* 校验正则
*/
if (!isEmpty && !StringTools.isEmpty(verifyParams.regex().getRegex()) && VerifyUtils.verify(verifyParams.regex(), String.valueOf(value))) {
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
这样我们的基本数据类型校验就已经可以实现了!在要校验的方法参数前加上我们的@VerifyParams注解就可以了!
接下来我们实现如何根据对象类型进行一个校验
定义一个checkObjValue方法我们在这个方法里面编写校验对象参数的逻辑,这里需要使用反射获取到对象之后根据获取到的对象来进行处理
private void checkObjValue(Parameter parameter, Object obj) {
/**
* 1 、获取参数类型 传递的是对象 例如 com.xxx.entity.po.SysAccount
*/
String typeName = parameter.getParameterizedType().getTypeName();
try {
/**
* 2、 根据反射获取类和类的字段
* Class.forName 是一个静态方法,用于根据类的完全限定名(包括包名)获取 Class 对象。
*/
Class classz = Class.forName(typeName);
Field[] fields = classz.getDeclaredFields();
/**
* 3. 遍历反射获取到的字段
*/
for (Field field : fields) {
// 3.1 拿到对象字段上的注解
VerifyParams fieldVerifyParams = field.getAnnotation(VerifyParams.class);
if (fieldVerifyParams == null) {
continue;
}
/**
* 3.2 设置字段可访问
*/
field.setAccessible(true);
/**
* 4. 获取字段的值
*/
Object resultValue = field.get(obj);
// 5. 校验
checkValue(resultValue, fieldVerifyParams);
}
} catch (Exception e) {
logger.error("校验参数失败", e);
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
}
到这一步已经完成了,可以进行一个参数校验了。
四 完整代码
@Aspect
@Component("operationAspect")
public class OperationAspect {
// @Pointcut("@annotation(com.easyjob.annotation.GlobalInterceptor)")
// private void pointcut() {
//
// }
// 前置通知
// @Before("pointcut()")
// public void interceptorDo(JoinPoint joinPoint) {
//
// }
private Logger logger = LoggerFactory.getLogger(OperationAspect.class);
// 基本数据类型
private static final String[] TYPE_BASE = {"java.lang.String", "java.lang.Integer", "java.lang.Long"};
// private static final String TYPE_STRING = "java.lang.String";
// private static final String TYPE_INTEGER = "java.lang.Integer";
// private static final String TYPE_LONG = "java.lang.Long";
// 前置通知
@Before("@annotation(com.easyjob.annotation.GlobalInterceptor)")
public void interceptorDo(JoinPoint joinPoint) {
// 获取参数
Object[] arguments = joinPoint.getArgs();
//TODO 使用反射来获取方法的参数名和其他信息 例如方法名、参数和返回值等
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取方法上的注解
GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
if (interceptor == null) {
return;
}
// 参数校验
if (interceptor.checkParams()) {
validateParams(method, arguments);
}
}
// 参数校验
private void validateParams(Method method, Object[] arguments) {
//method.getParameters() 它提供了关于参数的类型、名称和其他属性的信息。类型是一个 Parameter[]数组
Parameter[] parameters = method.getParameters();
// 获取方法上的参数
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
// 这个对象的值跟我们的方法参数是一样的(就是我们传递来的参数值)
Object value = arguments[i];
// 获取参数上的注解
VerifyParams verifyParams = parameter.getAnnotation(VerifyParams.class);
if (verifyParams == null) {
continue;
}
/**
* 获取参数类型: 例如 java.lang.String
* parameter.getParameterizedType().getTypeName();
*
*/
String paramTypeName = parameter.getParameterizedType().getTypeName();
// 判断参数是否为基本数据类型
if (ArrayUtils.contains(TYPE_BASE, paramTypeName)) {
checkValue(value, verifyParams);
} else {
checkObjValue(parameter, verifyParams);
}
}
}
/**
* 校验对象参数
*/
private void checkObjValue(Parameter parameter, Object obj) {
/**
* 1 、获取参数类型 传递的是对象 例如 com.xxx.entity.po.SysAccount
*/
String typeName = parameter.getParameterizedType().getTypeName();
try {
/**
* 2、 根据反射获取类和类的字段
* Class.forName 是一个静态方法,用于根据类的完全限定名(包括包名)获取 Class 对象。
*/
Class classz = Class.forName(typeName);
Field[] fields = classz.getDeclaredFields();
/**
* 3. 遍历反射获取到的字段
*/
for (Field field : fields) {
// 3.1 拿到对象字段上的注解
VerifyParams fieldVerifyParams = field.getAnnotation(VerifyParams.class);
if (fieldVerifyParams == null) {
continue;
}
/**
* 3.2 设置字段可访问
*/
field.setAccessible(true);
/**
* 4. 获取字段的值也就是 例如:18666666666
*/
Object resultValue = field.get(obj);
// 5. 校验
checkValue(resultValue, fieldVerifyParams);
}
} catch (Exception e) {
logger.error("校验参数失败", e);
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
}
/**
* 开始校验基本参数
*
* @param value 方法传递的校验参数
* @param verifyParams 这个的作用是用来区分是否是必须校验的,或者是使用了什么正则
*/
private void checkValue(Object value, VerifyParams verifyParams) {
//是否是空
Boolean isEmpty = value == null || StringTools.isEmpty(value.toString());
//长度
Integer length = value == null ? 0 : value.toString().length();
/**
* 校验空
*/
if (isEmpty && verifyParams.required()) {
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
/**
* 校验长度 作用:如果值不为空,并且其长度不在指定的最大和最小长度范围内,就抛出一个业务异常
*/
if (!isEmpty &&
(verifyParams.max() != -1 && verifyParams.max() < length || verifyParams.min() != -1 &&
verifyParams.min() > length)
) {
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
/**
* 校验正则
*/
if (!isEmpty &&
!StringTools.isEmpty(verifyParams.regex().getRegex())
&& VerifyUtils.verify(verifyParams.regex(), String.valueOf(value))
) {
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
}
}
作者:想努力找到前端实习的呆呆鸟
链接:https://juejin.cn