IOC和AOP概念理解

Posted by     "zengchengjie" on Thursday, June 18, 2020

IOC 和 AOP:Spring 框架的两大基石

前言

对于 Java 开发者来说,Spring 框架几乎已经成为开发规范本身。而理解 Spring,绕不开两个核心概念:IOC(控制反转)AOP(面向切面编程)

这两个概念听起来高深莫测,但本质上解决的是软件开发中最朴素的问题:如何写出低耦合、高复用、易维护的代码

本文将用最通俗的语言,结合实际代码示例,帮你彻底理解 IOC 和 AOP 的本质。

一、IOC:控制反转

1.1 一个简单的例子

假设我们要开发一个订单系统,订单服务需要调用用户服务来获取用户信息。

传统做法(没有 IOC)

// 用户服务
public class UserService {
    public User getUserById(Long id) {
        // 查询数据库...
        return new User(id, "张三");
    }
}

// 订单服务
public class OrderService {
    // 主动创建依赖对象
    private UserService userService = new UserService();
    
    public void processOrder(Long userId) {
        User user = userService.getUserById(userId);
        System.out.println("处理订单,用户:" + user.getName());
    }
}

// 使用
public class Main {
    public static void main(String[] args) {
        OrderService orderService = new OrderService();
        orderService.processOrder(1L);
    }
}

这段代码有什么问题?

┌─────────────────────────────────────────────────────────┐
│                    问题分析                               │
├─────────────────────────────────────────────────────────┤
│  1. 高度耦合:OrderService 直接依赖 UserService 的具体实现  │
│  2. 难以测试:无法替换 UserService 为 Mock 对象            │
│  3. 难以扩展:如果要换成其他 UserService 实现,必须改代码    │
│  4. 控制权在业务代码:对象由业务代码主动创建和管理           │
└─────────────────────────────────────────────────────────┘

1.2 什么是控制反转?

控制反转(Inversion of Control,IOC)的核心思想是:将对象的创建和管理交给容器,而不是由业务代码主动控制

通俗理解:传统模式下,你需要一个对象,就自己 new 一个;IOC 模式下,你告诉容器"我需要什么",容器帮你创建好送过来。

传统模式:                     IOC模式:
┌─────────┐                   ┌─────────┐
│ 业务代码 │  ──new──▶  │ 依赖对象 │       │ 业务代码 │  ──声明依赖──▶ │  容器   │
└─────────┘                   └─────────┘       └─────────┘      │       │
    ▲                              │                 │          ▼       │
    │                              │                 │     ┌─────────┐  │
    └──────────────────────────────┘                 └────▶│ 依赖对象 │◀─┘
         业务代码负责创建和管理                                 │ 容器负责创建和注入

1.3 IOC 的实现:依赖注入

依赖注入(Dependency Injection,DI)是 IOC 的具体实现方式。主要有三种注入方式:

方式一:构造器注入(推荐)

@Service
public class OrderService {
    
    private final UserService userService;
    private final ProductService productService;
    
    // 通过构造器注入依赖
    public OrderService(UserService userService, ProductService productService) {
        this.userService = userService;
        this.productService = productService;
    }
    
    public void processOrder(Long userId, Long productId) {
        User user = userService.getUserById(userId);
        Product product = productService.getProductById(productId);
        System.out.println("用户:" + user.getName() + " 购买了 " + product.getName());
    }
}

优点

  • 依赖不可变(final 修饰)
  • 对象创建即完成依赖注入,状态完整
  • 便于单元测试(直接传入 Mock 对象)

方式二:Setter 注入

@Service
public class OrderService {
    
    private UserService userService;
    private ProductService productService;
    
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    
    @Autowired
    public void setProductService(ProductService productService) {
        this.productService = productService;
    }
}

适用场景:可选依赖、需要动态替换的场景

方式三:字段注入(最方便但争议最大)

@Service
public class OrderService {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private ProductService productService;
}

缺点

  • 无法声明为 final
  • 外部无法看到依赖关系
  • 单元测试时需要反射注入

💡 最佳实践:团队项目建议统一使用构造器注入。

1.4 Spring IOC 容器的工作原理

