被重复代码逼疯?AOP来当“舔狗”!日志/事务/权限,随叫随到!

被重复代码逼疯?AOP来当“舔狗”!日志/事务/权限,随叫随到!_第1张图片

文章目录

      • 一、什么是AOP?
      • 二、AOP核心概念
        • 1. 切面(Aspect)
        • 2. 通知(Advice)
        • 3. 切点(Pointcut)
        • 4. 连接点(Joinpoint)
      • ️ 三、代码示例(Spring Boot环境)
        • 0. 添加依赖
        • 1. 简单日志记录(@Before)
        • 2. 方法性能监控(@Around)
        • 3. 权限校验(自定义注解 + @Before)
        • 4. 缓存优化(@Around + 缓存逻辑)
      • 四、AOP底层原理(简单版)
      • 五、AOP最佳实践
      • 六、总结

这篇文章用 超详细版 + 完整代码 给你拆解Spring AOP,保证手把手教会!


一、什么是AOP?

AOP(面向切面编程) 的核心思想:把横跨多个模块的通用功能(如日志、事务)抽出来,统一管理,再动态“织入”到需要的地方,让业务代码只关注核心逻辑!

举个:
假设你开了一家奶茶店,核心业务是“做奶茶”,但每次做奶茶前要检查材料库存,做完后要清理操作台。如果用AOP,就能把“检查库存”和“清理操作台”这两个步骤抽出来,自动加到所有奶茶制作流程中,而不需要每个奶茶配方都写一遍!


二、AOP核心概念

1. 切面(Aspect)
  • 是什么:封装通用功能的类(比如日志模块)。
  • 怎么做:用@Aspect注解标记的Spring Bean。
@Aspect  // 声明这是一个切面
@Component  // 交给Spring管理
public class LogAspect { ... }
2. 通知(Advice)
  • 是什么:切面中具体执行的代码逻辑(比如方法前打印日志)。
  • 类型
    • @Before:目标方法执行
    • @After:目标方法执行(无论是否异常)
    • @AfterReturning:目标方法正常返回后执行
    • @AfterThrowing:目标方法抛出异常后执行
    • @Around包裹目标方法,控制是否执行
3. 切点(Pointcut)
  • 是什么:通过表达式定义哪些方法需要被拦截。
  • 常用表达式
    • execution(* com.example.service.*.*(..)):拦截service包下所有类的所有方法
    • @annotation(com.example.RequireAdmin):拦截带有@RequireAdmin注解的方法
4. 连接点(Joinpoint)
  • 是什么:程序执行过程中可以插入切面的点(比如方法调用、异常抛出)。

️ 三、代码示例(Spring Boot环境)

0. 添加依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-aopartifactId>
dependency>

1. 简单日志记录(@Before)

目标:在UserService所有方法执行前打印日志。

// UserService.java
@Service
public class UserService {
    public void addUser(String username) {
        System.out.println("添加用户: " + username);
    }
}
// LogAspect.java
@Aspect
@Component
public class LogAspect {
    
    // 定义切点:拦截UserService所有方法
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void userServiceMethods() {}
    
    // 前置通知
    @Before("userServiceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println(" 调用方法: " + methodName);
    }
}

测试

@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;
    
    @Test
    public void testAddUser() {
        userService.addUser("张三");
    }
}

输出

 调用方法: addUser  
添加用户: 张三  

2. 方法性能监控(@Around)

目标:统计方法执行耗时。

// PerformanceAspect.java
@Aspect
@Component
public class PerformanceAspect {
    
    // 拦截所有Service层方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}
    
    // 环绕通知
    @Around("serviceMethods()")
    public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 执行目标方法
        long time = System.currentTimeMillis() - start;
        String methodName = joinPoint.getSignature().getName();
        System.out.println("⏱️ 方法 " + methodName + " 耗时: " + time + "ms");
        return result;
    }
}

输出

 调用方法: addUser  
添加用户: 张三  
⏱️ 方法 addUser 耗时: 12ms  

