Spring依赖注入浅析

来源:互联网 发布:什么是网络教育 编辑:程序博客网 时间:2024/04/24 10:09

什么是依赖注入?


我们都知道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 的代码如下:

@Componentpublic 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的代码如下:
@Componentpublic 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
  • Conditional
  • 自动注入的模糊性

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“属性指定。

0 0