概念
依赖注入(DI
)是一个过程;
通过该过程,对象仅通过构造函数参数
、工厂方法的参数
,或其被构造创建
或从工厂方法返回
的对象实例上设置的属性来定义其依赖关系(即: 与它们一起工作的其他对象)。 然后,容器在创建bean时注入那些依赖项。
DI
有两种方法:
① 基于构造函数的依赖注入
通过容器调用具有多个参数的构造函数来完成的,每个参数表示一个依赖项。调用带有特定参数的静态工厂方法来构造Bean
几乎是等效的。
构造函数参数解析:根据参数的类型
和顺序
进行匹配。
构造函数参数解析匹配通过使用参数的类型进行,在没有潜在的歧义的情况下,配置元数据提供参数的顺序就是构造函数参数的顺序;
当类型存在歧义的情况:可以通过type
或者index
来显示指定。
如:
type
的方式
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
xml
的方式
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
index 是从0开始的
消除歧义的另一种方法,使用构造函数参数名称
:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
要使用上面参数名称
的功能,需要启用debug flag
,以便Spring
可以从构造函数中查找参数名称。
但是如果你不想启用,那么就得使用注解的方式,(使用@ConstructorProperties
注解)如下:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
② 基于Setter
的依赖项注入
xml配置如下:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
当使用无参构造器或无参数的静态工厂方法bean
后,容器通过在bean
上调用setter
方法来完成基于setter
的DI
;
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
即:ApplicationContext
在已经通过构造函数方法注入了某些依赖项之后,它还支持基于setter
的DI
。
依赖注入的底层原理:
使用XML bean定义,带注解的组件(即以@Component
,@Controller
等进行注释的类)或在基于@Configuration类中使用@Bean方法
然后将这些资源(配置元数据)在内部转换为BeanDefinition
实例,并用于加载整个Spring IoC
容器实例。
因为可以混合使用基于构造函数的DI
和基于setter的DI
;因此,比较好的经验法则是:
强制性依赖项使用构造函数
,setter
方式用于可选依赖,需要注意的是,在setter
方法上使用@Required
注释会使该属性变成必需的依赖(不建议这么用)。
Spring
官方建议,使用构造函数
注入,因为它使你可以将应用程序组件实现为不可变对象,并确保所需的依赖项不为null
。此外,注入构造函数的组件始终以完全初始化的状态返回到客户端(调用)代码。但是如果构造函数
参数特别多的话,那么这将是怀代码的味道,这表明该类承担了太多的职责,应该对其进行重构拆分。
依赖项解析过程
1、使用配置元数据来创建并初始化ApplicationContext
。(配置元数据:描述所有Bean
的东东)
2、对于每个bean
,其依赖项都以属性
,构造函数参数
或static-factory
方法的参数的形式表示,等待实际创建bean
时,会将这些依赖项提供给bean
;
3、每个属性或构造函数参数要有实际的定义或者是容器中另一个bean
的引用;说白了就是防止出现null
的情况,要么自己new
一个,要么赋值引用;
4、值的每个属性或构造函数参数都将从指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring
可以将字符串类型的值转为所有内置类型,例如:int,long,String,boolean等。
在创建容器时,Spring容器会验证每个bean的配置。 但是,在实际创建Bean之前,不会设置Bean属性本身。 创建容器时,将创建具有单例作用域并设置为预先实例化(默认)的Bean。 范围在bean
范围中定义。 否则,仅在请求时才创建Bean。 创建和分配bean的依赖关系及其依赖关系(依此类推)时,可能会潜在的创建一个了bean图
(关系图、或者说是依赖图)。
请注意,这些依赖项之间不匹配的问题可能会在后期出现,即在第一次创建受影响的bean时。
Q:循环依赖的问题?
A: 如果主要使用构造函数注入,则可能会创建无法解决的循环依赖方案。
解决办法:
1、要么改代码
2、要么使用setter注入;(当然,这种注入方式我们一般是不推荐的)
构造注入为啥解决不了循环依赖?
构造注入解决不了:因为构造方法创建实例,每次都要new一个要构造的实例bean,而A创建时,依赖B,就去创建B,B又依赖了A,继续构造A,如此循环下去 A(B) B(A) A(B)->…
那么我们得思考下,为什么setter
注入不会产生循环依赖的问题?
解决办法:使用三级缓存。
在单例模式
下,Spring
使用三级缓存来解决循环依赖的问题。
我们假设现在有这样的场景AService
依赖BService
,BService
依赖AService
1. `AService`首先实例化,实例化通过`ObjectFactory`半成品暴露在三级缓存中
2. 填充属性`BService`,发现`BService`还未进行过加载,就会先去加载`BService`
3. 再加载`BService`的过程中,实例化,也通过`ObjectFactory`半成品暴露在三级缓存
4. 填充属性`AService`的时候,这时候能够从三级缓存中拿到半成品的`ObjectFactory`
循环依赖的解决
使用三个缓存来解决循环依赖的问题的自我的理解:
spring使用三级缓存来解决。
如果只是为了解决简单的循环依赖,那么其实只要二级缓存就行了,在bean被实例化之后,就会以早期依赖提前暴露出去。
举例:serviceA 实例化话后,接着做属性填充,发现依赖了serviceB,这个时候把serviceA半成品(未完成)放入earlySingletonObjects集合中,接着实例化serviceB,在对其进行属性填充时,发现依赖serviceA,然后去二级缓存(earlySingletonObjects)中找,发现找到后,完成初始化,接着又去完成serviceA的初始化工作。
复杂情况 :aop
代理的情况
在serviceA
需要依赖serviceB
,需要的是其代理,这个时候如果只使用二级缓存,那么那么就得在实例化serviceB
之后,立马又生成代理对象,这不符合spring
设计(应该在最后处理代理的情况),所以引入了三级缓存,在这个三级缓存是存放的是工厂对象,通过这些工厂对象,调用工厂方法getObject()方法来获取早期依赖bean
,如果是代理,该方法就会生成并返回代理对象。
小结:
bean
在实例化之后,会写入三级缓存相应的工厂方法,如果没有aop
,那么方法返回的就是原始bean
,否则就返回代理对象(里面包含target
即原始bean
)。- 之后再删除三级缓存,把早期
bean
移到二级缓存中; - 在完成填充和初始化后,移动到一级缓存中。
Q: 默认情况下ApplicationContext
实现会预先实例化单例bean呢?
A: 这是因为在实际创建Bean
时,Spring
设置属性并尽可能晚地解决依赖关系。
这意味着,如果创建对象或其依赖项之一有问题,则在正确加载了Spring
的容器以后,当请求对象时会生成异常-例如,由于缺少或无效,bean
引发异常属性。这可能会延迟某些配置问题的可见性。为了解决这个问题,在实际需要这些bean
之前需要花一些前期时间和内存来创建它们,会在创建ApplicationContext
时发现配置问题,而不是稍后。当然,我们可以覆盖此默认行为(预先实例化
),以便单例bean
延迟初始化,而不是预先实例化。
如果是不存循环依赖的情况,比如beanA
依赖beanB
,则springIOC容器会调用setter
方法之前完全配置beanB
。换句话说,一旦实例化该bean
(如果它不是预先实例化的单例),则设置其依赖关系,并调用相关的生命周期方法(例如配置的init
方法或InitializingBean
回调方法)。
如果是循环依赖的话, 不会立马设置其依赖关系,而是会提前暴露一个半成品。
对其他Bean的引用(协作者)
<ref/>
在 <constructor-arg/> or <property/>
都有,用于引用容器中的另一个bean
,如果引用的bean
没有初始化,那么就会对其进行初始化再设置。(如果是单例的话,容器可能早已初始化了)。
<ref bean="someBean"/>
这个someBean
可以是另一个bean
的id
或者name
,都行。
还有一个层次用法,子容器里引用父容器中的bean
;
父容器
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
</property>
<!-- insert other configuration and dependencies as required here -->
</bean>
这里可能会问,什么是父子容器?
父子容器
典型的父子容器就是spring
和springmvc
同时使用的时候。分别ContextLoaderListener
创建的 容器是父容器,DispatcherServlet
创建的容器是子容器。
而父容器和子容器的区别。比如父容器有a.b.c
三个bean
对象,子容器有d.e.f
三个bean
对象,子容器就可以通过getBean
方法调用父容器的a.b.c bean
对象,而父容器不能通过getBean
拿到子容器的d.e.f
三个bean
对象。但是这里有一个例外property-placeholder
,容器中读取的配置文件就是私有的,互相不能访问。其中也要弄清楚的是父子容器并不是一种包含关系,而是平行关系,但是在子容器中有一个parent
,指向父容器,也就是说子容器在通过getBean
访问父容器中的bean
对象时是通过parent
访问。
这种做法的实际意思就是在一个JVM
,只有一个树状结构的容器树。可以通过子容器访问父容器资源。就比如在实际开发中使用ssm
框架,spring
可以管理service
、mapper
,springmvc
管理controller
、mybatis
编写mapper
、controller
就需要调用service
,service
调用mapper
,因为springmvc
容器是spring
的子容器,可以通过父容器找到service
和mapper
,但是在service
中却是找不到controller
的。保证一种资源的局部性。
参考地址: https://www.cnblogs.com/xuelin1221/p/10053059.html
内部bean
在<property/>
or <constructor-arg/>
标签内使用<bean>
包裹的元素就是内部bean
。
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
内部bean
定义不需要定义的ID
或名称
。 如果指定,则容器不使用该值作为标识符
。 容器在创建时也将忽略作用域
标志,因为内部bean
始终是匿名的,并且始终与外部bean
一起创建。 不能独立访问内部bean
,也不能将它们注入到协作bean
中除非封装在里面。
内部bean
通常只共享其包含bean
的作用域。但是也有特殊情况,可以从自定义作用域中接收销毁回调,例如对于单例bean中包含的请求范围内的bean。 内部bean实例的创建与其包含的bean绑定在一起,但是销毁回调使它可以参与请求范围的生命周期,当然这种情况不常见。