// 1. 定义配置类
@Configuration
@ComponentScan("com.example")
public class AppConfig {
    
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

// 2. 容器启动
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

// 3. 获取 Bean
OrderService orderService = context.getBean(OrderService.class);

容器启动流程

┌─────────────────────────────────────────────────────────────────┐
│                    Spring IOC 容器启动流程                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 扫描                                                         │
│     └── 扫描指定包下的所有类,找到 @Component、@Service 等注解     │
│                                                                 │
│  2. 解析                                                         │
│     └── 解析类上的注解、字段上的 @Autowired、方法上的 @Bean        │
│                                                                 │
│  3. 创建 BeanDefinition                                          │
│     └── 将类的元信息封装成 BeanDefinition 对象                    │
│                                                                 │
│  4. 实例化                                                       │
│     └── 通过反射创建对象实例(此时尚未注入依赖)                    │
│                                                                 │
│  5. 依赖注入                                                     │
│     └── 根据 @Autowired 或构造器信息,注入依赖对象                 │
│                                                                 │
│  6. 初始化                                                       │
│     └── 执行 @PostConstruct 方法、InitializingBean 等            │
│                                                                 │
│  7. 注册到容器                                                   │
│     └── 将完整对象放入容器(Map<String, Object>)                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.5 Bean 的作用域

作用域 说明 适用场景
singleton(默认) 容器中只有一个实例 无状态的服务类
prototype 每次获取都创建新实例 有状态的对象
request 每个 HTTP 请求一个实例 Web 应用中的请求级对象
session 每个 HTTP Session 一个实例 用户会话级对象
@Service
@Scope("prototype")
public class ShoppingCart {
    private List<Item> items = new ArrayList<>();
    // 每个用户独立的购物车
}

1.6 为什么需要 IOC?

┌─────────────────────────────────────────────────────────────────┐
│                    IOC 的核心价值                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ✅ 解耦                                                         │
│     上层模块不再依赖下层模块的具体实现,只依赖抽象                    │
│                                                                 │
│  ✅ 可测试性                                                     │
│     可以轻松注入 Mock 对象进行单元测试                             │
│                                                                 │
│  ✅ 可维护性                                                     │
│     替换依赖实现只需修改配置,无需改动业务代码                      │
│                                                                 │
│  ✅ 统一管理                                                     │
│     对象的生命周期、作用域由容器统一管理                           │
│                                                                 │
│  ✅ 关注点分离                                                   │
│     业务代码专注于业务逻辑,对象管理交给容器                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

二、AOP:面向切面编程

2.1 一个常见的痛点

假设我们有一个订单服务,现在需要在每个方法前后添加日志和权限检查:

@Service
public class OrderService {
    
    public void createOrder(Order order) {
        // 日志:方法开始
        System.out.println("[LOG] createOrder 开始,参数:" + order);
        
        // 权限检查
        if (!hasPermission()) {
            throw new SecurityException("无权限");
        }
        
        // 业务逻辑
        System.out.println("创建订单:" + order);
        
        // 日志:方法结束
        System.out.println("[LOG] createOrder 结束");
    }
    
    public void updateOrder(Order order) {
        // 日志:方法开始
        System.out.println("[LOG] updateOrder 开始,参数:" + order);
        
        // 权限检查
        if (!hasPermission()) {
            throw new SecurityException("无权限");
        }
        
        // 业务逻辑
        System.out.println("更新订单:" + order);
        
        // 日志:方法结束
        System.out.println("[LOG] updateOrder 结束");
    }
    
    // ... 更多方法,相同的代码重复出现
}

问题

  • 大量重复代码(日志、权限、事务等)
  • 业务逻辑被非业务代码污染
  • 修改切面逻辑(如日志格式)需要改所有方法

2.2 什么是 AOP?

面向切面编程(Aspect Oriented Programming,AOP)的核心思想是:将横切关注点(如日志、事务、权限)从业务逻辑中分离出来,实现关注点复用

传统 OOP 视角(纵向继承):        AOP 视角(横向切面):
                                
