目录
一、HTTP请求进入Controller(发送邮件验证码)
1. 切面拦截(GlobalOperactionAspect.class)
3. 参数校验入口(valiadateParams方法)
三. 对象类型递归校验(checkObValue方法)[这段代码没有调用这个方法,下面有解释]
利用AOP实现参数拦截:
一、HTTP请求进入Controller(发送邮件验证码)
@RequestMapping("/sendEmailCode")
@GloballInterceptor(checkParams=true) // 触发AOP拦截
public ResponseVO sendEmailCode(HttpSession session,
@VerifyParam(required = true) String email,
@VerifyParam(required = true) String checkCode,
@VerifyParam(required = true) Integer type){
// 实际方法体执行前会先被AOP拦截
}
二、AOP切面触发
0.首先我们先定义一个切点方法
@Pointcut("@annotation(com.cjl.annotation.GloballInterceptor)")// 表示下面方法为切点方法
private void requestIntercector() {
System.out.println("请求拦截");
}
- 切点(
@Pointcut
):定义“在哪里拦截”,不包含逻辑。 - 增强(
@Before
/@Around
):定义“拦截后做什么”,需引用切点。 - 注解匹配:通过
@annotation
实现声明式拦截,避免硬编码。
1. 切面拦截(GlobalOperactionAspect.class)
这是一个全局拦截器
@Before("requestIntercector()") // 声明在切点方法执行前运行
public void interceptor(JoinPoint point) throws BusinessException {
try {
// 1. 获取目标方法元信息
Object target = point.getTarget(); // 获取被代理的目标对象实例
Object[] arguments = point.getArgs(); // 获取方法调用参数值数组
String methodName = point.getSignature().getName(); // 获取目标方法名称
// 2. 通过方法签名获取精确的方法定义(考虑方法重载情况)
MethodSignature signature = (MethodSignature) point.getSignature();
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
Method method = target.getClass().getMethod(methodName, parameterTypes);
// 3. 检查方法上的拦截器注解
GloballInterceptor interceptor = method.getAnnotation(GloballInterceptor.class);
if (null == interceptor) {
return; // 无拦截注解则直接放行
}
// 4. 执行参数校验(根据注解配置决定)
if (interceptor.checkParams()) {
valiadateParams(method, arguments); // 进入核心校验流程
}
} catch (BusinessException e) {
// 业务级异常处理(如参数校验失败)
log.error("全局拦截器拦截到业务异常", e);
throw e; // 原样抛出保持异常链
} catch (Exception e) {
// 系统级异常处理(如反射异常)
log.error("全局拦截器发生系统异常", e);
throw new BusinessException("系统繁忙,请稍后重试");
} catch (Throwable e) {
// 兜底处理所有错误(包括Error)
log.error("全局拦截器捕获不可预期错误", e);
throw new BusinessException("系统服务不可用");
}
}
method.getAnnotation()
- 作用:通过反射获取方法上的
@GloballInterceptor
注解实例
null == interceptor
判断
- 条件成立:表示该方法没有标注
@GloballInterceptor
- 执行逻辑:直接退出拦截器,不进行任何校验,让方法正常执行
2.参数校验注解
@Retention(RetentionPolicy.RUNTIME)// 表示注解在运行时存在
@Target({ElementType.PARAMETER,ElementType.FIELD})// 表示该注解用于方法参数和字段上
public @interface VerifyParam {
int min() default -1;
int max() default -1;
boolean required() default true; // 是否必传
//正则校验
VerifyRegexEnum regex() default VerifyRegexEnum.NO;//默认不校验
}
3. 参数校验入口(valiadateParams方法)
private void valiadateParams(Method method, Object[] arguments) throws BusinessException {
// 获取方法的所有参数定义
Parameter[] parameters = method.getParameters();
// 遍历每个参数
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
Object value = arguments[i];
// 获取参数上的校验注解
VerifyParam verifyParam = parameter.getAnnotation(VerifyParam.class);
if (null == verifyParam) {
continue; // 无校验注解则跳过
}
// 基本数据类型校验(String/Long/Integer)
if (TyPE_STRING.equals(parameter.getType().getName())
|| TyPE_LONG.equals(parameter.getType().getName())
|| TyPE_INTEGER.equals(parameter.getType().getName())) {
checkValue(value, verifyParam);
}
// 对象类型校验
else {
checkObValue(parameter, value);
}
}
}
三. 对象类型递归校验(checkObValue方法)[这段代码没有调用这个方法,下面有解释]
private void checkObValue(Parameter parameter, Object value) throws BusinessException {
try {
// 1. 获取参数的实际类型(支持泛型)
String typeName = parameter.getParameterizedType().getTypeName();
Class<?> classz = Class.forName(typeName); // 加载类定义
// 2. 遍历对象的所有字段
Field[] fields = classz.getDeclaredFields();
for (Field field : fields) {
// 3. 检查字段上的校验注解
VerifyParam fieldVerifyParam = field.getAnnotation(VerifyParam.class);
if (fieldVerifyParam == null) {
continue; // 无注解字段跳过
}
// 4. 获取字段值(突破private限制)
field.setAccessible(true);
Object resultValue = field.get(value);
// 5. 递归校验字段值
checkValue(resultValue, fieldVerifyParam); // 调用基础校验方法
}
} catch (BusinessException e) {
// 透传业务校验异常
log.error("对象参数校验业务异常", e);
throw e;
} catch (Exception e) {
// 处理反射等系统异常
log.error("对象参数校验系统异常", e);
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
}
四. 基础类型校验(checkValue方法)
private void checkValue(Object value, VerifyParam verifyParam) throws BusinessException {
/**
* 空值检测准备
*/
Boolean isEmpty = value==null || StringTools.isEmpty(value.toString());
Integer length = value==null?null:value.toString().length();
/**
* 校验空
*/
if(isEmpty && verifyParam.required()){
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
/**
* 校验长度
*/
if(!isEmpty && (verifyParam.max() != -1&& verifyParam.max()< length || verifyParam.min() != -1 && verifyParam.min()>length)){
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
/**
* 校验正则
*/
if(!isEmpty && !StringTools.isEmpty(verifyParam.regex().getRegex())&& !VerifyUtils.verify(verifyParam.regex(),String.valueOf(value))){
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
}
五、工具类调用
1.空值判断(StringTools)
public static boolean verify(VerifyRegexEnum regex, String value) {
// 调用重载方法(图片中定义的EMAIL/PASSWORD规则在此生效)
return verify(regex.getRegex(), value); // ← 使用枚举中的正则表达式
}
private static boolean verify(String regex, String value) {
Pattern p = Pattern.compile(regex); // ← 编译正则表达式(如EMAIL的复杂规则)
return p.matcher(value).matches(); // ← 执行实际匹配
}
2.正则校验(VerifyUtils)
// 图片中定义的枚举校验规则
public enum VerifyRegexEnum {
EMAIL("^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$", "邮箱格式错误"),
PASSWORD("^(?=.*\\d)(?=.*[a-zA-Z]).{8,18}$", "需包含字母和数字,8-18位");
// 当@VerifyParam(regex=VerifyRegexEnum.EMAIL)时:
// 实际使用的正则表达式就是EMAIL枚举实例中的regex值
}
六、流程图
七、典型校验失败场景示例
空值校验失败
- 调用:
sendEmailCode(null, "123", 0)
- 抛出:
BusinessException("参数不能为空")
- 调用:
邮箱格式错误
- 调用:
sendEmailCode("invalid#email", "123", 0)
- 匹配:EMAIL正则表达式失败
- 抛出:
BusinessException("邮箱格式错误")
(消息来自枚举的desc字段)
- 调用:
八、设计亮点解析
双校验层设计
- 前端:基础非空校验(快速反馈)
- 后端:AOP+正则深度校验(安全兜底)
规则集中管理
// 修改密码规则只需调整枚举即可全局生效 PASSWORD("^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,20}$", "需包含大小写字母和数字")
递归对象校验
支持对嵌套对象的字段级校验:public void update(@VerifyParam UserDTO user) { // 会自动校验UserDTO中所有带@VerifyParam的字段 }
总结流程
🔀 参数校验流程全链路总结:
HTTP请求 → @GloballInterceptor触发AOP
→ 解析@VerifyParam注解
→ 分发校验(基本类型→checkValue() / 对象类型→递归checkObValue())
→ 调用StringTools/VerifyRegexEnum工具
→ 校验失败抛BusinessException
→ 校验通过执行业务逻
核心箭头图:
Controller
→ 🌐 AOP切面
→ 🔍 参数扫描
→ ✂️ 规则匹配
→ ✅/❌ 校验结果
→ ⚡ 业务逻辑/异常返回
疑问:
1.为什么遍历对象所有带@VerifyParam注解的字段进行校验为什么是递归?
答:遍历对象字段校验”本身不是递归,但当字段本身又是对象时,需要再次触发相同的校验流程,这才是递归的本质
如果变量是普通字段(checkvalue(user对象)),会采取普通字段校验,比如:
// 简单User对象(无嵌套)
public class User {
@VerifyParam String name; // 基本类型字段
@VerifyParam Integer age; // 基本类型字段
}
如果变量是嵌套对象字段->触发递归,比如:
// 嵌套的Order对象,里面有User
public class Order {
@VerifyParam User user; // 对象类型字段!
}
校验流程:
checkObValue(Order对象)
→ 发现user
字段是对象类型- 递归调用
checkObValue(user)
→ 进入User类的字段校验 - 如果
User
类中还有对象字段,继续向下递归
🔁 这才是真正的递归调用链!
2.@Pointcut切点和@GloballInterceptor是什么关系?
🔍 准确关系说明:
@Pointcut
和 @GloballInterceptor
是观察者与被观察者的关系,具体流程如下:
你在方法上标注
@GloballInterceptor
→ 声明"这个方法需要被拦截@Pointcut
像扫描仪一样持续监控所有方法通过表达式
@annotation(com.cjl.annotation.GloballInterceptor)
专门寻找带该注解的方法// 切面内部工作原理
if (当前方法有@GloballInterceptor注解) {
执行@Before/...增强逻辑(interceptor方法);
}
3.这个JoinPoint point参数传进来的是什么?
@Before("requestIntercector()")
public void interceptorDO(JoinPoint point) throws BusinessException {
...
}
解析一下:
@Before("requestIntercector()")
public void interceptorDO(JoinPoint point) {
// 1. 获取目标方法实例
Method method = ((MethodSignature) point.getSignature()).getMethod();
// 输出:public com.cjl.vo.ResponseVO com.cjl.controller.SessionController.sendEmailCode(...)
// 2. 获取方法参数值数组(按声明顺序)
Object[] args = point.getArgs();
// 内容:[HttpSession实例, "user@example.com", "A7b9", 1]
// 3. 获取具体参数
HttpSession session = (HttpSession) args[0]; // 第一个参数
String email = (String) args[1]; // 第二个参数
String checkCode = (String) args[2]; // 第三个参数
Integer type = (Integer) args[3]; // 第四个参数
// 4. 获取注解配置
VerifyParam emailVerify = method.getParameters()[1].getAnnotation(VerifyParam.class);
// 获取email参数的@VerifyParam(required=true)注解
}
4.它怎么知道我要不要拦截检验