Spring依赖注入浅析

什么是依赖注入?


我们都知道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“属性指定。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值