    ┌─────────┐                    ┌─────────────────────────────┐
    │ 用户服务 │                    │  ┌───────┐  ┌───────┐  ┌───────┐│
    ├─────────┤                    │  │ 日志  │  │ 权限  │  │ 事务  ││ ← 切面
    │ 订单服务 │                    │  └───┬───┘  └───┬───┘  └───┬───┘│
    ├─────────┤                    │      │         │         │     │
    │ 商品服务 │                    │  ┌───▼─────────▼─────────▼───┐│
    └─────────┘                    │  │      业务逻辑层            ││
                                   │  └───────────────────────────┘│
     代码重复散布在各处              │        核心关注点              │
                                   └─────────────────────────────┘

2.3 AOP 核心概念

概念 说明 类比
Aspect(切面) 横切关注点的模块化(如日志、事务) 一个功能模块
Join Point(连接点) 程序执行过程中可以插入切面的点(方法执行、异常抛出等) 可以插入代码的位置
Advice(通知) 切面在特定连接点执行的动作(前置、后置、环绕等) 要插入的代码
Pointcut(切入点) 匹配连接点的表达式,定义切面应用到哪些方法 定义"在哪里"插入
Target(目标对象) 被代理的对象 原始的业务对象
Proxy(代理) 为目标对象创建的代理对象 包装后的对象
Weaving(织入) 将切面应用到目标对象的过程 把代码插入进去

2.4 Advice 类型详解

@Component
@Aspect
public class LogAspect {
    
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    
    // 定义切入点:匹配 com.example.service 包下所有类的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethod() {}
    
    // 1. 前置通知:方法执行前
    @Before("serviceMethod()")
    public void beforeMethod(JoinPoint joinPoint) {
        log.info("【前置通知】开始执行方法:{},参数:{}", 
            joinPoint.getSignature().getName(),
            joinPoint.getArgs());
    }
    
    // 2. 后置通知:方法执行后(无论是否异常)
    @After("serviceMethod()")
    public void afterMethod(JoinPoint joinPoint) {
        log.info("【后置通知】方法执行完毕:{}", 
            joinPoint.getSignature().getName());
    }
    
    // 3. 返回通知:方法正常返回后
    @AfterReturning(pointcut = "serviceMethod()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        log.info("【返回通知】方法返回结果:{}", result);
    }
    
    // 4. 异常通知:方法抛出异常后
    @AfterThrowing(pointcut = "serviceMethod()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        log.error("【异常通知】方法执行异常:{},异常信息:{}", 
            joinPoint.getSignature().getName(), 
            e.getMessage());
    }
    
    // 5. 环绕通知:最强大的通知类型,可以完全控制方法的执行
    @Around("serviceMethod()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        
        try {
            // 前置处理
            log.info("【环绕-前置】开始执行:{}", methodName);
            
            // 执行目标方法
            Object result = joinPoint.proceed();
            
            // 后置处理
            long costTime = System.currentTimeMillis() - startTime;
            log.info("【环绕-后置】执行完成:{},耗时:{}ms", methodName, costTime);
            
            return result;
            
        } catch (Exception e) {
            log.error("【环绕-异常】执行异常:{}", methodName, e);
            throw e;
        } finally {
            log.info("【环绕-最终】最终处理:{}", methodName);
        }
    }
}

2.5 切入点表达式详解

// 1. 匹配指定包下的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")

// 2. 匹配指定类的所有方法
@Pointcut("execution(* com.example.service.OrderService.*(..))")

// 3. 匹配带特定注解的方法
@Pointcut("@annotation(com.example.annotation.Cacheable)")

// 4. 匹配带特定注解的类中的所有方法
@Pointcut("@within(com.example.annotation.Service)")

// 5. 组合表达式(&&、||、!)
@Pointcut("execution(* com.example.service.*.*(..)) && !@annotation(com.example.annotation.NoLog)")

// 6. 匹配参数类型
@Pointcut("execution(* com.example.service.*.*(String, ..))")  // 第一个参数是 String

// 7. 匹配返回类型
@Pointcut("execution(com.example.entity.User com.example.service.*.*(..))")  // 返回 User 类型

表达式语法

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

示例:execution(public * com.example.service.UserService.getUser*(..))

┌──────────┬────────────────────────────────────────────────┐
│ public   │ 修饰符(可选)                                   │
│ *        │ 返回类型(* 表示任意类型)                        │
│ com...   │ 声明类型(可选)                                 │
│ getUser* │ 方法名(* 通配符)                               │
│ (..)     │ 参数(.. 表示任意参数)                          │
└──────────┴────────────────────────────────────────────────┘

