写点什么

Spring 5 中文解析核心篇 -IoC 容器之依赖关系

用户头像
青年IT男
关注
发布于: 2020 年 09 月 04 日
Spring 5 中文解析核心篇-IoC容器之依赖关系

一个典型的企业应用不是由一个简单的对象(在Spring中叫 bean)组成。即使是最简单的应用程序,也有一些对象协同工作,以呈现最终用户视为一致的应用程序。(备注:相当于所有的 bean 一起协同工作对于用户是无感知的)。下一部分将说明如何从定义多个独立的 Bean 对象协作去实现应用程序的目标。


###### 1.4.1 依赖注入


依赖注入是从工厂方法构造或返回的实例并通过设置对象实例的构造参数、工厂方法参数或者属性去定义它的依赖关系(与它一起工作的对象)的过程。当创建bean的时候容器注入这些依赖。从根本上讲,此过程是通过使用类的直接构造或服务定位器模式来控制bean自身依赖关系的实例化或位置的bean本身的逆过程(因此称为控制反转)。


DI(依赖注入)使代码更简洁和解偶,当为这些对象提供依赖时候是更高效的。(通过依赖注入来注入对象更高效)。对象不用去主动查找它的依赖项并且不用知道依赖类的位置。这样的结果是我们的类变成了更容易被测试,特别地,当这些依赖在接口或者抽象类上时,在单元测试中去使用stub或者mock方式去实现这些接口和抽象类。


