IOC(Inversion of Control)控制反转
控制反转是软件设计大师 Martin Fowler在 2004 年发表的“Inversion of Control Containers and the Dependency Injection pattern”提出的。这篇文章系统阐述了控制反转的思想,提出了控制反转有依赖查找和依赖注入实现方式。
什么是控制反转?IOC 也被称为好莱坞原则:“不要给我们打电话,我们会给你打电话”[好莱坞经纪人总是给别人打电话,而不让别人打给他们],怎么理解这句话呢,如果我们主动给经纪人打电话,这时候的控制权其实是在我们自己的手里,我可以决定打不打,什么时候打,而当我们遵守好莱坞原则的时候,控制权就不再是在我们自己的手里,而是在经纪人的手里,这时候就由经纪人决定什么时候打,打不打了。控制权也因此进行了反转。
在程序中怎么理解呢?当我们写代码的过程中,我们需要重新创建一个新的对象,我们可以直接通过 new 的形式,这时候控制权是在我们自己手里的,而当我们把这个控制权转移到由框架管理的时候,我们不再关注我们自己什么时候创建这个对象,而由框架帮我们进行创建和管理,这就是控制反转。控制反转是一种设计原则,用于降低代码耦合度。在传统程序设计中,开发者自己创建对象并管理对象之间的依赖,而通过 IOC,控制权交给框架或容器来处理。
作用:减少代码耦合,提升代码的可测试性和可维护性。提供对象管理机制,简化开发。
应用场景:IOC 的核心是将对象的创建与管理权交给容器(例如 Spring 容器),开发者通过配置或注解告诉容器对象之间的依赖关系,容器负责依赖注入。常见于 Spring 框架,Spring 的核心理念就是 IOC,容器负责管理对象的创建、生命周期和依赖。
DI(Dependency Injection,依赖注入)依赖注入(控制反转的一种形式)
依赖注入是一种实现控制反转的方式,它通过容器将对象的依赖(即它所依赖的其他对象)注入到对象内部。
假设存在一个类 A ,A 有个属性是类 B,我们在创建 A 的时候,必须得先创建 B ,然后 A 通过 set 设置属性 B,这个过程,如果由我们自己手动控制,控制权是在我们自己手里。而通过依赖注入,上面给 A 设置属性 B 的这个过程,我们不再需要自己进行控制。这是怎么做到的呢? (这就可以通过很多框架来做了:Spring,Guice,PicoContainer)框架会自动通过构造器注入、Setter 方法注入或接口注入,依赖对象被传递给目标对象,而不是目标对象主动去获取依赖。
作用:提高代码的灵活性和可测试性。解耦依赖对象,提升代码的模块化程度。
应用场景:广泛应用于 Java 框架(如 Spring)中,尤其在管理复杂依赖关系时非常有用。
JNDI(Java Naming and Directory Interface,Java 命名和目录接口)
JNDI 是 Java 中的 API,用于访问命名和目录服务。它可以用来查找和管理网络中资源的引用,如数据库连接、EJB 组件、消息队列等。JNDI 提供一种标准化方式,通过名称(如字符串)来定位和访问外部资源。在企业级应用中,JNDI 通常用于获取数据源、EJB 等。
作用:提供统一的资源查找方式。便于企业级应用进行资源的集中管理。
应用场景:常见于 Java EE 环境中,用于从应用服务器中查找数据源、EJB、邮件会话等资源。
AOP(Aspect-Oriented Programming,面向切面编程)
AOP 是一种编程范式,旨在通过分离横切关注点(cross-cutting concerns)来增强代码的模块化。横切关注点是指那些在多个模块中重复出现的功能,如日志、事务管理、安全等。
术语
# 与大多数技术一样,AOP已经形成了自己的术语。描述切面的常用术语有
# 通知(advice)、切点(pointcut)和连接点(join point)
# 通知(**Advice**)
定义:通知是指在程序执行的某个特定时刻执行的代码片段。它是面向切面编程中的核心部分,用来在特定位置执行额外的逻辑。
作用:通知是定义具体行为的地方,比如记录日志、处理异常、管理事务等。
类型:
• 前置通知(Before Advice):在方法执行之前执行的通知。
• 后置通知(After Advice):在方法执行之后执行的通知。
• 返回通知(After Returning Advice):方法正常返回结果后执行的通知。
• 异常通知(After Throwing Advice):方法抛出异常时执行的通知。
• 环绕通知(Around Advice):在方法执行前后执行的通知,甚至可以完全控制方法的执行过程。
例子:
@Around("execution(* com.example.service.*.*(..))")
public Object logMethodCall(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method execution");
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("After method execution");
return result;
}
# 切点(Poincut)
定义:切点是用来定义在哪些地方插入通知的表达式。它指定在何处(哪些方法或类)执行通知。
作用:切点定义了通知应该应用于哪些连接点,可以通过表达式进行灵活筛选,例如指定某个包下的所有方法、某个类中的特定方法等。
表达式例子:
• execution(* com.example.service.*.*(..)):匹配 com.example.service 包下的所有类的所有方法。
• within(com.example.controller..*):匹配 com.example.controller 包及其子包中的所有类。
# 连接点(Join point)
定义:连接点是指程序执行过程中的一个具体点,通常是方法调用。AOP 允许在这些连接点上插入通知。
理解:连接点可以理解为程序执行时的某个特定时刻,比如一个方法的开始或结束。所有的方法调用都是潜在的连接点。
作用:通知通过切点选择连接点并插入相应逻辑。例如,在方法执行前或执行后插入日志记录就是选择了连接点。
# 切面(Aspect)
定义:切面是 AOP 的核心概念,指的是横切关注点的模块化封装。切面是由通知和切点组合而成的,即在哪些地方(切点)执行哪些逻辑(通知)。
理解:切面相当于一块“横切关注点”的代码,比如日志记录、事务管理等,切面将这些逻辑与业务逻辑分离开来,增强了代码的可维护性。
例子:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethod() {
System.out.println("Method called");
}
}
# 引入(Introduction)
定义:引入允许我们在不修改原始类的情况下,为类添加新的方法或属性。
理解:引入通常用于动态地为现有类添加新的接口实现,而不需要改变类的定义。这使得我们可以为现有类添加新的行为。
例子:通过引入一个新的接口实现来为类添加额外的功能。
# 织入(Weaving)
定义:织入是指将切面应用到目标对象的过程。这是将通知(Advice)嵌入到目标对象的代码中的操作。
理解:织入的结果是产生一个增强了的代理对象,这个代理对象包含了切面定义的逻辑。在应用程序运行时,织入机制将通知代码插入到合适的连接点上。
织入时机:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译 器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特 殊的类加载器(ClassLoader),它可以在目标类被引入应用 之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织 入切面时,AOP容器会为目标对象动态地创建一个代理对象。 Spring AOP就是以这种方式织入切面的。
AOP 的概念总结
| 名词术语 | 定义与作用 | 举例 |
|---|---|---|
| 通知 | 在连接点上执行的代码,定义了何时执行特定的逻辑(如日志、事务)。 | @Before、@Around、@After |
| 切点 | 定义了在哪些连接点执行通知的表达式,指定哪些方法或类需要被增强。 | execution(* com.example.service. . (..)) |
| 连接点 | 程序执行过程中的一个具体点,通常是方法的执行,是通知可能执行的地方。 | 方法调用、异常抛出等 |
| 切面 | 横切关注点的模块化封装,由通知和切点组合而成。切面定义了“在哪里做什么”。 | 一个记录日志的切面,在所有 service 方法执行前后记录日志 |
| 引入 | 为类动态添加新的方法或属性,不修改原始类。 | 为一个类添加一个新的接口实现,例如为现有类添加新的功能 |
| 织入 | 将切面应用到目标对象的过程,生成一个增强后的代理对象,在编译时、类加载时或运行时进行。 | Spring AOP 运行时通过代理类将切面逻辑织入到目标对象中 |
循环依赖
什么是循环依赖?
循环依赖就是循环引用。
比如ClassA 类中存在一个属性,这个属性的类型是 ClassB,而ClassB类中又存在一个属性,属性类型是ClassA,
当创建ClassA时候,因为属性需要引入ClassB,所以就需要创建ClassB,可是ClassB中属性又需要引入ClassA,这时候又需要创建ClassA,但是ClassA又要等待ClassB的创建,这就形成了循环依赖了。如果不解决循环依赖问题,那么就造成了死循环,最后就内存溢出了
事例:
@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
@Service
public class A {
@Autowired
private A a;
}
如何解决呢???
这里我们先补充一些概念
完整的Bean :指new出来的对象,并且对象中所有的属性都已经初始化
不完整的Bean:指只是刚刚new出来,属性什么的都还没初始化
在 Spring中创建 Bean 分三步:
- 实例化,createBeanInstance,就是 new 了个对象。(这个时候,我们称这个对象是一个不完整对象,为什么?因为对象的属性还没有set),
- 属性注入,populateBean, 就是 set 一些属性值。
- 初始化,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等。
Spring获取对象的顺序,刚好是遵守以下这个缓存规则的,从一级开始获取到三级,直到从中获取到。
- 第一级缓存〈也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象(完整的 Bean)
- 第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(不完整的对象,因为属性还未填充完整)
- 第三级缓存:Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂
现在开始,我们可以解决循环依赖了
(1)构造器注入引起的循环依赖:这个解决不了
Spring中,如果是通过构造器,引入对象的依赖的话,造成的循环依赖问题,Spring会直接抛出异常。
奇怪了,不是说解决循环依赖?怎么这里又直接异常了呢?我们来梳理以下。
如果全是构造器注入,比如A(B b),那表明在 new 的时候,就需要得到 B,此时需要 new B 。
但是 B 也是要在构造的时候注入 A ,即B(A a),这时候 B 就找到A了,因为A的构造函数还没执行结束,A还不存在呢 ,所以发现找不到。这不就卡死了
(2)setter 引起的循环依赖:这个可以解决
Spring中只能解决单例作用域的循环依赖,是通过Spring容器提前暴露刚完成构造器实例化后注入但是未能完成其他步骤,(比如属性的setter,初始化)的bean来完成的。什么意思呢?
当我们创建A的时候,A的构造器执行结束后,说明A已经存在于内存中了,但是这个时候的A还是不完整的对象,Spring会把A放入第二级缓存中,这时候发现A依赖B,于是创建B,B的构造器执行结束后,说明B也已经存在于内存中了,B也是一个不完整的对象,Spring也会把B放入第二级缓存中,这个时候,B发现他依赖A,于是从第二级缓存中取出A,将A setter进入 B,这个时候,B就成了完整的对象了,从二级缓存中放入一级缓存,这个时候A 在从一级缓存中获取B ,并 setter B