2.6 完整示例:性能监控切面

@Component
@Aspect
@Slf4j
public class PerformanceAspect {
    
    // 使用 ThreadLocal 存储每个线程的执行时间
    private final ThreadLocal<Long> startTime = new ThreadLocal<>();
    
    // 定义切入点:Controller 层的所有方法
    @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
    public void controllerMethod() {}
    
    // 定义切入点:带 @PerfMonitor 注解的方法
    @Pointcut("@annotation(com.example.annotation.PerfMonitor)")
    public void annotatedMethod() {}
    
    @Around("controllerMethod() || annotatedMethod()")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        String fullMethodName = className + "." + methodName;
        
        // 记录开始时间
        startTime.set(System.currentTimeMillis());
        
        try {
            // 执行目标方法
            Object result = joinPoint.proceed();
            
            // 计算耗时
            long cost = System.currentTimeMillis() - startTime.get();
            
            // 性能告警:超过 500ms 打印警告
            if (cost > 500) {
                log.warn("【性能告警】方法 {} 执行耗时 {}ms,超过阈值 500ms", 
                    fullMethodName, cost);
            } else {
                log.info("【性能监控】方法 {} 执行耗时 {}ms", fullMethodName, cost);
            }
            
            return result;
            
        } catch (Exception e) {
            long cost = System.currentTimeMillis() - startTime.get();
            log.error("【性能监控】方法 {} 执行异常,耗时 {}ms", fullMethodName, cost, e);
            throw e;
            
        } finally {
            startTime.remove();  // 清理 ThreadLocal,防止内存泄漏
        }
    }
}

2.7 AOP 的实现原理:动态代理

Spring AOP 底层基于两种动态代理机制:

JDK 动态代理(基于接口)

// 目标类实现接口
public interface UserService {
    void addUser(String name);
}

public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String name) {
        System.out.println("添加用户:" + name);
    }
}

// 代理工厂
public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            (proxy, method, args) -> {
                System.out.println("【前置通知】准备调用方法:" + method.getName());
                Object result = method.invoke(target, args);
                System.out.println("【后置通知】方法调用完成");
                return result;
            }
        );
    }
}

CGLIB 动态代理(基于继承)

当目标类没有实现接口时,Spring 会使用 CGLIB 生成目标类的子类作为代理。

// 目标类(无接口)
public class OrderService {
    public void createOrder() {
        System.out.println("创建订单");
    }
}

// CGLIB 代理(生成子类)
// 代理类 extends OrderService {
//     @Override
//     public void createOrder() {
//         前置处理();
//         super.createOrder();
//         后置处理();
//     }
// }

选择规则

┌─────────────────────────────────────────────────────────────┐
│              Spring AOP 代理方式选择                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  目标类是否实现了接口?                                        │
│       │                                                     │
│       ├── 是 ──▶ 使用 JDK 动态代理(基于接口)                 │
│       │                                                     │
│       └── 否 ──▶ 使用 CGLIB 动态代理(基于继承)               │
│                                                             │
│  强制使用 CGLIB:                                             │
│  @EnableAspectJAutoProxy(proxyTargetClass = true)           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.8 AOP 的实际应用场景

场景 说明 示例
事务管理 声明式事务 @Transactional 最经典的 AOP 应用
日志记录 方法调用日志、参数日志 统一日志处理
权限控制 方法级权限检查 @PreAuthorize Spring Security
性能监控 方法耗时统计、慢查询告警 监控切面
缓存管理 @Cacheable、@CacheEvict Spring Cache
异常处理 统一异常捕获和处理 @ControllerAdvice
参数校验 方法参数自动校验 @Validated
重试机制 失败自动重试 @Retryable

三、IOC 和 AOP 的协同工作

3.1 关系图

