目录
文章所用项目源码参考:java_spring_learn_repo
什么是依赖注入
- 依赖指的是对象要完成某种功能方法,需要其他对象或属性一起协作完成,而被需要的其他对象和属性则被称为对象的依赖。
- 依赖注入(DI)是Spring IoC技术实现的关键,其本质是Bean通过调用Class反射的构造函数或使用服务器定位模式来逆向控制依赖的实例化过程。(
换句话说Spring使用了服务器定位模式来实现DI
) - 使用DI技术可以让代码变的更加干净,(
有的文章会说IoC完全消除了对象之间的依赖关系,实质上依赖关系是不会被消除的,DI只是让对象之间的依赖从类的内部绑定转变为从外部注入,从而降低代码的耦合度,完全消除的概念是指这两对象就没有任何联系了,互不干涉,也不存在调用
),当对象被外部提供依赖时,我们不需要查询其依赖关系,也不需要去关注依赖对象的位置和类别,因此代码会更容易被维护和测试。
依赖的解析
- 首先通过XML,Java代码或者注解来描述Bean的配置数据
- 每个Bean的依赖通过属性,构造函数的参数,静态工厂方法参数(
需要工厂方法设置的参数能代理正常的构造函数
)的形式进行表达 - 每个配置的属性和构造参数都要被赋予值或引用
- 作为直接传递数值的属性或参数均以字符串的形式进行配置,在Spring中会自动转换成对应类型
- 每个Bean的依赖通过属性,构造函数的参数,静态工厂方法参数(
- 在Spring容器创建时,Spring ApplicationContext会验证每个Bean的配置,并将具有单例作用域的Bean实例化,其他Bean则会在被请求的时候才会被创建
Bean的循环依赖
在遇到BeanA引用BeanB,BeanB里又引用BeanA的时候,如果都通过构造参数进行互相注入,Spring容器会在运行时检测到这种循环引用并抛出异常。对于Spring来说,Spring会在ApplicationContext加载配置时预先检测对还未被加载Bean的引用和循环依赖,到实际创建Bean的时候Spring会尽可能晚的设置属性和解析依赖关系(
不能是构造注入,需要Setter注入
),换句话说就是在有存在循环依赖的配置BeanA中,Spring检测到需要BeanB的依赖时,会在调用BeanA的setter方法之前实例化BeanB虽然Setter方法能解决循环依赖的问题,但是归根结底还是业务逻辑划分不清晰的缘故
。
Spring提供的两种注入方式
1. 基于构造器的赖注入
- 构造器也被称为构造函数,既然DI是为了通过构造函数进行依赖注入,那么肯定是需要提供有参构造才能进行传参初始化。
- Spring在进行构造注入的时候不需要明确指定调用哪个构造函数,Spring会根据传入的参数数量以及参数类型和名称自动匹配合适的构造方法
public class ConstructBeanImpl implements ConstructBean {
private int count;
private String poolName;
private String poolPassword;
private Double money;
@Override
public String toString() {
return "ConstructBeanImpl{" +
"count=" + count +
", poolName='" + poolName + '\'' +
", poolPassword='" + poolPassword + '\'' +
", money=" + money +
'}';
}
public ConstructBeanImpl(int count, String poolName, String poolPassword, Double money) {
this.count = count;
this.poolName = poolName;
this.poolPassword = poolPassword;
this.money = money;
}
public ConstructBeanImpl(int count, String poolName, String poolPassword) {
this.count = count;
this.poolName = poolName;
this.poolPassword = poolPassword;
}
public ConstructBeanImpl(int count, String poolName) {
this.count = count;
this.poolName = poolName;
}
@Override
public void printHello() {
System.out.println("construct hello world");
}
}
1.1 通过类型注入
- 在constructor-arg里配置type属性来通知Spring注入类型
<!-- 基于构造器注入使用construct-arg标签进行标记 -->
<!-- 注入的数据可以是通过type类型进行匹配,也可以是通过构造参数的序列进行注入 -->
<!-- 请注意务必要确保有与注入属性数量相同的构造参数 -->
<bean id="constructBean" class="com.nobugnolife.bean.impl.ConstructBeanImpl">
<constructor-arg type="int" value="114"/>
<constructor-arg type="java.lang.String" value="mysql.driver"/>
</bean>
1.2 通过索引注入
- 在constructor-arg里配置index来确定注入构造参数的位置,从左往右,下标从0开始
<!-- 使用类型注入的构造注入方法在遇到参数类型相同的构造参数时容易出现歧义,可以通过按照索引的方式进行定位注入 -->
<!-- 索引下标从0开始,从左往右的参数顺序 -->
<bean id="constructBean01" class="com.nobugnolife.bean.impl.ConstructBeanImpl">
<constructor-arg index="0" value="11145"/>
<constructor-arg index="1" value="mysql.driver"/>
<constructor-arg index="2" value="123321"/>
</bean>
1.3 通过参数名注入
- 在constructor-arg中通过name提供对应参数名
<!-- 如果遇到同类型,同等数量的构造参数的构造方法,还可以使用构造参数名来消除歧义 -->
<bean id="constructBean02" class="com.nobugnolife.bean.impl.ConstructBeanImpl">
<constructor-arg name="count" value="114514"/>
<constructor-arg name="poolName" value="mysql.driver"/>
<constructor-arg name="poolPassword" value="123321"/>
<constructor-arg name="money" value="10000.11111"/>
</bean>
- 这里的参数名可以使用注解@ConstructorProperties来重命名构造参数
@ConstructorProperties({"count","name","password","money"})
public ConstructBeanImpl(int count, String poolName, String poolPassword, Double money) {
this.count = count;
this.poolName = poolName;
this.poolPassword = poolPassword;
this.money = money;
}
- XML配置的为注解所标记的构造参数名
<bean id="constructBean02" class="com.nobugnolife.bean.impl.ConstructBeanImpl">
<constructor-arg name="count" value="114514"/>
<constructor-arg name="name" value="mysql.driver"/>
<constructor-arg name="password" value="123321"/>
<constructor-arg name="money" value="10000.11111"/>
</bean>
1.4 通过静态工厂方法参数注入
- 上文提到过,如果静态工厂方法能代理构造函数返回对象实例的话,同样也可以使用静态工厂参数进行构造注入
- 首先创建静态工厂类(
实例工厂的方法以此类推即可
)
public class ConstructFactory {
private ConstructFactory() {}
public static ConstructBean createConstructBean(int count, String poolName, String poolPassword, Double money){
return new ConstructBeanImpl(count,poolName,poolPassword,money);
}
}
- 然后将对应配置的class替换成静态工厂的类路径即可,还有添加factory-method
<bean id="constructBean03" class="com.nobugnolife.factory.ConstructFactory" factory-method="createConstructBean">
<constructor-arg name="count" value="114514"/>
<constructor-arg name="poolName" value="mysql.driver"/>
<constructor-arg name="poolPassword" value="14444"/>
<constructor-arg name="money" value="1002.221"/>
</bean>
基于Setter的依赖注入
- 使用setter注入的话是Spring容器在调用无参构造实例化Bean之后调用setter方法进行注入,因此请确保类能正常调用无参构造。
- 通过property标签实现Setter的依赖注入
<bean id="setterBean" class="com.nobugnolife.bean.impl.SetterBeanImpl">
<property name="beanOne" ref="beanOne"/>
<property name="id" value="10"/>
<property name="name" value="mybean"/>
</bean>
Spring对不同类型的注入方式
1. 字面值(String,基本类型)注入
- Spring对可以直接传值的属性统一使用value进行注入,并且均使用字符串形式注入(
在Spring加载ApplicationContext的时候会自动将这些配置转化成对应属性的类型
) 构造器和Setter均适用,这里就不分开举例了
<bean id="simpleTypeDao" class="com.nobugnolife.dao.impl.SimpleTypeDaoImpl">
<!-- num对应在类里的int属性名num -->
<property name="num" value="10"/>
<!-- point对应float属性 -->
<property name="point" value="1.1"/>
<!-- flag对应boolean -->
<property name="flag" value="true"/>
<!-- bt对应byte -->
<property name="bt" value="127"/>
<!-- ch对应char -->
<property name="ch" value="c"/>
<!-- lg对应long -->
<property name="lg" value="114514134"/>
<!-- money对应double -->
<property name="money" value="3.14159"/>
<!-- str对应String -->
<property name="str" value="hello world"/>
</bean>
2. 引用类型注入
- 引用类型注入使用ref进行表述,可以是独立的ref标签,也可以是内部ref属性
<bean id="constructRefBean" class="com.nobugnolife.bean.impl.ConstructRefBeanImpl">
<constructor-arg ref="beanOne"/>
<constructor-arg>
<ref bean="beanTwo"/>
</constructor-arg>
</bean>
- 在配置引用类型注入的时候如果传入的引用找不到id或者name的话则会返回空指针
- 在Spring中,如果遇到具备重复配置信息或者是相似重复属性的配置Bean时,可以通过抽象出父Bean(可以是类继承的方式,或者是子Bean对应的类包含父Bean中所有的属性,
父Bean的配置只能是abstract或者是lazy-init,不能让工厂实例化Bean
)来进行parent继承 - Parent类
public class ParentBean {
private String name;
private String password;
public void setName(String name) {
this.name = name;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "ParentBean{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
public String getPassword() {
return password;
}
}
- 子类A通过继承的方式传递parent中的属性
public class SonBeanA extends ParentBean {
private int connectNum;
private int timeout;
private int poolMax;
public void setConnectNum(int connectNum) {
this.connectNum = connectNum;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public void setPoolMax(int poolMax) {
this.poolMax = poolMax;
}
@Override
public String toString() {
return "SonBean{" +
"name=" + getName() +
", password" + getPassword() +
", connectNum=" + connectNum +
", timeout=" + timeout +
", poolMax=" + poolMax +
'}';
}
}
- 子类B中包含parent中所有的属性
public class SonBeanB {
// private ParentBean parentBean;
private String tools;
private String password;
@Override
public String toString() {
return "SonBeanB{" +
"tools='" + tools + '\'' +
", password='" + password + '\'' +
", name='" + name + '\'' +
'}';
}
public void setPassword(String password) {
this.password = password;
}
private String name;
public void setName(String name) {
this.name = name;
}
public void setTools(String tools) {
this.tools = tools;
}
}
- XML配置
<!-- 设置abstract为true后,容器将不会实例化Bean对象,但会保留配置属性 -->
<bean id="parentBean" abstract="true">
<property name="name" value="msyql"/>
<property name="password" value="123"/>
</bean>
<!-- 通过parent可以继承父bean的属性配置,同时也可以覆盖父bean的配置,子bean的类需要继承或包含父bean中所有的属性 -->
<bean id="sonBeanA" class="com.nobugnolife.bean.impl.SonBeanA" parent="parentBean">
<property name="poolMax" value="100"/>
<property name="timeout" value="1000"/>
<property name="connectNum" value="30"/>
</bean>
<!-- spring提供的parent不只是可以通过继承,也可以是当前类所包含的属性,确保父Bean中有的属性,子Bean对应的类也有,注意是类,不是bean -->
<bean id="sonBeanB" class="com.nobugnolife.bean.impl.SonBeanB" parent="parentBean">
<property name="tools" value="rock and stone"/>
<!-- 同样也可以覆盖父bean中的属性,如果对应类而不是Bean是非继承关系的话,需要确保类中有相同的属性 -->
<property name="name" value="MongoDB"/>
</bean>
在Spring Framework4.0之后ref就不再支持local属性,可以替换成ref bean
3. 集合类型注入
- List注入
<!-- List注入 -->
<property name="idList">
<list>
<value>1</value>
<value>2</value>
<value>3</value>
</list>
</property>
- set注入
<!-- Set注入 -->
<property name="nameSet">
<set>
<value>xiaoming</value>
<value>lisi</value>
<!-- 在集合注入中同样可以使用引用注入,只要有配置的引用bean -->
<idref bean="myString"/>
</set>
</property>
- property注入
<!-- property注入 -->
<property name="properties">
<props>
<prop key="adminstrator">admin@go.org</prop>
<prop key="password">admin</prop>
<prop key="phone">114514</prop>
</props>
</property>
- map注入
<!-- map注入 -->
<property name="map">
<map>
<entry key="1" value-ref="beanOne01"/>
<entry key="2" value-ref="beanOne02"/>
</map>
</property>
- Spring会通过集合中定义的泛型类型,自动将字符串转化为对应类型的数值如:
private Map<String, Float> accounts;
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
- 如果value设定为""空字符串,则默认数值为空或者Null
4. 集合合并
- Spring支持让子集合(list,map,set,props)元素继承父集合的值,并且子集合的元素能覆盖父集合中的值。
- 如果不知道什么是Bean继承的话可以参考Bean定义的继承
- 父集合
<bean id="parentBean03" abstract="true">
<property name="props">
<props>
<prop key="username">
administrator
</prop>
<prop key="password">admin</prop>
</props>
</property>
</bean>
- 子集合
<bean id="childBean" class="com.nobugnolife.bean.impl.ChildBean" parent="parentBean03">
<property name="props">
<!-- 子集合重写密码并新增email配置 -->
<props>
<prop key="password">123</prop>
<prop key="email">example@gmail.com</prop>
</props>
</property>
</bean>
list,map,set等用法基本和上述结构差不多,不过对于有序集合List,父列表的值会被排在子列表的值前面。
并且集合合并只能对同类型的集合合并,无法混合不同类型集合进行合并
5. 内部Bean
- 当某个类只是作为当前类的属性而并没有其他引用的时候,可以通过创建内部Bean的方式进行注入,这样就不需要配置新的Bean,内部Bean不需要定义ID和Name,并且也会忽略创建的作用域
- 内部Bean
public class InnerBean {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "InnerBean{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 需要内部bean的类
public class HumanBeanImpl implements HumanBean {
@Override
public void printHello() {
System.out.println("human hello world");
}
private InnerBean innerBean;
@Override
public String toString() {
return "HumanBeanImpl{" +
"innerBean=" + innerBean +
'}';
}
public void setInnerBean(InnerBean innerBean) {
this.innerBean = innerBean;
}
}
- 配置xml
<bean id="humanBean" class="com.nobugnolife.bean.impl.HumanBeanImpl">
<property name="innerBean">
<bean class="com.nobugnolife.bean.impl.InnerBean">
<property name="name" value="Sham"/>
<property name="age" value="28"/>
</bean>
</property>
</bean>
Spring使用命名空间快捷注入
使用p命名空间的XML
- p-namespace可以简化bean中property元素来配置Bean属性注入
- p命命名空间是Spring基于XML Schema定义的可扩展配置格式的命名空间,因此需要先导入p命名空间配置
xmlns:p="http://www.springframework.org/schema/p"
- 使用p命名空间只需要在bean的属性前添加p:即可,对引用类型的配置属性需在属性后加-ref
<!-- 使用p命名空间对bean进行setter注入,对于引用类型需在属性名后加-ref -->
<bean id="setterBean02" class="com.nobugnolife.bean.impl.SetterBeanImpl"
p:beanOne-ref="beanOne"
p:id="11"
p:name="pBean"/>
p命名空间不像标准的XML格式那样灵活。例如,声明属性引用的格式与以 Ref 结尾的属性发生冲突,而标准的XML格式则不会
使用c命名空间的XML
- 与p命名空间相似,c命名空间是对构造注入的constructor-arg标签进行的简化
- 首先导入c命名空间
xmlns:c="http://www.springframework.org/schema/c"
- 通过参数名进行c命名空间的构造注入
<!-- 利用c命名空间进行构造注入 -->
<bean id="constructBean04" class="com.nobugnolife.bean.impl.ConstructBeanImpl"
c:count="12"
c:money="22.3"
c:poolName="mysql"
c:poolPassword="123321"/>
- c命名空间通过索引形式进行的构造注入,引用类型的注入方式与p命名空间一样
<!-- c命名空间通过序列索引形式的构造注入 -->
<bean id="constructBean05" class="com.nobugnolife.bean.impl.ConstructRefBeanImpl"
c:_0-ref="beanOne"
c:_1-ref="beanTwo"/>
Spring使用dependes-on
- 对于Bean的直接依赖来说,可以通过配置属性中的ref进行引用,而对于一个类在初始化之前需要先初始化类中的静态变量时,可以使用depends-on属性来指定Bean之间的初始化时间依赖关系。
public class ManagerBean {
private String driver;
private String url;
private String userName;
private String password;
public ManagerBean() {
System.out.println("manager bean以被实例化");
}
public String getDriver() {
return driver;
}
public String getUrl() {
return url;
}
public String getUserName() {
return userName;
}
public String getPassword() {
return password;
}
public void setDriver(String driver) {
this.driver = driver;
}
@Override
public String toString() {
return "ManagerBean{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", userName='" + userName + '\'' +
", password='" + password + '\'' +
'}';
}
public void setUrl(String url) {
this.url = url;
}
public void setUserName(String userName) {
this.userName = userName;
}
public void setPassword(String password) {
this.password = password;
}
}
- 依赖ManagerBean的DependBean
public class DependBeanImpl implements DependBean {
private ManagerBean manager;
public DependBeanImpl() {
System.out.println("depend bean已被实例化");
}
public void setManager(ManagerBean manager) {
this.manager = manager;
}
@Override
public void printHello() {
System.out.println("depend bean hello world");
}
@Override
public void registDataSourct() {
System.out.println("创建数据库连接对象:" +
"数据库驱动:" + manager.getDriver() +
"\n数据库连接地址: " + manager.getUrl() +
"\n连接用户名:" + manager.getUserName() +
"\n连接密码:" + manager.getPassword());
}
}
- XML配置dependes-on,如果有多个依赖,使用逗号,空格或分号在字符串中进行分割即可
<bean id="dependBean" class="com.nobugnolife.bean.impl.DependBeanImpl" depends-on="managerBean">
<property name="manager" ref="managerBean"/>
</bean>
<bean id="managerBean" class="com.nobugnolife.bean.impl.ManagerBean">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306"/>
<property name="userName" value="root"/>
<property name="password" value="123"/>
</bean>
- 如果不配置depends-on的话,depend bean会在manager bean之前被实例化,这样是不符合逻辑的
depends-on可以指定Bean的初始化时间以来关系,而在单例模式的Bean中,也可以指定小伙时间的依赖关系,在Bean中如过定义了depends-on的依赖,则依赖的Bean会在本体被销毁前销毁
懒加载注入Bean
- 在默认的单例模式Bean中Spring容器加载ApplicationContext的时候会非常急切的创建和配置所有的单例模式Bean(这样会尽可能早的发现环境中的错误,而不是等到被调用的时候再报错),如果不想要Bean立即被创建,可以使用懒加载模式阻止Bean的实例化,懒加载模式下的Bean只有当在第一次被请求创建的时候才会实例化对象,而不是在启动容器的时候。
- Bean属性中配置lazy-init=true即可开启懒加载
<bean id="lazyBean" class="com.nobugnolife.bean.impl.LazyBean" lazy-init="true"/>
<bean id="activeBean" class="com.nobugnolife.bean.impl.ActiveBean"/>
- 测试是否在被加载时调用,优先实例化lazyBean,然后再在activeBean后调用lazyBean的方法,如果不是懒加载,那么activeBean的创建会和lazyBean的方法挨在一起
@Test
public void testDependBean() throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("dependBean.xml");
DependBean dependBean = ctx.getBean(DependBean.class);
dependBean.registDataSourct();
System.out.println(dependBean);
}
Spring自动注入
- Spring容器可以自动建立与依赖Bean之间的联系,可以省去手动配置属性或构造参数的过程。
自动注入可以极大减少对指定属性或构造参数的配置,可以随着对象的变化而更新配置,因此在开发过程中,自动注入是主流用法,能极大提升开发效率。
- Spring基于XML的自动注入通过在bean标签的属性中autowire属性来定义,自动注入的形式一共有4种
模式 | 说明 |
---|---|
no | 默认关闭自动注入模式 |
byName | 通过属性名称进行自动注入,Spring会自动匹配一个与需要自动注入属性同名的bean |
byType | 通过属性类型注入,如果容器中只有一个与当前被注入类的类型相同或者为其配置的实现类的类型,则会进行自动注入, 如果存在多个会报错,如果没有匹配的Bean则不会发生任何事 |
constructor | 通过被注入Bean的构造器参数类型查找同类型或配置的实现类型的Bean进行注入,与byType类似 |
<!-- 创建需要注入依赖的Bean -->
<bean id="bean" class="com.nobugnolife.bean.impl.BeanImpl">
</bean>
<!-- 通过命名自动注入bean,Spring容器会在Bean中查询与autoWire中属性bean命名相同的Bean对象 -->
<bean id="autoWireBean" class="com.nobugnolife.bean.impl.AutoWireBeanImpl" autowire="byName"/>
<!-- 通过属性类型进行注入,Spring会自动在容器中查询与属性类型相匹配的配置Bean -->
<bean id="autoWireBean02" class="com.nobugnolife.bean.impl.AutoWireBeanImpl" autowire="byType"/>
<!-- 通过构造参数类型进行自动注入,Spring会自动在容器中查询符合类提供的构造器参数的类型相匹配的Bean,最好提供全参数构造 -->
<bean id="autoWireBean03" class="com.nobugnolife.bean.impl.AutoWireBeanImpl" autowire="constructor"/>
- 注意使用construct注入的话要提供全参构造否则会报错
public class AutoWireBeanImpl implements AutoWireBean {
private Bean bean;
@Override
public String toString() {
return "AutoWireBeanImpl{" +
"bean=" + bean +
'}';
}
public AutoWireBeanImpl() {
}
public AutoWireBeanImpl(Bean bean) {
this.bean = bean;
}
public void setBean(Bean bean) {
this.bean = bean;
}
@Override
public void printHello() {
bean.printHello();
System.out.println("autowire bean hello world");
}
}
自动注入的局限
- 自动注入的优先级是在setter与构造器注入之后,因此如果存在构造注入或setter注入,那么自动注入的效果会被覆盖
- 自动注入无法注入简单的属性,如基本数据类型和String
- 自动注入的效果不一定准确,虽然Spring在尽可能的避免对模糊情况下的猜测,但总可能会发生意外
- 自动注入如果在项目中统一被使用,效果会很好,但如果大量使用配置的情况下混合使用自动注入则会带来误解。
将Bean排除自动注入的搜索
- 在bean标签中通过设置autowire-candidate=false属性可以将当前Bean排除自动注入的搜索。
方法注入
- 具体参考:Spring基础——方法注入