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 框架的关键。希望这篇文章能帮你建立起清晰的概念模型,在实际开发中更自如地运用这些强大的特性。