IoC & AOP详解(快速搞懂)
IoC & AOP详解(快速搞懂)
IoC(控制反转)和AOP(面向切面编程)是Spring框架中的两个核心概念,它们在解决软件开发中的耦合度问题和横切关注点问题方面发挥着重要作用。本文将从多个维度深入解析这两个概念,帮助读者全面理解它们的原理和应用场景。
IoC(Inversion of Control)
什么是IoC?
IoC(Inversion of Control)即控制反转/反转控制。它是一种思想,而不是一个具体的技术实现。描述的是:Java开发领域对象的创建以及管理的问题。
例如:现有类A依赖于类B
- 传统的开发方式:往往是在类A中手动通过new关键字来new一个B的对象出来
- 使用IoC思想的开发方式:不通过new关键字来创建对象,而是通过IoC容器(Spring框架)来帮助我们实例化对象。我们需要哪个对象,直接从IoC容器里面去取即可。
从以上两种开发方式的对比来看:我们“丧失了一个权力”(创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)
为什么叫控制反转?
- 控制:指的是对象创建(实例化、管理)的权力
- 反转:控制权交给外部环境(IoC容器)
IoC解决了什么问题?
IoC的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?
- 对象之间的耦合度或者说依赖程度降低;
- 资源变的容易管理;比如你用Spring容器提供的话很容易就可以实现一个单例。
例如:现有一个针对User的操作,利用Service和Dao两层结构进行开发
在没有使用IoC思想的情况下,Service层想要使用Dao层的具体实现的话,需要通过new关键字在UserServiceImpl中手动new出IUserDao的具体实现类UserDaoImpl(不能直接new接口类)。
很完美,这种方式也是可以实现的,但是我们想象一下如下场景:
开发过程中突然接到一个新的需求,针对IUserDao接口开发出另一个具体实现类。因为Server层依赖了IUserDao的具体实现,所以我们需要修改UserServiceImpl中new的对象。如果只有一个类引用了IUserDao的具体实现,可能觉得还好,修改起来也不是很费力气,但是如果有许许多多的地方都引用了IUserDao的具体实现的话,一旦需要更换IUserDao的实现方式,那修改起来将会非常的头疼。
使用IoC的思想,我们将对象的控制权(创建、管理)交由IoC容器去管理,我们在使用的时候直接向IoC容器“要”就可以了
IoC和DI有区别吗?
IoC(Inverse of Control:控制反转)是一种设计思想或者说是某种模式。这个设计思想就是将原本在程序中手动创建对象的控制权交给第三方比如IoC容器。对于我们常用的Spring框架来说,IoC容器实际上就是个Map(key,value),Map中存放的是各种对象。不过,IoC在其他语言中也有应用,并非Spring特有。
IoC最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称DI)。
老马(Martin Fowler)在一篇文章中提到将IoC改名为DI,原文如下,原文地址:https://martinfowler.com/articles/injection.html。
老马的大概意思是IoC太普遍并且不表意,很多人会因此而迷惑,所以,使用DI来精确指名这个模式比较好。
AOP(Aspect Oriented Programming)
这里不会涉及太多专业的术语,核心目的是将AOP的思想说清楚。
什么是AOP?
AOP(Aspect Oriented Programming)即面向切面编程,AOP是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)。
AOP常见的通知类型有哪些?
- Before(前置通知):目标对象的方法调用之前触发
- After(后置通知):目标对象的方法调用之后触发
- AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发
- AfterThrowing(异常通知):目标对象的方法运行中抛出/触发异常后触发。AfterReturning和AfterThrowing两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
- Around(环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法
AOP解决了什么问题?
OOP不能很好地处理一些分散在多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等),这些行为通常被称为横切关注点(cross-cutting concerns)。如果我们在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。
AOP可以将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑(core concerns,核心关注点)中分离出来,实现关注点的分离。
以日志记录为例进行介绍,假如我们需要对某些方法进行统一格式的日志记录,没有使用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 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快很多。