什么是依赖注入?
我们都知道Spring的两大特性,以来注入(DI)和面向切面编程(AOP),那么什么是依赖注入呢?我们举个例子说明一下。
假设要写一个简单的音乐播放器,我们通常会这么写:
首先创建一个CDPlayer类,如下:
public class CDPlayer {
private CD cd;
public CDPlayer() {
this.cd = new CD();
}
public void play() {
this.cd.play();
}
}
然后再创建一个表示唱片的CD类,如下:
public class CD {
private String artist;
private String cdName;
private List<Song> songs;
public CD() {
this.artist = "Bruno Mars";
this.cdName = "Uptown Funk";
//this.songs = ...
}
public CD(String artist, String cdName, List<Song> songs){
this.artist = artist;
this.cdName = cdName;
this.songs = songs;
}
public void play(){
//play songs...
}
}
class Song在这里省去
上面的例子有什么问题呢,它当然可以工作,但在播放音乐时,你需要在CDPlayer类中new一个CD对象,这样两个class就紧密的耦合在一起。并且当你想要播放其它唱片时,就需要修改java代码。
有了依赖注入,我们就不需要这样了,我们可以给class CDPlayer增加一个方法,让我们可以设置要播放的唱片:
public void SetCd(CD cd) {
this.cd = cd;
}
有了这个方法,我们就不需要手动的在CDPlayer中new一个CD实例了。
当然,Spring提供地依赖注入不止这么简单,下面就来仔细的了解下Spring的依赖注入(DI)功能。
Spring DI
有了Spring的依赖注入,依赖类不再需要手动实例化,而是由Spring容器帮我们实例化并注入到需要的对象中,依赖类的生命周期也无需程序员关心,Spring容器会照顾它的一生。
依赖注入的几种方式:
- Setter方法注入
- Constructor 方法注入
Setter方法注入
Setter方法注入是最简单的一种注入方法,以上面的CDPlayer为例,它需要一个CD class的实例,就在class CDPlayer中定义一个CD类的实例,然后设置它的Setter方法:
public class CDPlayer {
private CD cd;
public void setCd(CD cd) {
this.cd = cd;
}
}
然后编写Spring的xml配置文件,指定依赖注入的配置
cdplayer.xml
<!-- 创建一个CD bean-->
<bean class="demo.CD" id="cd"/>
<!--创建CDplayer bean 在property中指定ref参数为CD bean 的id-->
<bean class="demo.CDPlayer" id="player">
<property name="cd" ref="cd"/>
</bean>
有了上面的代码,Spring容器就可以创建两个Bean实例并自动将class CD的实例注入到class CDplayer的实例中。
如何执行程序呢?需要以下代码:
MainApp.java
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("cdplayer.xml");
CDPlayer player = (CDPlayer) context.getBean("player");
player.play();
}
}
xml文件中定义的bean的id属性是每一个bean的唯一标识,ApplicationContext实例通过这个id属性就可以获取Spring容器创建的Bean的实例。
需要注意的是,当使用以上的xml配置时,class CD 必须有一个无参的默认构造函数,Spring容器需要这个构造函数创建cd bean。
class CD的实现可以为:
public class CD {
private String artist;
private String name;
public CD(String artist, String name){
this.artist = artist;
this.name = name;
}
public CD() {
this.artist = "BM";
this.name = "UF";
}
public void play() {
System.out.println("Play songs in " + name + " by " + artist);
}
}
Constructor方法注入
Constructor方法注入顾名思义就是通过构造函数注入依赖像,还是上面的例子,将CDPlayer修改为:
public class CDPlayer {
private CD cd;
public CDPlayer(CD cd) {
this.cd = cd;
}
}
然后修改xml配置文件为:
<!-- 创建一个CD bean-->
<bean class="demo.CD" id="cd"/>
<!--创建CDplayer bean 在property中指定ref参数为CD bean 的id-->
<bean class="demo.CDPlayer" id="player">
<constructor-arg ref="cd"/>
</bean>
上面的配置文件只是将player Bean的property属性改成了<constructor-arg>
属性,就完成了Constructor注入方式。
关于构造函数注入必须指出的是,在上面的例子中,我们在<constructor-arg>
中并没有指定要注入的参数是哪一个,但因为CDPlayer的构造函数只有一个参数,所以Spring默认是传递给其中的cd属性的。那么,当构造函数不止一个参数,尤其当多个参数具有相同类型时,怎么确定要传递给哪个参数呢?
我们通过class CD来说明,它有两个属性,CD的名字和歌手名字,我们把无参的构造函数去掉,改成下面的形式:
public class CD {
private String artist;
private String name;
public CD(String artist, String name){
this.artist = artist;
this.name = name;
}
public void play() {
System.out.println("Play songs in " + name + " by " + artist);
}
}
class CD现在有一个构造函数,都是String类型,我们通过xml来配置它具体的值,如下:
<bean id="cd" class="demo.CD">
<constructor-arg index="0" value="BM"/>
<constructor-arg index="1" value="UF"/>
</bean>
可以看到我们使用了<constructor-arg>
标签的index属性来指明是哪一个参数。通过上面的配置,我们把“BM”字符串传递给了CD 的artist
属性,把“UF”字符串传递给了它的name
属性。
通过上面的例子还可以看到ref
属性指定一个bean,而value
属性制定一个值,如一个字符串常量。这对 <property>
标签同样适用。
三种配置方式
上面我们使用了xml文件的方式配置bean之间的依赖注入关系,但是,当我们的bean变得足够多时,使用xml文件的配置方式会使xml文件过于臃肿。还好,Spring为我们提供了其他的配置方式,分别是基于注解的自动发现,基于java代码的配置。下面介绍其他两种配置方式:
- 基于注解的自动发现和注入
- 基于java的配置
基于注解的自动发现和注入 自动装配主要使用了`@Component`和`@Autowaired`注解,首先更改class CD 的代码如下:
@Component
public class CD {
private String artist;
private String name;
public CD(String artist, String name){
this.artist = artist;
this.name = name;
}
public CD() {
this.artist = "bruno mars";
this.name = "uptown funk";
}
public void play() {
System.out.println("Play songs in " + name + " by " + artist);
}
}
可以看到,在class CD的上面添加了`@Component`,这样Spring就会把它当作一个bean创建它的实例。 再修改class CDPlayer的代码如下:
@Component
public class CDPlayer {
@Autowired
private CD cd;
public void play(){
cd.play();
}
CDPlayer 有一个CD的实例作为属性,这里,我们既没有给cd设置setter方法,也没有给CDPlayer编写构造函数,而是在cd的头上添加了一个`@Autowired`注解。这样,Spring就知道,cd的实例由添加了@Component的class CD提供,并将cd的实例自动注入到CDPlayer的实例中。 当然,使用这种方式,同样要求CD class有一个无参的构造函数,Spring容器使用这个无参的构造函数初始化CD实例。 当然,只有这样还不够,还需要一个配置类,我们创建一个配置class,代码如下:
@Configuration
@ComponentScan(basePackageClasses = CDPlayer.class)
public class ContextConfigurationDemo {
}
class ContextConfigurationDemo 中没有任何代码。只是在其上添加了`@Configuration` 表示这是一个Spring 配置类,`@ComponentScan` 通过其basePackageClasses属性指明了Component组件所在的包。Spring会将这个包下的带有`@Component` 注解的class初始化为bean并自动注入到带有`@Autowired` 的属性上去。 为了测试上面的自动发现和注入的例子,我们编写了如下的测试代码:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ContextConfigurationDemo.class)
public class DemoTest {
@Autowired
private CDPlayer player;
@Test
public void test() {
player.play();
}
}
`@RunWith` 表示这是一个测试类,`@ContextConfiguration` 注解指明了配置class,`@Test` 指明了test()是一个测试方法,执行它,将会看到正确的输出。 仔细考虑一下,将会看到这种方式的局限性,自动发现和注入方式在代码完全由自己编写时能够很好的工作,但如果我们使用了其他人编写的代码,如导入的jar包,我们是不可能在这些代码的class类头上添加注解的,这时,基于java的配置方案就很有用了。
基于java的配置
基于java的配置和基于自动发现和注入的方式非常相似,两者经常配合使用,弥补自动发现和注入的不足。
基于java的方式,只是将class头上的 @Component
注解去掉,而在配置class中使用 @Bean
手动添加实例。接着上面的例子,我们去掉class CD 上的 @Component
注解,在class ContextConfigurationDemo中添加如下代码:
@Configuration
@ComponentScan(basePackageClasses = CDPlayer.class)
public class ContextConfigurationDemo {
@Bean
public CD cd() {
return new CD();
}
}
在这里,我们编写了一个普通的方法,在其上添加一个 @Bean
注解。这个方法返回一个CD类型的对象,在这里我们只是简单的new了一个CD对象,你也可以任意修改它,设置artist和name都可以。
再执行测试,发现结果和使用自动发现是一样的。
高级特性
profile
有时候,我们需要根据不同的场景决定一些bean是否应该被创建,比如,在你的project中,一些bean是为测试创建的,当部署到生产环境中时,这些为测试而定义的bean就不需要创建了。那么,就需要一种机制,让开发者决定一些bean什么时候被创建,什么时候不创建。Spring提供的方式就是 profiles
。
@Profile
注解完成这一功能,假设我你们有两个场景分别是开发场景和测试场景,还是上面的播放器的例子,我们可以定义如下java配置类:
@Configuration
@ComponentScan(basePackageClasses = CDPlayer.class)
public class ContextConfigurationDemo {
@Bean
@Profile("dev")
public CD cddev() {
return new CD("dev-artist","dev-name");
}
@Bean
@Profile("test")
public CD cdtest() {
return new CD("test-artist", "test-name");
}
}
这里我们定义了两个bean,都返回class CD类型的实例,通过 @Profile
注解分别将其定义到”dev“和”test“两个profile下。
为了测试这个例子,我们将测试代码更改为:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ContextConfigurationDemo.class)
@ActiveProfiles("dev")
public class DemoTest {
@Autowired
private CDPlayer player;
@Test
public void test() {
player.play();
}
}
可以看到,在测试class的顶部,我们通过 @ActiveProfiles
注解激活了”dev“profile,这样,cddev()方法创建的bean就会被实例化并且注入到CDplayer的实例中,而cdtest()方法创建的 CD bean就不会被初始化。
@Profile
注解不仅可以作用于创建bean的方法,还可以作用于整个Configuration class,只需将 @Profile
注解移到类前头即可。
除了在java 中配置,xml文件同样可以配置profile,也同样有两种作用域,配置文件域和单个Bean域。配置的方法为:
作用于整个配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans/spring-beans.xsd"
profile="dev">
</beans>
作用于单个bean:
<bean id="xxx" class="xxx.xxx.xxx" profile="dev">
</bean>
激活profile
我们定义了几个profile,怎么设置究竟哪个起作用呢?
Spring通过两个参数配置激活的profile:
spring.profiles.active和spring.profiles.default
如果设置了spring.profiles.active,那么就由它决定哪个profile被激活,此时,spring.profiles.default就不起作用;如果spring.profiles.active没有设置,就由spring.profiles.default决定激活哪个profile;如果两个都没有被配置,就只有那些不属于任何profile的bean创建。
定义这两个参数的方式共有一下几种:
1. 通过DispatcherServlet的参数配置
2. 作为context 参数配置
3. 使用环境变量配置
4. 通过JVM系统参数配置
Conditional
Profile根据不同的环境决定一些bean是否被创建,而Conditional根据一些条件是否满足决定一些bean是否被实例化。
在Spring中,这一功能由@Conditional
注解来完成,它作用在创建bean的方法上,和@Bean
注解一起使用。
假设你有一个MagicBean,你希望仅当Magic存在时它才被实例化,你可以这样完成:
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
@Conditional
注解有一个MagicExistsCondition.class参数,这个参数决定了条件是否满足,这个类必须实现Condition 接口,这个接口只有一个返回布尔值的matches方法,这个接口的定义为:
public interface Condition {
boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata);
}
如果matchs方法返回true,该bean就会被创建,否则该bean就不会被创建。
自动注入的模糊性
上面我们举得例子都比较简单,一个需要注入的变量只有一个bean实例满足要求,当有多个实例满足要求时,Spring就无法决定注入 哪个实例,就会抛出异常。此时,就需要一种在bean实例冲突时的仲裁方案。
Spring有两种解决方法。最简单的是使用@Primary
注解,当有两个满足注入条件的bean实例时,带有@Primary注解的Bean会被注入。这个注解既可以和@Component
注解一起使用在class级,也可以和@Bean
注解用在java配置文件中,当然也可以在xml中通过指定<bean>
标签的”primary=true“来设置。
和@Primary
注解这种一刀切的方式不同,Spring还提供了一种方式处理注入的模糊性,使用@Qualifier
注解。这个注解的作用通过定义一个子集来缩小bean的范围,直到只有一个bean满足要求。与@Primary
注解不同,@Qualifier
注解作用于需要注入的地方,也就是和@Inject
或 @AutoWired
注解一起使用,表明注入的bean需要满足的条件。他的使用也有两种方式,一种是直接指定要注入的Bean的class,例如:
@AutoWired
@Qualifier("iceCream")
private Dessert dessert;
上面的代码指明dessert参数将由class IceCream的实例注入。
这种方式仍然有局限,使用的较少,最多的还是使用如下的方式:
假设需要注入的仍然是一个Dessert实例,满足的实例有如下几个:
//IceCream 和Cookie 类都是 Dessert 接口的实现类
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
@Bean
@Qualifier("hot")
public Dessert cookie() {
return new Cookie();
}
然后指定注入的bean为IceCream的实例可以这样:
@AutoWired
@Qualifier("cold")
private Dessert dessert;
可以看到,我们在定义@Bean时也指定一个@Qualifier注解,这相当于将一个bean加入到了一个集合中,然后就可以在要注入的地方指定需要注入的集合是哪个。
一个集合中可能有不止一个bean实例,此时,我们可以在要注入的地方通过多个@Qualifier
注解限定最终只有一个bean符合要求。如:
@AutoWired
@Qualifier("cold")
@Qualifier("fruity")
private Dessert dessert;
Bean 的作用域
在使用依赖注入时,我们定义一次Bean的实例,可以将其注入到多个不同的地方,那么,这些注入到不同地方的Bean的实例是只有一个还是每次注入都新创建一个实例呢?在Spring中这是可以配置的。
默认情况下,Bean只实例化一次,注入到不同地方的Bean都是一个实例,在一个地方更改了这个实例,全局有效。这有时候不适合一些应用场景。
Spring一共提供了四种bean的作用范围,分别是:
1. Singleton
2. prototype
3. Session
4. Request
后两种都只在web 中有效。
Singleton表示这个bean是全局唯一的,无论注入多少次,它都只被初始化一次;
Prototype表示每次注入都创建一个新的实例。
Session表示在wen应用中,每个会话都单独创建一个bean实例;
Request表示在web应用中,每次请求都创建一个bean的实例;
如何制定bean的作用域呢?在java配置或自动发现中,通过@Scope
注解来具体具体的作用域,这个注解和@Bean
或 @Component
注解一起使用。如:
@Bean
@Scope(ConfigurableBeanFactory.PROTOTYPE)
public CD cd() {
return new CD();
}
这样,每个需要注入class CD实例的地方得到的都是不同的实例。
在xml文件中,通过<bean>
标签的”scope“属性指定。