┌─────────────────────────────────────────────────────────────────┐
│                        Spring 框架                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                      IOC 容器                            │   │
│   │  ┌─────────────────────────────────────────────────┐    │   │
│   │  │              Bean 工厂 / 应用上下文              │    │   │
│   │  │  ┌─────────┐ ┌─────────┐ ┌─────────┐           │    │   │
│   │  │  │ 对象实例 │ │ 对象实例 │ │ 对象实例 │           │    │   │
│   │  │  │ (Target)│ │ (Target)│ │ (Target)│           │    │   │
│   │  │  └────┬────┘ └────┬────┘ └────┬────┘           │    │   │
│   │  │       │           │           │                 │    │   │
│   │  │       ▼           ▼           ▼                 │    │   │
│   │  │  ┌─────────┐ ┌─────────┐ ┌─────────┐           │    │   │
│   │  │  │  代理   │ │  代理   │ │  代理   │  ← AOP 织入 │    │   │
│   │  │  │ (Proxy) │ │ (Proxy) │ │ (Proxy) │           │    │   │
│   │  │  └─────────┘ └─────────┘ └─────────┘           │    │   │
│   │  └─────────────────────────────────────────────────┘    │   │
│   │                                                         │   │
│   │  流程:容器创建对象 → AOP 生成代理 → 代理替换原对象         │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.2 协同工作流程

@Configuration
@ComponentScan
@EnableAspectJAutoProxy  // 开启 AOP 代理
public class AppConfig {
    
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();  // IOC 管理数据源
    }
    
    @Bean
    @Transactional  // AOP 事务切面会织入这个方法
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

@Service
public class OrderService {
    
    @Autowired  // IOC 注入依赖
    private UserService userService;
    
    @Transactional  // AOP 事务切面
    public void createOrder(Order order) {
        // 业务逻辑
        userService.getUser(order.getUserId());
        // ...
    }
}

四、常见问题与误区

4.1 为什么静态方法无法被 AOP 代理?

AOP 基于代理实现,静态方法属于类而不是实例,代理对象无法覆盖静态方法。

@Service
public class OrderService {
    
    // ❌ 静态方法无法被 AOP 代理
    @Transactional
    public static void staticMethod() {
        // 事务不会生效
    }
    
    // ✅ 实例方法可以被代理
    @Transactional
    public void instanceMethod() {
        // 事务生效
    }
}

4.2 为什么内部调用 AOP 不生效?

@Service
public class OrderService {
    
    @Transactional
    public void methodA() {
        // 事务生效
    }
    
    public void methodB() {
        // ❌ 内部调用:直接调用 this.methodA(),绕过了代理
        this.methodA();  // 事务不生效!
    }
    
    public void methodC() {
        // ✅ 解决方案:通过代理调用
        ((OrderService) AopContext.currentProxy()).methodA();  // 事务生效
    }
}

4.3 final 类和方法的问题

CGLIB 通过继承生成代理,因此 final 类和 final 方法无法被代理。

// ❌ final 类无法被 CGLIB 代理
@Service
public final class FinalService {
    // ...
}

@Service
public class MyService {
    
    // ❌ final 方法无法被代理
    @Transactional
    public final void finalMethod() {
        // @Transactional 不生效
    }
}

4.4 IOC 容器管理的对象不是"new"出来的

@Service
public class DemoService {
    
    @Autowired
    private AnotherService anotherService;  // ✅ 由容器注入
    
    public void test() {
        // ❌ 错误:手动 new 的对象不会被容器管理
        AnotherService manualService = new AnotherService();
        // manualService 内部的 @Autowired 不会生效
        
        // ✅ 正确:从容器获取
        AnotherService containerService = 
            ApplicationContextHolder.getBean(AnotherService.class);
    }
}

五、总结

IOC 总结

概念 核心要点
本质 将对象的创建和管理交给容器
实现方式 依赖注入(构造器、Setter、字段)
核心价值 解耦、可测试、可维护
常见注解 @Component, @Service, @Repository, @Controller, @Autowired

AOP 总结

概念 核心要点
本质 将横切关注点从业务逻辑中分离
实现方式 JDK 动态代理 / CGLIB 代理
核心价值 代码复用、关注点分离
常见注解 @Aspect, @Before, @After, @Around, @Pointcut

一句话总结

IOC 让你不用自己 new 对象,AOP 让你不用重复写相同的代码。两者结合,让 Spring 帮你管理对象和处理通用逻辑,你只需要专注业务本身。


理解 IOC 和 AOP 是掌握 Spring 框架的关键。希望这篇文章能帮你建立起清晰的概念模型,在实际开发中更自如地运用这些强大的特性。