深入理解Spring框架核心:IoC(控制反转)和AOP(面向切面编程)详解

本文旨在深入探讨Spring框架的两大基石:IoC(控制反转)和AOP(面向切面编程)。我们将从多个维度剖析这两个概念,包括IoC的概念、作用、与依赖注入的关系,以及AOP的概念、原理、应用场景和实现方式。通过本文,您将对IoC和AOP有更清晰的理解,并了解它们如何在Spring框架中发挥重要作用,提升应用程序的可维护性和可扩展性。

IoC (控制反转)

IoC 的本质

IoC(Inversion of Control),即控制反转,并非某种具体的技术,而是一种设计思想,旨在解决Java开发中对象的创建和管理问题。

传统的开发模式下,如果类A依赖于类B,通常会在类A中使用new关键字手动创建类B的对象。而采用IoC思想后,对象的创建不再由程序员手动控制,而是交给IoC容器(如Spring框架)负责。当需要使用某个对象时,直接从IoC容器中获取即可。

这种方式看似“丧失”了程序员对对象创建的控制权,实则带来了一种好处:程序员无需再关注对象的创建和管理细节,可以专注于业务逻辑的实现。

那么,为何称之为“控制反转”呢?

  • 控制:指代对象创建(实例化、管理)的权力。
  • 反转:将控制权转移给外部环境(IoC 容器)。

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

IoC 如何解决问题

IoC的核心在于降低对象间的耦合度,并简化资源管理。例如,使用Spring容器可以轻松实现单例模式。

假设我们有一个针对User的操作,采用Service和Dao两层架构进行开发。

在未使用IoC的情况下,如果Service层需要使用Dao层的具体实现,则必须在UserServiceImpl中使用new关键字手动创建IUserDao的具体实现类UserDaoImpl的对象(无法直接new接口)。

这种方式虽然可行,但存在一个问题:如果后续需要更换IUserDao的实现类,就必须修改所有引用了UserDaoImpl的地方。当引用UserDaoImpl的地方很多时,修改工作量将会非常巨大。

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

而采用IoC思想后,对象的控制权(创建、管理)交由IoC容器管理,我们需要使用对象时,只需向IoC容器“索取”即可。

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

IoC 与 DI 的关系

IoC(控制反转)是一种设计理念或模式,其核心是将对象创建的控制权转移给第三方,例如IoC容器。在Spring框架中,IoC容器本质上是一个Map(key,value),用于存储各种对象。值得注意的是,IoC并非Spring独有,在其他编程语言中也有应用。

依赖注入(Dependency Injection,简称DI)是IoC最常用且最合理的实现方式。

Martin Fowler在一篇文章中建议将IoC更名为DI,原文地址:https://martinfowler.com/articles/injection.html

Martin Fowler认为,IoC的概念过于宽泛且不够精准,容易造成混淆,因此建议使用DI来更准确地描述这种模式。

AOP(面向切面编程)

AOP 简介

AOP(Aspect Oriented Programming),即面向切面编程,是OOP(面向对象编程)的延伸和补充,两者并非相互排斥,而是相辅相成。