3. 权限校验(自定义注解 + @Before)

目标:只有管理员能执行某些方法。

步骤1:定义自定义注解

// RequireAdmin.java
@Target(ElementType.METHOD)  // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时生效
public @interface RequireAdmin {
}

步骤2:在Service方法上添加注解

// UserService.java
@Service
public class UserService {
    @RequireAdmin  // 只有管理员能调用
    public void deleteUser(int userId) {
        System.out.println("删除用户: " + userId);
    }
}

步骤3:实现权限校验切面

// SecurityAspect.java
@Aspect
@Component
public class SecurityAspect {
    
    // 拦截所有带有@RequireAdmin注解的方法
    @Before("@annotation(com.example.annotation.RequireAdmin)")
    public void checkAdmin(JoinPoint joinPoint) {
        // 模拟获取当前用户
        User currentUser = getCurrentUser();
        if (!currentUser.isAdmin()) {
            throw new RuntimeException(" 权限不足!");
        }
    }
    
    private User getCurrentUser() {
        // 模拟返回普通用户
        return new User("李四", false);
    }
}

测试

@Test(expected = RuntimeException.class)
public void testDeleteUserWithoutPermission() {
    userService.deleteUser(1);
}

输出

Exception:  权限不足!  

4. 缓存优化(@Around + 缓存逻辑)

目标:优先从缓存读取数据,缓存不存在再查数据库。

// CacheAspect.java
@Aspect
@Component
public class CacheAspect {
    
    // 模拟缓存(实际可用Redis)
    private Map<String, Object> cache = new ConcurrentHashMap<>();
    
    // 拦截带有@Cacheable注解的方法
    @Around("@annotation(com.example.annotation.Cacheable)")
    public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable {
        String key = generateCacheKey(joinPoint);
        Object value = cache.get(key);
        if (value != null) {
            System.out.println(" 从缓存获取数据: " + key);
            return value;
        }
        // 缓存不存在,执行方法并缓存结果
        value = joinPoint.proceed();
        cache.put(key, value);
        System.out.println(" 缓存数据: " + key);
        return value;
    }
    
    // 生成缓存Key(类名+方法名+参数)
    private String generateCacheKey(ProceedingJoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        return className + ":" + methodName + ":" + Arrays.toString(args);
    }
}

使用注解

// UserService.java
@Service
public class UserService {
    @Cacheable
    public User getUserById(int userId) {
        System.out.println(" 查询数据库用户: " + userId);
        return new User("用户" + userId);
    }
}

测试

@Test
public void testGetUserById() {
    userService.getUserById(1);  // 第一次查数据库
    userService.getUserById(1);  // 第二次走缓存
}

输出

 查询数据库用户: 1  
 缓存数据: UserService:getUserById:[1]  
 从缓存获取数据: UserService:getUserById:[1]  

四、AOP底层原理(简单版)

Spring AOP通过 动态代理 实现:

  • 如果目标类实现了接口 → 使用 JDK动态代理
  • 如果目标类没有实现接口 → 使用 CGLIB代理

代理对象在方法调用时,会按顺序执行切面逻辑(如前置通知→目标方法→后置通知)。


五、AOP最佳实践

1️⃣ 切点表达式尽量精确:避免拦截不需要的方法(如execution(* com.example.service.UserService.*(..))execution(* *.*(..))更安全)。
2️⃣ 优先使用注解配置:比XML配置更直观。
3️⃣ 注意代理失效场景:同一个类内部方法调用(AOP不生效),可通过注入自身Bean解决。


六、总结

AOP就像给你的代码加了一个“智能管家”:

  • 日志、权限、事务这些重复工作统统交给它!
  • 业务代码只专注核心逻辑,不再臃肿!
  • 需要加新功能(比如缓存)时,改一处就行,维护超省心!

下次看到代码里重复的try-catch或日志打印,就大喊:“切面来救!”

你可能感兴趣的:(SpringBoot,数据库)