正文
spring框架(1)— 依赖注入
小程序:扫一扫查出行
【扫一扫了解最新限行尾号】
复制小程序
【扫一扫了解最新限行尾号】
复制小程序
依赖注入
spring核心容器就是一个超级大工厂,所以的对象(数据源、hibernate SessionFactory等基础性资源)都会被当做spring核心容器的管理对象——spring把容器中的一切对象统称为Bean。
Spring对Bean没有任何要求,只要是一个java类,spring就可以管理这个java类,并把它当做Bean处理。对于spring框架而言,一切java对象都是Bean。
package service;public class Axe
{
public String chop()
{
return "使用斧头砍柴";
}
}
这个Axe类是一个普通的java类。
package service;public class Person
{
private Axe axe;
//设值注入所需的setter方法
public void setAxe(Axe axe)
{
this.axe = axe;
}
public void useAxe()
{
System.out.print("我打算去砍柴!");
//调用axe的chop()方法
//声明Person对象依赖于Axe
System.out.println(axe.chop());
}
}
这个Person类的useAxe()方法需要调用Axe对象的chop()方法,这种A对象需要调用B对象方法的情形,被称为依赖。
Spring核心容器是整个应用的超级工厂,所有的java对象都会讲给Spring的容器管理——这些java对象被称为Spring容器中的Bean。
Spring对于bean的管理需要在xml中进行配置。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!--配置名为person的Bean,其实现类为service.Person-->
<bean id="person" class="service.Person">
<!--控制调用setAxe()方法,将容器中的ax Bean作为参数传递进去-->
<property name="axe" ref="axe"/>
</bean>
<!--配置名为axe的Bean,其实现类为service.Axe-->
<bean id="axe" class="service.Axe"></bean></beans>
配置文件的根元素是<beans .../>,根元素中包含多个<bean .../>元素,每个元素定义一个Bean。在这里配置了两个Bean,分别是service.Person和service.Axe。
只要将java类配置到xml中,spring就可以对其进行管理。
<bean id="person" class="service.Person">
配置文件会将<bean .../>元素默认以反射方式类调用这个类的无参构造器,spring解析这一元素之后得到两个字符串,其中idStr的值为"person"(对应的是id属性的值),classStr的值为"service.Person"(对于的是class属性的值)。
String idStr = ...;
String classStr = ...;
Class clazz = Class.forName(classStr);
Object obj = clazz.newInstance();
//container代表spring容器
container.put(idStr, obj);
spring框架通过反射根据<bean .../>元素的class属性创建了一个java对象,并以<bean .../>元素的id属性的值为key,将该对象放入spring容器中——这个java对象就成为了spring容器中的Bean。
在spring配置文件中配置Bean时,class属性的值必须是Bean实现类的完整类名。
上面配置文件中还包括一个<property .../> 子元素,它驱动spring在底层以反射执行一次setter方法。name属性决定了执行哪些setter方法,value或者ref决定执行setter方法的传入参数。
- 如果传入的是基本类型及其包装类、String等类型,则使用value属性指定传入参数;
- 如果以容器中其他Bean作为传入参数,则使用ref属性指定传入的参数。
spring框架只要看到<property .../>子元素,就会在底层反射执行一次setter方法,该Bean一旦创建,spring就会立即根据<property .../>子元素来执行setter方法。也就是说:
- <bean .../>元素驱动spring调用构造器创建对象;
- <property .../>元素驱动spring执行setter方法。
这两步是先后执行的,中间几乎没有时间间隔。
上面的配置中<property .../>元素的name属性为axe,该元素驱动spring以反射方式执行person Bean中的setAxe()方法;ref属性值为axe,该属性值指定以容器名为axe的Bean作为执行setter方法的传入参数。
也就是说,在spring的底层会执行如下代码:
//解析<property .../>元素的name属性得到该字符串值为"axe"
String nameStr = ...;
//解析<property .../>元素的ref属性得到该字符串的值为"axe"
String refStr = ...;
String setterName = "set" + nameStr.substring(0, 1).toUpperCase() + nameStr.substring(1);
//获取spring容器中名为refStr的Bean,该Bean将会作为传入参数
Object paramBean = container.get(refStr);
//此处的clazz是从反射得到的Class对象
Method setter = clazz.getMethod(setterName, paramBean.getClass());
//此处的obj参数是之前一段反射代码创建的对象
setter.invoke(obj, paramBean);
上述的代码是反射代码的实例,通过<property .../>的name属性决定调用哪个setter方法,并且根据value和ref决定调用setter方法的传入参数。
id为person的<bean .../>元素还包括一个<property .../>子元素,因此spring会在创建完person Bean之后,立即以容器中id为axe的Bean被赋值给person对象的axe实例变量。
接下来程序会通过spring容器来访问容器中的Bean,ApplicationContext是Spring容器中最常用的接口,该接口有如下两个实现类。
- ClassPathXmlApplicationContext:从类加载路径下搜索配置文件,并根据配置文件来创建spring容器;
- FileSystemXmlApplicationContext:从文件系统的相对路径或绝对路径下去搜索配置文件,并根据配置文件来创建spring容器。
在spring中,类加载路径是稳定的,因此通常使用ClassPathXmlApplicationContext来创建容器。
package service;import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;public class Main {
public static void main(String[] args)
{
//引入配置文件,创建spring容器
Resource r = new FileSystemResource("src/beans.xml");
//加载配置文件
BeanFactory f = new XmlBeanFactory(r);
//获取id为person的Bean
Person p = (Person)f.getBean("person", Person.class);
//调用useAxe()方法
p.useAxe();
}}
spring获取Bean对象的方式有两种:
1.Object getBean(String id):根据容器中Bean的id来获取Bean,获取Bean之后需要进行强制类型转换;
2.T getBean(String name, Class<T> requiredType):根据容器中Bean的id来获取指定的Bean,但是该方法带一个泛型参数,因此获得Bean之后无需进行强制类型转换。
获取Bean对象之后,可以调用方法、访问实例变量,即可以像使用java对象一样使用这个Bean对象。
1、依赖注入
1.调用者面向被依赖对象的接口编程;
2.将被依赖对象的创建交给工厂完成;
3.调用者通过工厂来获取被依赖组件。
通过这三点,可以保证调用者主需与被依赖对象的接口耦合,这就避免了类层次的硬编码耦合,使用spring框架之后,调用者无需主动获取被依赖对象,只需被动接受spring容器为调用者的成员变量赋值即可(只要配置一个<property .../>子元素,spring就会执行对应的setter方法为调用者的成员变量赋值)。于是,使用了spring之后,调用者获取被依赖对象的方式由原来的主动获取变成了变动接受,这被称为控制反转(Inversion of Control,IoC)。
从spring框架的角度来说,spring容器负责将被依赖对象赋值给调用者的成员变量——相当于为调用者注入它依赖的实例,因此这种方式被称为依赖注入(Dependency Injection)。
使用了spring框架之后,主要有两个变化:
1.程序员无需使用new创建对象,所有的java对象的创建都交给spring容器完成;
2.当调用者需要调用被依赖对象的方法的时候,调用者无需主动获取被依赖对象,只需要等待spring容器注入即可。
2、注入方式
- 设值注入:IoC容器使用成员变量的setter方法来注入被依赖对象;
- 构造注入:IoC容器使用构造器来注入被依赖对象。
2.1 设值注入
设值注入指的是IoC容器通过成员变量的setter方法来注入被依赖对象。
spring推荐面向接口编程,不管是调用者还是被依赖的对象,都应该为之定义接口,程序应该面向它们的接口,而不是面向实现类编程,这样利于后期的维护和升级。
①Axe接口
package test;public interface Axe
{
public String chop();
}
②Person接口
package test;public interface Person
{
public void useAxe();}
③Chinese类
package test;public class Chinese implements Person
{
private Axe axe;
public void setAxe(Axe axe)
{
this.axe = axe;
}
//实现Person接口定义的useAxe()方法
public void useAxe()
{
System.out.println(axe.chop());
}
}
④StoneAxe类
package test;public class StoneAxe implements Axe
{
public String chop()
{
return "石斧砍柴好慢";
}}
⑤beans.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="chinese" class="test.Chinese">
<property name="axe" ref="stoneAxe"/></bean>
<bean id="stoneAxe" class="test.StoneAxe"></bean>
</beans>
⑥Main类
package test;import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;//import org.springframework.context.ApplicationContext;
public class Main {
public static void main(String[] args)
{
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:/java/workspace_j2ee/SpringDemo3/src/beans.xml");
Person p = (Person)ctx.getBean("chinese", Person.class);
p.useAxe();
}}
注意使用FileSystemXmlApplicationContext的时候,可以直接使用"src/beans.xml"作为beans.xml的路径,spring可以找的到配置文件的位置。
假设Axe有另外的实现类:SteelAxe。
package test;public class SteelAxe implements Axe{
public String chop()
{
return "钢斧砍柴更快";
}}
此时,需要将SteelAxe部署在spring容器中去,只需要在beans.xml中添加配置信息。
<bean id="steelAxe" class="test.SteelAxe"></bean>
这一行定义了一个Axe实例,id是steelAxe,实现的类是SteelAxe,然后需要修改chinese的配置信息。
<bean id="chinese" class="test.Chinese">
<!-- <property name="axe" ref="stoneAxe"/></bean> -->
<property name="axe" ref="stoneAxe"/></bean>
因为chinese实例与具体的Axe实现类之间没有任何关系,chinese仅仅与接口Axe耦合,这样就能保证chinese实例与Axe的松耦合——这是spring强调面向接口编程的原因。
Bean与Bean之间的依赖关系由spring管理,spring采用setter方法为目标Bean注入所依赖的Bean,让Bean之间的耦合从代码层次上分离出来,依赖注入是一种优秀的解耦方式。
所以,可以看出spring的IoC容器的三个基本要点:
①应用程序的各组件面向接口编程,面向接口编程可以将组件之间的耦合关系提升到接口层次,从而利于项目后期拓展;
②应用程序各组件不再由程序主动创建,而是由spring容器来负责产生并初始化;
③spring采用配置文件或注解来管理Bean的实现类、依赖关系,spring容器根据配置文件或注解,利用反射机制来创建实例,并将其注入依赖关系。
2、 构造注入
这种方式在构造实例的时候,已经为其完成了依赖关系的初始化,这种利用构造器来设置依赖关系的方式,被称为构造注入。
驱动spring在底层以反射方式执行带指定参数的构造器,当执行带参数的构造器时,就可利用构造器参数对成员变量执行初始化——这是构造注入的本质。
<bean .../>元素默认总是驱动spring调用无参数的构造器来创建对象,使用<consrtructor-arg .../>子元素,每个<constructor-arg .../>子元素代表一个构造器参数,如果<bean .../>元素包含N个<constructor-arg .../>子元素,就会驱动spring调用带N个参数的构造器来创建对象。
①Roles类
package test;public class Roles {
private int id;
private String roleName;
public Roles(){
}
public Roles(int id, String roleName)
{
this.id = id;
this.roleName = roleName;
}
public String toString()
{
return "Users [id = " + id + ", name= " + roleName + "]";
}
}
②beans.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="roles" class="test.Roles">
<constructor-arg value="1"/>
<constructor-arg value="小明"/>
</bean>
</beans>
③测试
package test;import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;public class SpringTest {
public static void main(String[] args)
{
ApplicationContext ctx = new FileSystemXmlApplicationContext("src/bean.xml");
Roles r = (Roles)ctx.getBean("roles");
System.out.println(r.toString());
}
}
设值注入是先通过无参数的构造器创建一个Bean实例,然后调用setter方法注入依赖关系,而构造注入则直接调用有参数的构造器,当Bean实例创建完成之后,已经完成了依赖关系的注入。
<constructor-arg .../>可以指定参数的值,
<bean id="roles" class="test.Roles">
<constructor-arg value="1"/>
<constructor-arg value="小明"/>
这一段的内容相当于:
Roles role = new Role(1, "小明");
有时候,如果包含的构造器中的参数不同,但是构造器的名称相同,假设Test(String, String) 和Test(String, int),假如通过<constructor-arg value="1">由于spring只能解析出"1"字符串,但是到底转换为哪一个明确的构造器的数据类型,就无从判断了,所以spring允许为<constructor-arg .../>元素指定一个type类型,比如说<constructor-arg value="1", type="int"/>,此时就完成了int类型参数的配置。
假如存在依赖,看下面的例子:
①Users类
package test;public class Users {
private int id;
private String name;
public Users(){
}
public Users(int id, String name)
{
this.id = id;
this.name = name;
}
public String toString()
{
return "User [id = " + id + ", name = " + name + "]";
}}
②Roles类
package test;public class Roles {
private int id;
private String roleName;
//用户
private Users users;
public Roles(){
}
public Roles(int id, String roleName, Users users)
{
this.id = id;
this.roleName = roleName;
this.users = users;
}
public String toString()
{
//因为这里是在字符串加法中调用的Users对象,所以会隐式调用user.toString()方法,也可以显示调用users.toString方法
return "Roles [id=" + id + ", roleName=" + roleName + ", users="
+ users + "]";
}
}
③配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="roles" class="test.Roles">
<constructor-arg value="1"/>
<constructor-arg value="小明"/>
<constructor-arg ref="users"/>
</bean>
<bean id = "users" class = "test.Users">
<constructor-arg value = "2"/>
<constructor-arg value = "小华"/>
</bean>
</beans>
④测试
package test;import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;public class SpringTest {
public static void main(String[] args)
{
ApplicationContext ctx = new FileSystemXmlApplicationContext("src/bean.xml");
Roles r = (Roles)ctx.getBean("roles");
System.out.println(r.toString());
}
}
⑤输出结果
Roles [id=1, roleName=小明, users=User [id = 2, name = 小华]]
3.两种注入方法的对比
设置注入的适用场景:
- 与传统的JavaBean的写法相似,更容易理解,通过setter方法设定依赖关系显得更加直观,自然;
- 对于复杂的依赖关系,如果采用构造注入, 会导致构造器过于臃肿,难以阅读,spring在创建Bean实例的时候,需要同时实例化其依赖的全部实例,因而导致性能下降,而如果使用设值注入,会比较轻松;
- 尤其是在某些成员变量可选的情况下,多参数的构造器很笨重。
构造注入的适用场景:
- 构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入,例如,组件中其它依赖关系的注入,常常需要依赖于Datasource的注入,采用构造注入可以设置注入的顺序;
- 对于依赖关系无需变化的Bean,构造注入更加实用。因为没有setter方法,所有的依赖关系都在构造器中设定,因此,无需担心后续代码对依赖关系产生破坏;
- 依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全是透明的,更符合高内聚的原则。
总结:
建议以设置注入为主,构造注入为辅。对于依赖关系无需变化的注入,尽量采用构造注入,而其他依赖关系的注入,考虑使用设值注入的方式。