这篇文章我将通过一个最常用的AOP场景-方法调用日志记录,带你彻底理解AOP的使用。例子使用Spring Boot+Spring AOP实现。
如果对你有帮助可以点个赞和关注。谢谢大家的支持!!
一、Demo实操步骤:
1.首先添加Maven依赖
<!-- Spring AOP支持 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.创建日志切面类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;@Aspect // 标识这是一个切面类
@Component // 让Spring能够扫描到这个组件
public class LoggingAspect {// 创建日志记录器private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 定义切点:拦截service包下的所有方法* execution(返回值类型 包名.类名.方法名(参数列表))*/@Pointcut("execution(* com.example.demo.service.*.*(..))")public void serviceLayer() {}/*** 前置通知:在目标方法执行前执行*/@Before("serviceLayer()")public void logBefore(JoinPoint joinPoint) {// joinPoint包含目标方法的信息logger.info("准备执行 {} 方法", joinPoint.getSignature().getName());logger.info("参数: {}", Arrays.toString(joinPoint.getArgs()));}/*** 后置通知:在目标方法正常返回后执行*/@AfterReturning(pointcut = "serviceLayer()", returning = "result")public void logAfterReturning(JoinPoint joinPoint, Object result) {logger.info("方法 {} 执行成功,返回值: {}", joinPoint.getSignature().getName(), result);}/*** 异常通知:在目标方法抛出异常后执行*/@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {logger.error("方法 {} 执行异常: {}", joinPoint.getSignature().getName(), ex.getMessage());}/*** 环绕通知:最强大的通知类型,可以控制方法执行*/@Around("serviceLayer()")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis();logger.info("进入方法: {}", joinPoint.getSignature().getName());try {// 执行目标方法Object result = joinPoint.proceed();long elapsedTime = System.currentTimeMillis() - start;logger.info("方法执行完成,耗时: {} ms", elapsedTime);return result;} catch (Exception e) {long elapsedTime = System.currentTimeMillis() - start;logger.error("方法执行异常,耗时: {} ms,异常: {}", elapsedTime, e.getMessage());throw e;}}
}
3.创建测试服务类
@Service
public class UserService {public String getUserById(Long id) {// 模拟数据库查询if (id == 1) {return "用户张三";} else if (id == 2) {return "用户李四";}throw new RuntimeException("用户不存在");}public void updateUser(String user) {// 模拟更新操作System.out.println("更新用户: " + user);}
}
4.测试Controller
@RestController
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/user/{id}")public String getUser(@PathVariable Long id) {return userService.getUserById(id);}@PostMapping("/user")public void updateUser(@RequestBody String user) {userService.updateUser(user);}
}
二、代码讲解:
1.切面(Aspect):
LoggingAspect类 就是一个切面,它封装了横切关注点(这里是日志记录)。
2.切点(Pointcut):
@Pointcut("execution(* com.example.demo.service.*.*(..))")
- 定义了"哪些方法需要被拦截"
- execution是匹配方法执行的连接点
- *表示任意返回值
- com.example.demo.service指定包名
- 第一个*表示所有类
- 第二个*表示所有方法
- (. .)表示任意参数
3.通知(Advice):
- @Before:方法执行前
- @AfterReturning:方法正常返回后
- @AfterThrowing:方法抛出异常后
- @Around:最强大,可以控制方法是否执行
三、执行流程演示:
场景1:调用GET/user/1
1.请求进入UserController.getUser(1)
2.调用userService.getUserById(1)时被AOP拦截
3.执行顺序:
- @Around的开始部分(记录开始时间)
- @Before(记录方法准备执行)
- 实际执行getUserById方法
- @AfterReturning(记录成功返回)
- @Around的结束部分(计算耗时)
控制台输出 执行结果:
进入方法: getUserById
准备执行 getUserById 方法
参数: [1]
方法 getUserById 执行成功,返回值: 用户张三
方法执行完成,耗时: 12 ms
场景2.调用GET/user/3(会抛出异常)
1.请求进入UserController.getUser(3)
2.调用userService.getUserById(3)时被AOP拦截
3.执行顺序:
- @Around的开始部分
- @Before
- 执行getUserById抛出异常
- @AfterThrowing(记录异常信息)
- @Around的异常处理部分
控制台输出 执行结果:
进入方法: getUserById
准备执行 getUserById 方法
参数: [3]
方法 getUserById 执行异常: 用户不存在
方法执行异常,耗时: 5 ms,异常: 用户不存在
四、为什么要用AOP?
对比传统写法,如果不用AOP,我们需要在每个方法中写日志代码,重复代码太多,显得冗杂。
@Service
public class UserService {private final Logger logger = LoggerFactory.getLogger(this.getClass());public String getUserById(Long id) {long start = System.currentTimeMillis();logger.info("准备执行 getUserById 方法");logger.info("参数: " + id);try {if (id == 1) {String result = "用户张三";logger.info("方法执行成功,返回值: " + result);return result;}throw new RuntimeException("用户不存在");} catch (Exception e) {logger.error("方法执行异常: " + e.getMessage());throw e;} finally {long elapsedTime = System.currentTimeMillis() - start;logger.info("方法耗时: " + elapsedTime + " ms");}}
}
此时就可以使用AOP来解决。
AOP的优势:
1.消除重复代码:日志逻辑只写一次,应用到所有方法。
2.业务更纯净:业务方法只关注核心逻辑。
3.维护方便:修改日志格式只需改切面类。
4.灵活扩展:可以随时添加新的横切逻辑(如权限检查)。
五、应用场景:
1. 统一日志记录(如上面的例子)
2.性能监控:统计方法执行时间
@Around("serviceLayer()")
public Object monitorPerformance(ProceedingJoinPoint jp) throws Throwable {long start = System.currentTimeMillis();Object result = jp.proceed();long elapsed = System.currentTimeMillis() - start;if (elapsed > 500) { // 超过500ms记录警告logger.warn("方法 {} 执行耗时 {} ms", jp.getSignature(), elapsed);}return result;
}
3.事务管理(Spring的@Transactional就是基于AOP实现的)
4.权限控制:
@Before("@annotation(requiresAuth)")
public void checkAuth(JoinPoint jp, RequiresAuth requiresAuth) {if (!SecurityUtils.hasRole(requiresAuth.value())) {throw new AccessDeniedException("无权限访问");}
}
5.缓存处理:
@Around("@annotation(cacheable)")
public Object handleCache(ProceedingJoinPoint jp, Cacheable cacheable) throws Throwable {String key = generateKey(jp, cacheable);Object cached = cache.get(key);if (cached != null) return cached;Object result = jp.proceed();cache.put(key, result);return result;
}
六、小白常见问题:
1.切面不生效?
- 确保切面类有 @Component 注解。
- 确保启动类有 @EnableAspectJAutoProxy(Spring Boot自动配置)。
- 检查切点表达式是否正确匹配到目标方法。
2.执行顺序问题:
- 多个切面同一个切点,可以用 @Order注解指定顺序。
- 单个切面中通知执行顺序:Around前 → Before → 方法执行 → Around后 → AfterReturning/AfterThrowing → After
3.自调用问题:
- 同一个类内部方法调用不会触发AOP(因为不是代理对象调用)
- 解决方案:从Spring容器获取代理对象调用
七、总结:
通过这篇文章,你应该理解了:
1.AOP如何通过切面、切点、通知等概念工作。
2.为什么AOP能解决代码重复的问题。
3.如何在Spring项目中实际使用AOP。
4.AOP的各种实际应用场景。
AOP就像是一个"方法拦截器",在不修改原有代码的情况下,给方法添加各种增强功能。这是Spring框架的核心特性之一,掌握后能大幅提高代码质量和开发效率。