Spring 框架实现“统一逻辑处理”的两种核心方式
Spring 框架实现“统一逻辑处理”的两种核心方式
一、定义
Spring 框架实现“统一逻辑处理”的两种核心方式:Spring AOP(面向切面编程) 和 Spring MVC 拦截器(Interceptor)。
它们都是为了 提高代码复用、降低耦合度 的思想体现,但应用场景和实现机制有所不同。
- Spring AOP:非侵入式编程思想,允许将“横切关注点”(如日志记录、性能监控、事务管理、权限校验)从核心业务逻辑中分离出来,封装到独立的切面模块中。
- Spring MVC 拦截器:专门用于拦截 HTTP 请求,就像 Web 层的“门卫”。能在请求到达处理器(Controller)的 前、中、后 三个阶段插入自定义逻辑。
二、Spring AOP 范式:设计与实现(请求日志)
1. 核心概念
| 概念 | 说明 |
|---|---|
| 切面(Aspect) | 由 @Aspect + @Component 标记的类,存放横切逻辑。 |
| 切入点(PointCut) | 由 @Pointcut 定义,规定“在哪些方法上”进行拦截。 |
| 通知(Advice) | 描述“何时”和“做什么”,如 @Before、@AfterReturning、@AfterThrowing、@Around 等。 |
在请求日志场景中,首选的Advice是
@Around(环绕通知),因为它可以同时包裹方法执行的前、后、异常阶段,信息最全面。
2. 实现步骤
2.1 引入依赖
pom.xml 中引入 AOP starter(Spring Boot 项目):
▼xml复制代码<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2.2 定义切面
▼java复制代码@Aspect @Component public class RequestLogAspect { // 切入点与通知 }
2.3 定义切入点(切入点表达式)
▼java复制代码// 匹配 controller 包及其子包下所有类的所有方法 @Pointcut("execution(* com.example.demo.controller..*.*(..))") public void controllerLayer() {}
常用切入点表达式速查:
execution(* com.xxx.controller..*.*(..)):基于方法签名,最常用@annotation(com.xxx.Loggable):匹配带有某个自定义注解的方法(最灵活,常用来做日志开关)within(com.xxx.service.UserService):匹配某个类内的所有方法args(java.lang.String, ..):匹配第一个参数为 String 的方法bean(userService):匹配指定 bean 名称的所有方法(Spring AOP 独有)
通配符说明:
*匹配任意字符,..匹配包路径或参数中的零个或多个。
2.4 编写环绕通知
▼java复制代码@Around("controllerLayer()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); // 获取执行信息(类名、方法名、参数等) String methodName = joinPoint.getSignature().toShortString(); Object[] args = joinPoint.getArgs(); System.out.println("Start: " + methodName + ", args: " + Arrays.toString(args)); Object result = null; try { // 核心:这行代码真正执行被拦截的方法 result = joinPoint.proceed(); return result; } catch (Exception e) { // 方法抛出异常时记录 System.out.println("Error: " + methodName + " -> " + e.getMessage()); throw e; // 千万别吞异常,要原样抛出,不影响全局异常处理 } finally { long endTime = System.currentTimeMillis(); System.out.println("End: " + methodName + " Cost: " + (endTime - startTime) + "ms"); } }
@Around 的核心能力:
- 可以在
joinPoint.proceed()前后插入自定义逻辑(计时、打印参数/返回值) - 可以决定是否执行原方法(比如有缓存直接返回缓存值,绕过方法)
- 可以替换返回值(比如统一包装返回格式)
- 是本场“控制力最强”的通知类型
三、Spring Boot 统一拦截处理:设计与实现(权限校验)
在权限校验场景下,最佳实践是 自定义注解 + HandlerInterceptor,实现低耦合、声明式的权限控制。
1. 执行流程
▼text复制代码请求到达 → preHandle(前置处理) → Controller方法执行 ↓ postHandle(后置处理) ↓ afterCompletion(完成处理)
preHandle:Controller 执行前调用,是权限校验的核心。返回true放行,false拦截。postHandle:Controller 方法执行后、视图渲染前调用,可对 ModelAndView 做额外处理(现在前后端分离较少用)。afterCompletion:整个请求完成后调用,适合做资源清理,无论成功/异常都会执行。
2. 实现步骤
2.1 定义自定义权限注解
▼java复制代码@Target(ElementType.METHOD) // 只能用在方法上 @Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射读取,必不可少 public @interface RequirePermission { String value(); // 所需权限码,如 "user:delete" }
2.2 创建权限拦截器
▼java复制代码public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // a. 只拦截 Controller 方法(放过静态资源等) if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; // b. 尝试获取方法上的 @RequirePermission 注解 RequirePermission permission = handlerMethod.getMethodAnnotation(RequirePermission.class); if (permission == null) { return true; // 没有注解的方法直接放行 } // c. 获取当前登录用户,校验权限 String requiredPermission = permission.value(); User user = getCurrentUser(request); // 自己实现,如从 token/session 中解析 if (user != null && user.hasPermission(requiredPermission)) { return true; // 有权限,放行 } else { // 无权限,设置 403 状态码并返回错误信息 response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"message\":\"No Permission\"}"); return false; } } // postHandle、afterCompletion 可按需实现 }
2.3 注册拦截器(极易遗忘的步骤)
▼java复制代码@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns("/login", "/error"); // 排除不需要拦截的路径 } }
2.4 在 Controller 中使用
▼java复制代码@RestController @RequestMapping("/api/admin") public class AdminController { @RequirePermission("user:delete") @DeleteMapping("/users/{id}") public Result deleteUser(@PathVariable Long id) { // 业务逻辑:只有拥有 "user:delete" 权限的用户才能执行到这里 } }
四、AOP vs. 拦截器:如何选择?
| 对比维度 | Spring AOP (切面) | Spring 拦截器 (Interceptor) |
|---|---|---|
| 拦截目标 | 任意 方法 调用(Service、Repository等) | HTTP 请求(最终对应到 Controller 方法) |
| 实现机制 | 动态代理(JDK 动态代理 / CGLIB) | Java 反射 + 职责链模式 |
| 执行阶段 | 在 Spring 代理层执行,逻辑上在拦截器之后 | 在 DispatcherServlet 之后,Controller 之前 |
| 拿手好戏 | Service 层日志、事务、性能监控、缓存 | Web 层登录鉴权、URL级权限校验、请求参数处理 |
| 能否拿到 HTTP 对象 | 可以拿到,但不太方便(需要注入或上下文获取) | 天然支持 HttpServletRequest / Response |
快速决策经验:
- 需要处理 Web 层请求/响应对象(如获取 header、设置状态码)→ 首选 拦截器
- 想对 Service 层或任意方法 做统一增强(日志、事务)→ 首选 AOP
- 两者可以共存:拦截器做登录/粗粒度鉴权,AOP 做细粒度方法级权限/日志
五、总结
- AOP 是“骨骼”,在 方法层面 提供精细控制。
@Around环绕通知是其最强形态,能够织入方法执行全生命周期的日志、性能监控等逻辑,是项目中实现请求日志的首选。 - 拦截器 是“关卡”,在 请求层面 提供大门级的拦截。
通过HandlerInterceptor+ 自定义权限注解,可以优雅地实现 Web 层权限校验,代码清晰、耦合度低。 - 两者互补使用,可以构建出健壮、可维护的“统一处理层”,让业务代码更专注于核心逻辑。
六、补充
没有注解的方法直接放行 这句注释的意思是:
如果某个 Controller 方法上没有标注 @RequirePermission 注解,拦截器就认为这个方法不需要任何特殊权限,任何人都可以访问,因此直接“放行”,让请求继续执行这个方法。
举例对比,一目了然
假设你有以下两个接口:
▼java复制代码// 接口一:没有加 @RequirePermission 注解 @GetMapping("/public") public String publicInfo() { return "这是公开信息,所有人可见"; } // 接口二:加了 @RequirePermission 注解 @RequirePermission("user:delete") @DeleteMapping("/users/{id}") public String deleteUser(@PathVariable Long id) { return "用户已删除"; }
当请求到达拦截器的 preHandle 方法时:
| 请求目标 | 注解情况 | permission 的值 | 执行逻辑 |
|---|---|---|---|
/public | 没有 @RequirePermission | null | 进入 if (permission == null) 分支,直接 return true 放行,不校验权限 |
/users/123 | 有 @RequirePermission("user:delete") | 非 null | 跳过 if 分支,继续往下执行权限校验逻辑 |
为什么这样设计?
这是一种 “白名单”或“按需拦截” 的设计思想:
- 有注解的方法 = 需要保护,必须通过权限校验才能访问(如删除用户、修改配置等敏感操作)
- 没有注解的方法 = 公共接口,不需要权限控制,直接放行(如首页、登录、查询公开信息等)
这样做的好处是:
- 按需配置:不需要给每个接口都加注解,只需给需要权限的接口加即可
- 避免误拦:登录接口、注册接口等肯定不能要求用户已登录或拥有某种权限,不加注解就能自动放行
- 代码清晰:看注解就知道哪些接口有权限要求,一目了然
评论
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
内容推荐
Day 103✅ 今天做了:复习了多用户通信系统⏰ 明天计划:学习Java反射
1
Day 68时间19:00~ 22:00(3h)✅ 今天做了:Component注解、Mybatis配置、使用⏰ 明天计划:Lombok、Mapper映射、动态SQL📚 今日感悟:自动配置类DataSourceAutoConfiguration ,会读取properties文件,通过注解:@EnableConfigurationProperties(DataSourceProperties.cl
2
Day 19✅ 今天做了:MCP⏰ 明天计划:AI智能体构建📚 今日感悟:今天MCP问题有点多有点杂,明天找时间再捋一下。继续加油
1
Day 25✅ 今天做了:1、扇贝英语单词打卡2、英语听说读写、听力练习3、微信阅读15分钟4、编程导航学习⏰ 明天计划:待定📚 今日感悟:Keep going!
2
Day 104✅ 今天做了:学习了Java反射及快速入门⏰ 明天计划:继续学习Java反射
1
作者分享
口述学习内容
5.8
事务隔离级别:为解决多个事务并行操作数据库可能出现脏读、不可重复读与幻读这三种问题,得出了四种事务级别,作为不同场景下的事务问题解决方案。
读未提交;问题全有。
脏读:读到了别人已修改但还没提交的数据,后面别人的事务可能回滚,读到的就成了假数据。
不可重复读:同一事务内,两次读数据的值不同,因为期间被别人修改了。
幻读:同一事务内,两次查询数据的行数不同,因为期间数据被别人增加或删除了。
读已提交:通过锁表?将还没提交的行锁起来,解决脏读。
可重复读:通过给数据库加版本号,同一事务内读到的数据是同一个版本的数据库,解决了不可重复读。
串行:通过锁数据库?同一时间只有一个事务内操作数据库,解决了幻读。
5.7
mysql:DDL、DML。单表函数、多表函数:聚合函数。
项目:1.黑马苍穹外卖实现员工登录、新增员工功能。2.登录功能将明文登录密码转为密文存入数据库的功能。3.通过转换工具将员工DTO的属性拷贝到员工实体中,实现便捷编写代码。4.新增员工的用户名有唯一索引且与数据库表中已有数据重复而发现运行时异常报错,通过编写自定义用户已存在异常,编写全局异常处理器捕获自定义异常,实现异常处理。且统一异常处理,代码可复用、异常与业务代码分离,代码可读性高。
2
java高级与sping的学习
3
java集合复习
3
集合的迭代器遍历复习
2
java线程入门复习
3