DI(依赖注入)存在两个重要的变体:基于构造函数的依赖注入和[基于 Setter 的依赖注入](https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/core.html#beans-setter-injection)


  • 基于构造函数依赖注入


基于构造函数的 DI(依赖注入)是通过容器调用构造函数完成的,每一个构造函数参数代表一个依赖。调用带有特定参数的静态工厂方法来构造 Bean 几乎是等效的,并且本次讨论将构函数和静态工厂方法的参数视为类似的。下面的例子展示了只能通过构造函数进行对象注入:


 public class SimpleMovieLister {

// the SimpleMovieLister has a dependency on a MovieFinder
private MovieFinder movieFinder;

//构造函数注入MovieFinder对象
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// business logic that actually uses the injected MovieFinder is omitted...
}
复制代码


注意:这个类没有任何特别的。它是一个 POJO 类并且没有依赖容器特定接口、基础类或注解。


  • 构造函数参数解析


构造参数解析匹配是通过使用参数的类型进行的。如果 bean 定义的构造函数参数中不存在潜在的歧义,在 bean 定义中定义构造函数参数的顺序是在实例化 bean 时将这些参数提供给适当的构造函数的顺序。考虑下面的类


  package x.y;

public class ThingOne {

public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
复制代码


假设ThingTwoThingThree类没有继承关系,不存在潜在的歧义。因此,这个配置工作的很好并且我们没有必要显示的在<constructor-arg/>元素中指定构造函数参数的索引或类型。


  <beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>

<bean id="beanTwo" class="x.y.ThingTwo"/>

<bean id="beanThree" class="x.y.ThingThree"/>
</beans>
复制代码


当引用另一个 bean 时,类型是已知的,可以进行匹配。当一个简单类型被使用,例如<value>true</value>,Spring 不能确定这个值的类型,因此在没有类型的帮助下是不能被匹配的。考虑下面的类:


 package examples;

public class ExampleBean {

// Number of years to calculate the Ultimate Answer
private int years;

// The Answer to Life, the Universe, and Everything
private String ultimateAnswer;

public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
复制代码


  • 构造函数参数类型匹配


在前面的场景中,如果我们通过使用type属性明确指定了构造函数参数类型,容器会使用简单类型进行匹配。像下面的例子:


  <bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
复制代码


  • 构造函数参数索引


我们可以明确指定构造函数参数的索引通过index属性,像下面例子:


  <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>
复制代码


请记住,要使此工作开箱即用,必须在启用调试标志的情况下编译代码,以便 Spring 可以从构造函数中查找参数名称。如果不能或不想在 debug 标记下编译代码,可以使用 JDK 注解@ConstructorProperties 去明确构造函数参数的名称。参考下面例子:


package examples;

public class ExampleBean {

// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
复制代码


  • 基于 Setter 依赖注入


基于SetterDI(依赖注入)是在 bean 调用无参构造函数或无参static工厂函数去实例化bean后被容器调用函数去完成的。


下面的例子展示了一个只能通过使用Setter注入的类。这个类是常规的 Java。它是一个 POJO 并且没有依赖容器特定接口、基础类或者注解。


 public class SimpleMovieLister {

// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;

// 通过Setter进行注入
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// business logic that actually uses the injected MovieFinder is omitted...
}
复制代码


ApplicationContext支持基于构造函数和Setter的依赖注入的bean管理。也支持在通过构造函数注入后再通过基于Setter方法注入。可以使用BeanDefinition的形式配置依赖项,将其与PropertyEditor实例结合使用以从一种格式转换为另一种格式。然后,大多数Spring用户不会直接使用这些类,而是使用 XML 的 bean 定义、注解这些组件(类似@Component, @Controller等等),或者基于 Java 被标注@Configuration类的方法使用@Bean标注。这些配置数据源内部地转换为BeanDefinition实例并且被使用于加载整个Spring IoC容器实例。


> ​ **基于构造函数和Setter函数注入选择**

>

> 由于可以混合基于构造函数和Setter函数的依赖注入,<u>将构造函数用于强制性依赖项,将Setter方法或配置方法用于可选性依赖项是一个很好的经验法则</u>。需要注意在Setter方法上使用 @Required表明这个属性需要一个依赖;然而,构造函数注入可以通过编程方式校验参数是可取的。

>

> Spring团队一般推荐使用构建函数注入,因为可以允许我们去实现不可变的对象组件并且确保需要的依赖不为null。此外,构造函数注入组件总是被返回一个完整初始化的状态。构造函数大量的参数是一个坏代码味道,暗示着这个有太多的责任并且应该去重构以更好的分离关注点问题。

>

> Setter注入应该主要使用在可选依赖因为可以在在类中设置一个默认值。否则,必须在代码使用依赖项的任何地方执行非空检查。Setter注入的一种好处是可以在后面对Setter方法进行重新配置或重新注入。

>

> 使用对特定类最有意义的 DI 样式,有时候,当处理第三方类库没有源码的时候,这个选择是非常适合的。例如:如果第三方类库没有暴露任何的Setter方法,构造函数注入可能是依赖注入的唯一有效方式。


  • 依赖解析处理


容器执行 bean 依赖解析过程:


* ApplicationContext通过对所有 bean 的配置元数据描述进行创建和初始化。配置元数据通过XMLJavaConfig或者注解描述。

* 对于每个bean,它的依赖形式通过属性、构造函数参数或者static-factory(如果使用常规的构造函数替换)方法参数表达。当这个 bean 被创建的时候,这些依赖被提供给bean。(备注:被依赖 bean 先创建)

* 每一个属性或者构造函数参数的都要设置一个实际的定义,或引用容器其他bean

* 每一个属性或构造函数参数的值从指定的格式转换为属性或构造函数参数的真实类型。默认情况下,Spring 提供一个字符串格式转换为所有内建类型的值,例如:int、long、String、boolan 等等。


Spring 容器验证每一个创建 bean 的配置。然而,bean 属性本身没有被设置,直到 bean 被真正创建。在创建容器时,将创建单例作用域的 bean 并将其设置为预实例化(缺省值)。Scope 被定义在 Bean Scopes。除此之外,其他的 bean 创建仅仅在请求容器的时候。bean 的创建潜在的导致一些 bean 的图被创建(备注:意思是 bean 所依赖的 bean 被创建,类似于 bean 的依赖拓扑图),类似 bean 的依赖和它的依赖的依赖 bean 创建和被赋值。注意:这些依赖项之间的解析不匹配可能会在第一次创建受影响的 bean 时出现。


> ​ 循环依赖

>

> 如果主要使用构造函数注入,则可能会创建无法解决的循环依赖场景。

>

> 例如:类A通过构造函数需要依赖注入类B并且类B通过构造函数依赖注入A。如果配置类A和类B相互依赖注入,**Spring IoC**容器在运行时检测到循环依赖会抛出一个BeanCurrentlyInCreationException异常。

>

> 一种解决方法是编辑类的源码通过Setter而不是构造函数注入。或者避免使用构造函数注入而是仅仅使用Setter方法注入。换句话说,虽然它是不推荐使用的,我们可以通过Setter注入配置循环依赖对象。与典型情况(没有循环依赖关系)不同,Bean A和 Bean B之间的循环依赖关系迫使其中一个 Bean 在完全初始化之前被注入另一个 Bean(经典的“鸡与蛋”场景)


通常,你可以相信Spring做的事情是正确的。容器会在加载时候检测配置问题,例如:引用不存在的bean、循环依赖。当这个bean被真正创建的时候,Spring设置属性并且尽可能晚的解析依赖。这意味着如果创建该对象或其依赖项时遇到问题,则已正确加载的Spring容器可能在你请求对象时生成异常-例如,这个bean抛出一个错误或无效的属性异常结果。这可能会延迟某些配置问题的可见性,这就是为什么默认情况下 ApplicationContext 实现会预先实例化单例 bean 的原因。在实际需要使用这些 bean 之前要花一些前期时间和内存,你会在创建 ApplicationContext 时发现配置问题,而不是以后(使用 bean 的时候)。你可以覆盖这个默认行为,这样单例 bean 就可以惰性地初始化,而不是预先实例化。


如果不存在循环依赖关系,则在将一个或多个协作 bean 注入到依赖bean中时,每个协作bean在注入到依赖bean中之前都已完全配置。也就是,如果**bean A**有依赖**bean B**,Spring IoC容器在调用**bean A**的Setter方法之前完整的配置**bean B**。换句话说,这个 bean 被实例化,他的依赖被设置并且关联的生命周期函数(例如:init方法或者[InitializingBean 回调函数](https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/core.html#beans-factory-lifecycle-initializingbean))已经被调用。


  • 依赖注入例子


下面的例子基于 XML 配置元数据去配置基于Setter的依赖注入。Spring XML 配置文件指定一些 bean 的定义:


<bean id="exampleBean" class="examples.ExampleBean">
<!-- 属性注入:依赖一个bean -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- 属性注入:依赖一个bean-->
<property name="beanTwo" ref="yetAnotherBean"/>
<!-- 属性值填充-->
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
复制代码


下面定义ExampleBean的类


 public class ExampleBean {

private AnotherBean beanOne;

private YetAnotherBean beanTwo;

private int i;

// 指定属性需要注入bean类型
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}

// 指定属性需要注入bean类型
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}

public void setIntegerProperty(int i) {
this.i = i;
}
}
复制代码


上面的例子,在 XML 文件中通过Setter声明属性匹配类型。下面例子使用构造函数注入:


  <bean id="exampleBean" class="examples.ExampleBean">
<!-- 构造函数注入bean anotherExampleBean-->
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<!-- 构造函数注入bean yetAnotherBean-->
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
复制代码


下面例子对于ExampleBean的类定义


public class ExampleBean {

private AnotherBean beanOne;

private YetAnotherBean beanTwo;

private int i;

//需要在构造函数中声明需要依赖的类型定义
public ExampleBean(
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
this.beanOne = anotherBean;
this.beanTwo = yetAnotherBean;
this.i = i;
}
}
复制代码


bean定义中指定的构造函数参数用作ExampleBean构造函数的参数(意思是 xml 中指定的构造函数引用作为ExampleBean构造函数的参数)。


现在考虑这个例子的变体,使用构造函数替换,Spring调用static工厂方法去返回对象的实例:


  <bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">    <constructor-arg ref="anotherExampleBean"/>      <constructor-arg ref="yetAnotherBean"/>    <constructor-arg value="1"/>  </bean>    <bean id="anotherExampleBean" class="examples.AnotherBean"/>  <bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
复制代码


ExampleBean对应的类定义:


public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}

//静态工厂方法
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
复制代码


静态工厂方法的参数由<constructor-arg />元素提供,与实际使用构造函数时完全相同。通过工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在此例中为相同)。实例(非静态)工厂方法可以以基本上相同的方式使用(除了使用 factory-bean 属性代替 class 属性之外),因此不在这里详细讨论。


> 参考代码:com.liyong.ioccontainer.starter.XmlDependecyInjectContainer


###### 1.4.2 详细介绍依赖项和配置


在前面的章节提到,我们可以定义 bean 的属性和构造函数参数去引用其他被管理的 bean(协同者)或者作为一个内联值定义。Spring 的 XML 元数据配置支持子元素类型<property/>和<constructor-arg/>这个为了这个设计目的。


  • 直接值(原生类型、字符串等)


<property />元素的 value 属性将属性或构造函数参数指定为人可读的字符串表示形式。Spring 的conversion service 被使用从字符串转换属性或参数的实际类型。下面的例子展示各种值的设置:


 <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="masterkaoli"/>
</bean> <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="masterkaoli"/>
</bean>
复制代码


下面的例子使用p命名空间使xml配置方式更简洁:


 <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!--使用p命名空间 注意: xmlns:p="http://www.springframework.org/schema/p -->
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="root"
p:password="masterkaoli"/>

</beans>
复制代码


前面的 XML 配置非常的简洁,但是,拼写错误在运行时被发现而不是在设计时,除非我们使用 IDE(例如:Intellij IDEA或者Spring Tools)支持自动属性完成当我们创建bean定义的时候。IDE 助手是非常推荐的。


我们可以配置java.util.Properties实例,例如:


<bean id="mappings"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<!-- java.util.Properties类型配置 -->
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
</value>
</property>
</bean>
复制代码


Spring 容器通过使用JavaBeansPropertyEditor 机制转换<value/>元素值为java.util.Properties。这是一个不错的捷径,并且是 Spring 更喜欢使用嵌套的<value />元素而不是 value 属性样式。


  • idref元素


idref元素是一个简单的容错方式,将容器中另外 bean 的 id 传递(字符串值-不是引用)到 <constructor-arg/>或<property/>元素。下面的例子展示怎样去使用:


 <bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
复制代码


前面 bean 定义片段和下面的片段相同:


  <bean id="theTargetBean" class="..." />  <bean id="client" class="...">      <property name="targetName" value="theTargetBean"/>  </bean>
复制代码


> 参考代码:com.liyong.ioccontainer.starter.XmlIocContainer


第一种形式优于第二种形式,因为使用idref标记可使容器在部署时验证所引用的名 Bean 实际上是否存在。在第二个变体中,不对传递给客户端 bean 的 targetName 属性的值执行验证。拼写错误仅在实际实例化客户端 bean 时才发现(最有可能导致致命的结果)。如果这个客户端 bean 是原型 bean,这个拼写和结果异常可能在这个容器部署后很久才被发现。


> 在idref标签元素上的的local属性在spring-beans.xsd 4.0后不在支持。因为它没有提供常规的bean引用值。当升级到spring-beans.xsd 4.0更改idref localidref bean


idref标签元素带来的价值的地方是在ProxyFactoryBean bean定义中配置AOP拦截器。指定拦截器名称时使用<idref/>元素可防止你拼写错误的拦截器 ID。


  • 引用其他的 bean


ref元素是<constructor-arg/> 或<property/>中定义的最后一个元素。在这里,我们通过设置一个bean的指定属性的值引用被容器管理的其他bean。引用的bean是要设置其属性的bean的依赖关系,并且在设置属性之前根据需要对其进行初始化。(如果这个协同者是一个单例bean,它可能已经被容器初始化)所有的引用最终引用其他对象。bean的范围和校验依赖你是否有指定其他对象通过bean或者parent属性指定的id或者name


指定目标 bean 通过<ref/>标签的 bean 属性是最常见的形式并且允许在同一个容器或父容器引用任何的被创建的bean,而不管是否在同一个XML配置文件。bean 属性的值可以与目标 bean 的 id 属性相同,也可以与目标 bean 的 name 属性中的值之一相同。下面例子展示怎样使用ref元素。


 <bean id="userService" class="com.liyong.ioccontainer.service.UserService"> 
<!--属性注入 保存一种方式就可以-->
<property name="bookService">
<ref bean="bookService"/>
</property>
</bean>
复制代码


通过parent属性指定目标bean的引用,这个bean在当前容器的父容器中。parent属性的值可以与目标Beanid属性或目标Beanname属性中的值之一相同(id或者name指定引用)。这个目标bean必须在父容器中。当你有一个分层的容器并且你想去包装一个在父容器中存在的bean为代理对象同时有一个相同的名字作为这个父bean,你应该主要的使用这个bean应用的变体。下面的两个例子展示类怎样使用parent属性。


   <!--在父容器上下文-->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required as here -->
</bean>
复制代码


<!-- 在子容器上下文 -->
<!-- 产生一个代理bean,bean name is the same as the parent bean -->
<bean id="accountService"
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>
复制代码


  • 内部 bean


<property/>或<constructor-arg/>元素在<bean/>内定义内部 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定义 -->
<bean class="com.example.Person">
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
复制代码


一个内部bean定义不需要定义一个id或名称。如果指定名称,这个容器不会使用这个值作为标识(不会使用定义的作为idname标识)。容器在创建内部 bean 或忽略Scope(作用域),因为内部bean总是匿名的并且总是依赖外部bean的创建。不可能独立访问内部bean或将它们注入到协作bean中而是封装在bean中。一个极端的情况,可能接受定制的 Scope 的销毁回调-例如:一个请求域内部bean包含在一个单例bean中。内部 bean 实例的创建与其包含的 bean 绑定在一起,但是销毁回调使它可以参与请求范围的生命周期。这不是常见的情况。内部 bean 通常只共享其包含 bean 的作用域。


> 参考代码:com.liyong.ioccontainer.starter.XmlOutterInnerBeanIocContainer


  • 集合


<list/>、<set/>、<map/>和<props/>元素分别设置 Java 集合类型 List、Set、Map 和 Properties 的属性和参数。下面例子展示怎样使用:


<bean id="moreComplexObject" class="example.ComplexObject">
<!-- adminEmails属性为Properties类型。 results in a setAdminEmails(java.util.Properties) call -->
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>
<!--someList属性为java.util.List类型。 results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!--someMap属性类型为:java.util.Map。 results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key ="a ref" value-ref="myDataSource"/>
</map>
</property>
<!--someSet属性类型为:java.util.Set。 results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>
复制代码


Map key 的值、值、或者 set 的值,可以是任何下面元素:


 bean | ref | idref | list | set | map | props | value | null
复制代码


> 参考代码:com.liyong.ioccontainer.starter.XmlCollectionsIocContainer


  • 集合合并


Spring 容器也支持对集合合并。应用程序开发人员可以定义<list/>、<map/>、<set/>或<props/>元素有一个子元素集合<list/>、<map/>、<set/>或<props/>继承和覆盖父集合元素。因此,子集合的值是父集合和子集合合并元素后的结果,也就是子集合元素会覆盖父集合的元素值。


在合并章节讨论父-子 bean 的机制。不熟悉父 bean 和子 bean 定义的读者可能希望先阅读相关部分,然后再继续。


下面的例子展示集合的合并:


 <beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.com</prop>
<prop key="support">support@example.com</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- the merge is specified on the child collection definition -->
<props merge="true">
<prop key="sales">sales@example.com</prop>
<prop key="support">support@example.co.uk</prop>
</props>
</property>
</bean>
<beans>
复制代码


注意:在childbean定义的属性adminEmails的<props/>元素的属性是merge=true。当这个child bean 被容器解析和初始化的时候,这个结果实例有adminEmails Properties集合,这个集合包含了子集合和父集合的 adminEmails 合并集。结果列表:


administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk
复制代码


为了支持在父Properties值被覆盖,子Properties集合的值从父<props/>中继承所有的值和子Properties的值(备注:意思是子 Properties 会覆盖父Properties中重复的值)。


这个合并行为适用类似<list/>、<map/>、<set/>集合类型 。在<list/>元素的特定情况下,将维护与 List 集合类型关联的语义(即,值有序集合的概念)。父元素的值优先与所有的子元素值。在MapSetProperties集合类型中不存在顺序。因此,对于容器内部使用的关联MapSetProperties实现类型下的集合类型,没有有效的排序语义。


  • 集合合并限制


我们不能合并不同集合类型(例如:Map 和 List)。如果尝试去合并将会抛出一个 Exception 异常。这个 merge 属性必须被指定在子类中。在父集合定义中指定 merge 属性是多余的并且不会达到预期结果。


  • 强类型集合


在 Java 5 中泛型被引入,我们可以使用强类型集合。因此,仅仅包含String元素的集合声明成为可能。如果我们使用 Spring 去依赖注入一个强类型的 Collection 到一个 bean 中,可以利用 Spring 的类型转换在添加到 Collection 集合前对集合实例元素转换为适合的类型。下面的 java 代码和 bean 定义展示了怎样去使用:


 public class SomeClass {

private Map<String, Float> accounts;

public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
复制代码


  <beans>
<bean id="something" class="x.y.SomeClass">
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
</bean>
</beans>
复制代码


当准备注入something **bean**的accounts属性时,可以通过反射获得有关强类型Map <String,Float>的元素类型的泛型信息。因此,Spring的类型转换基础设施识别各种元素Float类型的值并且这些字符串值能够被转换为真实的Float类型。


  • Null 和空字符串值


Spring 将属性等的空参数视为空字符串。下面基于 XML 的配置元数据片段设置了 email 属性为空字符串


 <bean class="ExampleBean">
<property name="email" value=""/>
</bean>
复制代码


和下面 java 代码相等:


 exampleBean.setEmail("");
复制代码


<null/>元素被处理为 null 值,下面展示例子:


 <bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
复制代码


和下面 java 代码相等:


 exampleBean.setEmail(null);
复制代码


  • p-namespace快捷方式


p-namespace让你使用bean元素的属性(嵌入元素<property/>替换)去描述你的属性值协同者 bean,或者两种都使用。


Spring 支持可扩展的namespace配置格式,基于 XML Schema 定义。本章讨论的 bean 配置格式在 XML Schema 文档中定义。然而,p-namespaceXSD 文件中没有被定义仅仅在Spring Core中存在。


下面例子展示两个 XML 片段其解析结果是一致的.


<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
</bean>

<bean name="p-namespace" class="com.example.ExampleBean"
p:email="someone@somewhere.com"/>
</beans>
复制代码


该示例显示了p-namespace中的一个属性,该属性在 bean 定义中称为email。这告诉Spring包含一个属性的声明。前面提到,p-namespace没有schema定义,因此你可以设置属性名称为property(类字段)名称。


下一个示例包括另外两个 bean 定义,它们都有对另一个 bean 的引用:


 <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>

<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>

<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>
复制代码


这个例子包含了不仅属性值使用p-namespace而且还使用指定格式去声明属性引用。第一个定义使用<property name="spouse" ref="jane"/>去创建一个从 bean john到 bean jane的引用,第二个bean定义使用p:spouse-ref="jane"作为一个属性去做相同的事情。在这个例子中,spouse是属性名称,ref表示不是一个直接值而是一个引用值。


> p-namespace不像标准的 XML 格式灵活。例如,声明属性引用的格式与以 Ref 结尾的属性冲突,而标准 XML 格式则不会。我们推荐你选择你的方式小心地并和你的团队交流去避免在用一时间同时使用 XML 三种方式。


> 参考代码:com.liyong.ioccontainer.starter.XmlPNameSpaceIocContainer


  • c-namespace快捷方式


类似p-namespace的快捷方式,在Spring3.1引入,c-namespace允许配置构造函数参数内联属性而不是嵌入constructor-arg元素。


下面的例子使用c:命名空间去做相同的基于构造函数依赖注入事情:


 <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>

<!-- 传统声明可选参数 -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>

<!-- c-namespace声明参数名称 -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>

</beans>
复制代码


c:命名空使用类似于p:相同的约束(bean引用为跟随-ref)去通过它们的名字设置构造参数。类似地,即使它没有在XSD schema中定义,也需要在 XML 文件中去声明,(存在Spring Core中)。


一个少见的场景,构造函数参数名字不能使用(通常如果编译字节码时没有调试信息)可以使用回退参数索引,如下:


 <!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:0-ref="beanTwo" c:1-ref="beanThree"
c:_2="something@somewhere.com"/>
复制代码


> 由于 XML 语法,要求这个索引符号_必须存在,因为 XML 属性名称不能以数字开头(即使 IDE 工具允许也是不行的)。对应索引的符号在<constructor-arg>元素也是有效的但是不常用,因为这个声明顺序通常已经足够。


在实践中,构造函数解析机制在匹配参数是非常高效的,因此,除非你真的需要,我们推荐整个配置都使用名字符号。


> 参考代码:com.liyong.ioccontainer.starter.XmlCNameSpaceIocContainer


  • 复合属性名


当设置bean属性的时候,我们可以使用复合或嵌入属性名,只要这个path(点式引用)下面所有组件期望这个最终属性名不为null。考虑下面的bean定义:


 <bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>
复制代码


这个something bean 有一个fred属性,fred有一个bob属性,bob拥有一个sammy属性并且最终sammy属性被设置值为123。为了能正常工作,something的属性fredfred的属性bob在这个bean构造之前必须不能为null。否则,会抛出一个NullPointerException


###### 1.4.3 使用depends-on


如果bean依赖其他bean,也就是意味着 bean 需要设置依赖的bean属性。典型地,我们可以基于 XML 配置元数据使用ref去完成。然而,一些 bean 之间的依赖不是直接的。一个例子是在类中一个静态的初始化器需要被触发,例如:数据库驱动注册。depends-on属性能够显示地强制一个或多个bean在依赖bean初始化之前初始化。下面的例子使用depends-on属性去表达对一个简单bean的依赖。


 <!--beanOne依赖manager-->
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
复制代码


为了表达多个 bean 的依赖,提供一个 bean 名称的集合列表作为depends-on属性值(,;空格是有效的分隔符)。


<!--beanOne依赖manager,accountDao-->
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
复制代码


> 仅仅在单例bean场景下,depends-on属性能够指定初始化时间依赖和对应的销毁时间依赖。与给定 bean 定义依赖关系的从属 bean 首先被销毁,然后再销毁给定 bean 本身(备注:被依赖的 bean 先销毁,在销毁宿主 bean)。因此,depends-on可以控制关闭顺序。


参考代码:com.liyong.ioccontainer.starter.XmlDependOnSpaceIocContainer


###### 1.4.4 bean 的懒加载


默认情况下,ApplicationContext实现更早的创建和配置所有的单例bean作为初始化过程的一部分。通常地,这个前置初始化是可取的,因为错误的配置或环境变量被立即的发现,而不是几个小时甚至几天后才被发现。当这个行为不是可取的时候,我们可以通过标记bean作为一个懒加载的单例bean去阻止提前初始化。一个懒加载bean告诉容器当第一次请求的时候去创建实例而不是在容器启动时候。


在 XML 配置中,这个行为通过在<bean/>元素的属性lazy-init控制的。下面例子展示:


<!--设置bean延迟初始化 注意:Spring中的bean默认是单例的--><bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/><bean name="not.lazy" class="com.something.AnotherBean"/>
复制代码


当前面的配置通过ApplicationContext加载并启动时,这个lazy bean 没有被提前的初始化,而not.lazy bean 被尽早的初始化。


然而,当一个懒加载 bean 是另一个单例bean的依赖时候,这个懒加载不是懒加载的。ApplicationContext在启动时创建这个懒加载bean,因为它必须满足这个单例bean的依赖。这个懒加载bean被注入到一个单例bean所以它不是懒加载的。


我们也可以在容器级别通过使用<beans>元素的default-lazy-init属性控制懒加载,下面例子展示怎样使用:


<beans default-lazy-init="true">    <!-- no beans will be pre-instantiated... --></beans>
复制代码


参考代码:com.liyong.ioccontainer.starter.XmlLazyInitilaizedIocContainer


###### 1.4.5 自动装配协调者


Spring 容器能自动装配协调者 bean 之间的关系。通过检查ApplicationContext的内容,Spring 自动为你的 bean 解析协同者(其他依赖 bean)。自动装配有下面的优势:


  • 自动装配能显著的降低对属性和构造函数参数的需要。(其他机制例如:在这方面,其他机制(例如本章其他地方讨论的 bean 模板)也很有价值)。

  • 自动装配可以随着对象的演化而更新配置。例如:如果你需要添加一个类的依赖,依赖能够被自动地被满足不需要修改配置。因此,自动装配在开发过程中特别有用,当代码库变得更加稳定时,自动装配可以避免切换到显式连接的选项。


当使用基于 XML 元数据配置,我们可以为这个bean指定自动装配模式通过<bean/>元素的autowire属性。自动装配有 4 种模式。你可以对每个 bean 指定自动装配因此可以选择自动装配哪些 bean。下面的表格描述了 4 种装配模式:



使用byType或构造函数自动装配模式,你可以结合数组和类型化的集合,在这种情况下,提供容器中与期望类型匹配的所有自动装配候选,以满足相关性。如果期望key的 类型是String,你可以自动装配强类型的Map实例。一个自动装配Map实例的值由所有匹配期望类型实例组成,并且这个 Map 实例的这些keybean名称对应。


  • 自动装配的优势和限制


在一个系统中一致地使用自动装配将工作的更好。如果通常不使用自动装配,则可能使开发人员仅使用自动装配来连接一个或两个 bean 定义而感到困惑。


考虑自动装配的限制和优势:


* 在propertyconstructor-arg中显示依赖设置总是覆盖自动装配。你不能自动装配简单的属性例如:原生类型,StringClass(简单属性数组)。这种限制是由设计造成的。

* 自动装配没有显示装配精确。尽管如前面的表中所述,Spring小心地避免猜测,以免产生可能产生意外结果的歧义。Spring管理对象之间的关系不再明确记录。

* 装配信息可能对从Spring容器生成文档的工具不可用。

* 容器中的多个bean定义可能与要自动装配的setter方法或构造函数参数指定的类型相匹配。对于数组、集合或Map实例,这不一定是问题。然而,为了依赖期望一个简单值,这种歧义不会被任意解决(意思是期望一个bean容器中确有多个匹配的bean)。如果没有唯一的有效bean的定义会抛出一个异常。


在最后的场景中,你有一些可选项:


* 显示的装配放弃自动装配。

* 通过设置bean定义的autowire-candidate属性为false去避免自动装配,在下一个章节描述。

* 通过设置<bean/>元素属性primarytrue指定一个bean定义作为主要的候选者。

* 实现更细粒度的有效控制通过基于注解的配置,在基于注解容器配置中描述。


  • 自动装配排除 bean


在每个 bean 的基础上,你可以从自动装配中排除一个bean。在Spring的 XML 格式中,设置<bean/>元素的autowire-candidate属性为false。容器使该特定的bean定义不适用于自动装配基础结构(包括注释样式配置,例如 @Autowired)。


> autowire-candidate 属性被设计仅仅通过基于类型自动装配有影响。它不会影响通过名字来显示引用的方式,即使这个指定 bean 没有被标记作为一个自动装配候选者这个名字也会被解析。因此,如果名称匹配,按名称自动装配仍然会注入 Bean。


可以基于与Bean名称的模式匹配来限制自动装配候选者。顶层<beans/>元素接受一个或多个表达式在default-autowire-candidates 属性中。例如,去限制自动装配候选者任意状态的bean,它的名字以Repository结尾,提供一个值为*Repository的表达式。提供多个表达式可通过;号分割。为一个bean定义的autowire-candidate属性显示指定truefalse总是优先级最高(比如default-autowire-candidates优先级高),<beans>指定的规则被覆盖。


当这些bean不想自动装配注入到其他bean中时,这些技术是非常有用的。这并不意味着一个被排除的bean本身不能通过使用自动装配来配置。而是,bean本身不是一个候选者不会被注入到其他bean中。


> 参考代码:com.liyong.ioccontainer.service.AutowireCandidateService


###### 1.4.6 方法注入


在大多数应用场景中,在容器中大多数bean是单例的。当单例Bean需要与另一个单例Bean协作或非单例Bean需要与另一个非单例Bean协作时,典型的处理依赖通过定义一个bean作为其他bean的属性。当bean的生命周期不同时会出现问题。假设单例 bean A需要使用非单例 bean B(原型),假设A的每个方法被调用。这个容器仅仅创建单例 bean A一次并且只有一次机会去设置属性。容器无法每次为 bean A提供一个新的 bean B实例(单例 A 每次从容器获取 bean B不能每次提供一个新bean)。


一个解决方案时放弃控制反转。我们也可以通过实现ApplicationContextAware接口让 bean A意识到容器。并在 bean A每次需要 bean B时,通过使用getBean("B")获取一个新实例bean。下面例子展示使用方式:


package fiona.apple;
// Spring-API importsimport org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) { // grab a new instance of the appropriate Command Command command = createCommand(); // set the state on the (hopefully brand new) Command instance command.setState(commandState); return command.execute(); }
protected Command createCommand() { // 通过从容器获取bean return this.applicationContext.getBean("command", Command.class); }
public void setApplicationContext( ApplicationContext applicationContext) throws BeansException { //容器注入ApplicationContext this.applicationContext = applicationContext; }}
复制代码


上面的例子不是可取的,因为这个业务代码需要容器回调耦合了Spring 框架(备注:这个我不敢苟同,上面我也发表了观点)。方法注入,Spring IoC容器高级特性,让我们处理这种场景更简洁。(备注:上面Command配置为原型才能达到效果)


可以阅读更多关于方法注入的动机在 博客入口


  • 查找方法注入


查找方法注入是容器覆盖容器管理bean上的方法并返回容器中另一个命名bean的查找结果的能力。在前面描述的场景中,典型地查找涉及到原型 bean。Spring框架通过CGCLB库去动态地生成一个子类去覆盖这些方法以实现方法注入。


> * 为了动态子类能够正常工作,Spring bean不能是final并且方法也不能是final

> * 单元测试一个具有抽象方法的类需要你自己去子类化并且提供一个抽象方法的存根实现。

> * 组件扫描也需要具体方法,这需要具体的类别。

> * 进一步关键限制是方法查找不能在工厂方法并且特别在configuration类被@Bean标注的方法,因为,在这种情况,容器不负责创建实例,因此无法即时创建运行时生成的子类(因为这种方法Bean是由我们自己创建处理的容器不能控制bean的生成)。


在前面的代码片段CommandManager中,Spring容器动态的覆盖这个createCommand方法的实现。CommandManager类没有任何的Spring的依赖,重构如下:


package fiona.apple;

// 没有Spring的依赖!
public abstract class CommandManager {

public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}

// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
复制代码


在客户端类中包含被注入的方法(在这个例子中是CommandManager类),这个方法被注入要求一个下面格式的签名:


 <public|protected> [abstract] <return-type> theMethodName(no-arguments);
复制代码


如果这个方法是abstract,动态地生成子类实现这个方法。除此之外,动态生成子类覆盖在源类中具体方法定义。考虑下面例子:


  <!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
复制代码


每当需要新的myCommand bean实例时,标识为commandManager的 bean 就会调用其自己的createCommand()方法。你必须非常的小心应用myCommand bean作为一个原型,如果这是真实需要的。如果这个bean是单例的,myCommand实例每次都返回同一个bean


或者,在基于注解组件模式中,你可以声明一个查找方法通过@Lookup注解,像下面例子:


 public abstract class CommandManager {

public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}

//指定名称查找,在容器中查找
@Lookup("myCommand")
protected abstract Command createCommand();
}
复制代码


或者,更习惯地说,你可以依靠针对查找方法的声明返回类型来解析目标Bean


 public abstract class CommandManager {

public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}

//没有指定名称查找,通过查找方法的MyCommand类型去容器查找
@Lookup
protected abstract MyCommand createCommand();
}
复制代码


注意,通常应该使用具体的存根实现声明此类带注解的查找方法,为了使它们与Spring的组件扫描规则兼容,默认情况下,抽象类将被忽略。此限制不适用于显式注册或显式导入的Bean类。


> 获取不同范围的目标 bean 的其他方式是ObjectFactory/Provider注入点。查看 Scoped Beans as Dependencies.

>

> 你也可以找到ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包)去使用。


> 参考代码:

>

> com.liyong.ioccontainer.starter.XmlLookUpInjectionIocContainer

>

> com.liyong.ioccontainer.starter.XmlLookUpInjectionByAnnotaionIocContainer


  • 任意方法替换


与查找方法注入相比,方法注入的一种不太有用的形式是能够用另一种方式实现替换托管bean中的任意方法。你可以放心地跳过本节的其余部分,直到你真正需要此功能为止。


基于 XML 元素数据配置,你可以使用replaced-method元素将现有的方法实现替换为已部署的Bean。考虑下面的类,这个类有一个叫做computeValue的方法我们想去覆盖这个方法。


 public class MyValueCalculator {

public String computeValue(String input) {
// some real code...
}

// some other methods...
}
复制代码


类实现org.springframework.beans.factory.support.MethodReplacer接口提供新的方法定义,像下面定义:


  public class ReplacementComputeValue implements MethodReplacer {

public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}
复制代码


bean的定义去部署到源类并且指定方法覆盖类似如下例子:


  <bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- 替换方法 -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
复制代码


你可以使用一个或多个<arg-type/>在<replaced-method/>元素中去指示这个被覆盖的方法签名。仅当方法重载并且类中存在多个变体时,才需要对参数进行签名。


为了方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如:java.lang.String


  java.lang.String
String
Str
复制代码


因为参数的数量通常足以区分每个可能的选择,所以通过让你仅输入与参类型匹配的最短字符串,此快捷方式可以节省很多输入。


> 参考代码:com.liyong.ioccontainer.starter.XmlMethodReplaceIocContainer


作者


个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。


博客地址: http://youngitman.tech


CSDN: https://blog.csdn.net/liyong1028826685


微信公众号:

发布于: 2020 年 09 月 04 日阅读数: 60
用户头像

青年IT男

关注

站在巨人肩上看得更远! 2018.04.25 加入

从事金融行业,就职过易极付、思建科技、网约车平台等一流技术团队,目前就职于银行负责支付系统建设。对金融行业有强烈的爱好。实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域

评论

发布
暂无评论
Spring 5 中文解析核心篇-IoC容器之依赖关系