AOP旨在将横切关注点(例如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提升代码的可维护性和可扩展性。而OOP的目的是将业务逻辑按照对象的属性和行为进行封装,通过类、对象、继承、多态等概念,实现代码的模块化和层次化(也能实现代码的复用),提高代码的可读性和可维护性。

AOP 的命名由来

AOP之所以被称为面向切面编程,是因为其核心思想是将横切关注点从核心业务逻辑中分离出来,形成一个个的切面(Aspect)

AOP 关键术语:

  • 横切关注点(cross-cutting concerns) :多个类或对象中的公共行为(例如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
  • 切面(Aspect):对横切关注点进行封装的类,一个切面就是一个类。切面可以定义多个通知,用来实现具体的功能。
  • 连接点(JoinPoint):方法调用或者方法执行时的某个特定时刻(例如方法调用、异常抛出等)。
  • 通知(Advice):切面在某个连接点要执行的操作。通知有五种类型,分别是前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程。
  • 切点(Pointcut):一个切点是一个表达式,用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。例如 execution(* com.xyz.service..*(..))匹配 com.xyz.service 包及其子包下的类或接口。
  • 织入(Weaving):织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种,分别是编译期织入(Compile-Time Weaving 例如:AspectJ)和运行期织入(Runtime Weaving 例如:AspectJ、Spring AOP)。

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)### 常见的 AOP 通知类型

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

  • Before(前置通知):在目标方法调用之前触发。
  • After (后置通知):在目标方法调用之后触发。
  • AfterReturning(返回通知):在目标方法调用完成并返回结果值之后触发。
  • AfterThrowing(异常通知):在目标方法运行过程中抛出异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
  • Around (环绕通知):通过编程方式控制目标方法的调用。环绕通知是所有通知类型中操作范围最大的一种,因为它可以直接获取目标对象和要执行的方法,因此可以在目标方法调用前后进行各种操作,甚至可以选择不调用目标方法。

AOP 如何解决问题

OOP在处理分散在多个类或对象中的公共行为(例如日志记录、事务管理、权限控制、接口限流、接口幂等等)方面存在不足,这些行为通常被称为 横切关注点(cross-cutting concerns) 。如果在每个类或对象中都重复实现这些行为,会导致代码冗余、复杂且难以维护。

AOP可以将横切关注点(例如日志记录、事务管理、权限控制、接口限流、接口幂等等)从 核心业务逻辑(core concerns,核心关注点) 中分离出来,实现关注点的分离。

推荐一篇关于AOP在项目中应用的文章:面试官:你的项目哪里用到了 AOP?怎么用的? 。

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

以日志记录为例,假设我们需要对某些方法进行统一格式的日志记录。在未使用AOP技术之前,我们需要在每个方法中手动编写日志记录逻辑,这会导致大量的重复代码。

public CommonResponse<Object> method1() {  
      // 业务逻辑  
      xxService.method1();  
      // 省略具体的业务处理逻辑  
      // 日志记录  
      ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();  
      HttpServletRequest request = attributes.getRequest();  
      // 省略记录日志的具体逻辑 如:获取各种信息,写入数据库等操作...  
      return CommonResponse.success();  
}  
  
public CommonResponse<Object> method2() {  
      // 业务逻辑  
      xxService.method2();  
      // 省略具体的业务处理逻辑  
      // 日志记录  
      ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();  
      HttpServletRequest request = attributes.getRequest();  
      // 省略记录日志的具体逻辑 如:获取各种信息,写入数据库等操作...  
      return CommonResponse.success();  
}  
  
// ...  

而使用AOP技术后,我们可以将日志记录逻辑封装成一个切面,然后通过切入点和通知来指定哪些方法需要执行日志记录操作。

// 日志注解  
@Target({ElementType.PARAMETER,ElementType.METHOD})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface Log {  
  
    /**  
     * 描述  
     */  
    String description() default "";  
  
    /**  
     * 方法类型 INSERT DELETE UPDATE OTHER  
     */  
    MethodType methodType() default MethodType.OTHER;  
}  
  
// 日志切面  
@Component  
@Aspect  
public class LogAspect {  
  // 切入点,所有被 Log 注解标注的方法  
  @Pointcut("@annotation(cn.javaguide.annotation.Log)")  
  public void webLog() {  
  }  
  
   /**  
   * 环绕通知  
   */  
  @Around("webLog()")  
  public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {  
    // 省略具体的处理逻辑  
  }  
  
  // 省略其他代码  
}  

这样,我们只需一行注解即可实现日志记录:

@Log(description = "method1",methodType = MethodType.INSERT)  
public CommonResponse<Object> method1() {  
      // 业务逻辑  
      xxService.method1();  
      // 省略具体的业务处理逻辑  
      return CommonResponse.success();  
}  

AOP 的应用场景

  • 日志记录: 自定义日志记录注解,利用 AOP,一行代码即可实现日志记录。
  • 性能统计: 利用 AOP 在目标方法执行前后统计方法执行时间,方便性能优化和分析。
  • 事务管理: @Transactional 注解可以借助 Spring 实现事务管理,例如异常回滚,避免了重复的事务管理逻辑。@Transactional注解的实现正是基于 AOP。
  • 权限控制: 利用 AOP 在目标方法执行前判断用户是否具备所需权限,如果具备则执行目标方法,否则不执行。例如,SpringSecurity 利用@PreAuthorize 注解一行代码即可实现自定义权限校验。
  • 接口限流: 利用 AOP 在目标方法执行前,通过特定的限流算法和实现对请求进行限流处理。
  • 缓存管理: 利用 AOP 在目标方法执行前后进行缓存的读取和更新。
  • ……

AOP 的实现方式

AOP的常见实现方式包括动态代理和字节码操作等。

Spring AOP基于动态代理实现。如果被代理的对象实现了某个接口,Spring AOP会使用 JDK Proxy 创建代理对象;而对于没有实现接口的对象,则无法使用 JDK Proxy 进行代理,此时 Spring AOP 会使用 Cglib 生成一个被代理对象的子类作为代理。

当然,你也可以使用 AspectJ !Spring AOP 已经集成了 AspectJ,AspectJ 可以说是 Java 生态系统中最完整的 AOP 框架。

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ,AspectJ 可以说是 Java 生态系统中最完整的 AOP 框架。AspectJ 比 Spring AOP 功能更强大,但 Spring AOP 相对更简单。

如果切面数量较少,两者的性能差异不大。但是,当切面数量很多时,最好选择 AspectJ,因为它比 Spring AOP 快很多。

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)