NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

什么是Spring IoC?

| Comments

在直接进入主题之前,先引入一些基本概念。

概念1:什么是容器?

日常生活领域内的容器,用来包装或装载物品的贮存器(如箱、罐、罈)或者成形或柔软不成形的包覆材料。–维基百科。
简而言之,容器是存放东西的东西。

Java中,Java的类库有一系列的基本类来保存对象(正确的说是保存对象的引用),包括List,Set,Queue和Map,它们被称作集合类。
Bruce Eckel(《Thinking in Java》的作者)使用了一个范围更广的术语来称呼它们:“容器”,容器提供了完善的方法来保存和管理对象。

当然,这里只是为了简单介绍“容器”这个宽泛术语本身的含义。我们常说的容器并不是指Java的内部容器类。

那么我们常说的容器是指的什么呢?举个例子,Servlet容器
什么是Servlet?这里又扯远了,引用孙鑫老师的说法(《Servlet/JSP深入详解:基于Tomcat的Web开发》的作者),Java Servlet(Java服务器小程序)是一个基于Java技术的Web组件,运行在服务器端。Servlet是平台独立的Java类,编写一个Servlet,实际上就是按照Servlet规范编写一个Java类。

Servlet容器又称为Servlet引擎,它是Java Web服务器(例如:Apache Tomcat)的一部分,用来提供动态网页的服务。
我们知道,Servlet没有main方法,它是不能独立运行的。它必须被部署到Servlet容器中,由容器来实例化和调用Servlet中的方法。因此,Servlet容器的作用是在Servlet的生命周期内包容和管理Servlet。

用户通过单击某个链接或者直接在浏览器的地址栏中输入URL来访问Servlet,Java Web服务器接收到该请求后,并不是将请求直接交给Servlet,而是交给Servlet容器。Servlet容器实例化Servlet,调用Servlet的一个特定方法对请求进行处理,并产生一个响应。这个响应由Servlet容器返回给Web服务器,Web服务器包装这个响应,以HTTP响应的形式发送给Web浏览器。

Ok,容器的概念就讲到这里,下一个依赖注入。

概念2:依赖注入

谈到依赖注入,必须提Martin Fowler的经典文章IoC容器和Dependency Injection模式。这篇文章有对应的中文版翻译(熊节译),原文上有链接,但是由于时间太久了,链接已经不可用了。我把它转载到我的博客上做了记录,可以在这里访问。我相信如果有看完这篇文章的,应该不用听我在这里啰嗦了。 依赖注入的直接好处就是解耦,消除组件之间的直接依赖。这种优势能够非常好的体现在对组件的测试。

进入正题

在Martin的那篇文章中已经提到,依赖注入有三种方式,分别是接口注入,构造器注入和设值注入。其中比较常用的是构造器注入和设值方法注入。在Spring中,配置依赖的方式有两种,一种是比较常用的xml方式配置,另一种是通过注解的方式配置(Spring2.5之后)。

首先介绍基于xml的配置方式:

现在来看一下,如何通过xml来配置一个bean

bean-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <context:annotation-config/>
    <context:component-scan base-package="foo.bar"/>
    <beans>
        <bean id="dataBaseService" class="foo.bar.DatabaseService">
            <constructor-arg index="0" value="username"/>
            <constructor-arg index="1" value="password"/>
            <constructor-arg index="2" value="database_url"/>
        </bean>
        <bean id="justService" class="foo.bar.JustService">
            <constructor-arg value="word" type="java.lang.String"/>
            <constructor-arg value="10" type="int"/>
        </bean>
        <bean id="justReferenceService" class="foo.bar.JustReferenceService">
            <property name="justService" ref="justService"/>
        </bean>
        <bean id="calculatorService" class="foo.bar.CalculatorService">
            <property name="a" value="1"/>
            <property name="b" value="1"/>
        </bean>
        <bean id="useCalculatorService" class="foo.bar.UseCalculatorService" autowire="byName"/>
    </beans>
</beans>

每一个Bean都有对应的id用来唯一表示它,可以理解为Spring容器中,该Bean的名字。

在这个Bean的配置文件中,介绍了几种配置Bean的方法。

dataBaseService采用构造器方式注入,由于构造器参数都是String类型,所以这里采用index来指定对应的参数。
justService同样采用构造器方式注入,但构造器参数类型不同,所以这里可以根据类型指定参数。
justReferenceService采用的是设值方式注入,并且注入的参数为一个引用类型,name为类中对应的变量名,ref是xml中的某一个Bean。
calculatorService同样采用设值方式注入。
useCalculatorService同样采用设值方法注入,并声明autowire自动装配是byName,它会根据类中成员变量的名字,在这些Bean中找到对应id等于成员变量名字的Bean。

在xml中配置好这些Bean之后,就可以通过ApplicationContext去得到这些Bean。

helloworld.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package foo.bar;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HelloWorld {
  public static void main(String[] args) {
      ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");

      DatabaseService databaseService = context.getBean(DatabaseService.class);
      databaseService.connect();

      JustService justService = context.getBean(JustService.class);
      justService.justPrint();

      JustReferenceService justReferenceService = context.getBean(JustReferenceService.class);
      justReferenceService.justPrint();

      UseCalculatorService useCalculatorService = context.getBean(UseCalculatorService.class);
      System.out.println(useCalculatorService.calculate());
  }
}

另一种配置Bean的方式是直接在类文件中加入注解

bean-config.xml
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <context:annotation-config />
    <context:component-scan base-package="foo.bar"/>
</beans>

上面是一个没有任何Bean的xml配置文件,里面有两个标签,分别是<context:annotation-config />和<context:component-scan />。现在来解释一下这两个标签的作用:

<context:annotation-config />:隐式地向Spring容器注册 AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、 PersistenceAnnotationBeanPostProcessor 以及 RequiredAnnotationBeanPostProcessor 这 4 个BeanPostProcessor。这样我们就可以使用@Required、@Autowired以及JSR 250的@PostConstruct、@PreDestroy和@Resource (if available)等等这些注解。如果不使用这个标签,那么如果要使用这些注解,就必须在xml文件中配置这些processor的Bean。

<context:component-scan base-package=“foo.bar”/>:它会扫描classpath下被注解过的类,并注册为Spring容器中的bean。Spring默认提供的注解有@Component、@Repository、@Service和@Controller。

有了这两个标签,我们就可以简单的在类中添加注解实现Bean向Spring容器的注册,如下,是对JustReferenceService的配置:

JustReferenceService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package foo.bar;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class JustReferenceService {

  @Autowired
  private JustService justService;

  public JustService getJustService() {
      return justService;
  }

  public void setJustService(JustService justService) {
      this.justService = justService;
  }

  public void justPrint() {
      justService.justPrint();
  }
}

使用@Autowired注解进行装配,只能是根据类型进行匹配。@Autowired注解可以用于Setter方法、构造函数、字段,甚至普通方法,前提是方法必须有至少一个参数。

当容器中存在多个Bean的类型与需要注入的相同时,注入将不能执行,我们可以给@Autowired增加一个候选值,做法是在@Autowired后面增加一个@Qualifier标注,提供一个String类型的值作为候选的Bean的名字,见下例。

在xml配置中,有两个同类型的Bean,它们拥有不同的id,因此,需要在类中添加注解告诉它需要装配哪一个。

UseCalculatorService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package foo.bar;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class UseCalculatorService {

  @Autowired
  @Qualifier("anotherCalculatorService")
  private CalculatorService calculatorService;

  public CalculatorService getCalculatorService() {
      return calculatorService;
  }

  public void setCalculatorService(CalculatorService calculatorService) {
      this.calculatorService = calculatorService;
  }

  public int calculate(){
      return calculatorService.calculate();
  }
}

如果希望根据name执行自动装配

那么应该使用JSR-250提供的@Resource注解,而不应该使用@Autowired与@Qualifier 的组合。@Resource使用byName的方式执行自动封装。@Resource标注可以作用于带一个参数的Setter方法、字段,以及带一个参数的普通方法上。@Resource注解有一个name属性,用于指定Bean在配置文件中对应的名字。如果没有指定name属性,那么默认值就是字段或者属性的名字。@Resource和@Qualifier的配合虽然仍然成立,但是@Qualifier对于@Resource而言,几乎与name属性等效。如果@Resource没有指定name属性,那么使用byName匹配失败后,会退而使用byType继续匹配,如果再失败,则抛出异常。

UseCalculatorService.java 使用@Resource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package foo.bar;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class UseCalculatorService {

  @Resource(name = "anotherCalculatorService")
  private CalculatorService calculatorService;

  public CalculatorService getCalculatorService() {
      return calculatorService;
  }

  public void setCalculatorService(CalculatorService calculatorService) {
      this.calculatorService = calculatorService;
  }

  public int calculate() {
      return calculatorService.calculate();
  }
}

使用 @Required 进行 Bean 的依赖检查

依赖检查的作用是,判断给定Bean的相应Setter方法是否都在Bean被实例化的时候被调用了,而不是判断字段是否已经存在值了。Spring进行依赖检查时,只会判断属性是否使用了Setter注入。如果某个属性没有使用Setter注入,即使是通过构造函数已经为该属性注入了值,Spring仍然认为它没有执行注入,从而抛出异常。另外,Spring只管是否通过Setter执行了注入,而对注入的值却没有任何要求,即使注入的null,Spring也认为是执行了依赖注入。

@Required 注解只能标注在Setter方法之上。因为依赖注入的本质是检查Setter方法是否被调用了,而不是真的去检查属性是否赋值了以及赋了什么样的值。如果将该注解标注在非setXxxx()类型的方法则被忽略。

使用 @Configuration 和 @Bean 进行 Bean 的声明

Spring3.0新增了另外两个实现类:AnnotationConfigApplicationContext和AnnotationConfigWebApplicationContext,它们是为注解而生,直接依赖于注解,作为容器配置信息来源的IoC容器初始化类。AnnotationConfigApplicationContext搭配上@Configuration和@Bean注解,自此,XML配置方式不再是 Spring IoC容器的唯一配置方式。Spring对标注Configuration的类有如下要求:

(1)配置类不能是 final 的;(2)配置类不能是本地化的,亦即不能将配置类定义在其他类的方法内部;(3)配置类必须有一个无参构造函数。

BeanConfiguration.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package foo.bar;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfiguration {

  @Bean
  public DatabaseService dataBaseService() {
      return new DatabaseService("username", "password", "database_url");
  }

  @Bean
  public JustService justService() {
      return new JustService("justService", 10);
  }
}

上面这种配置bean的方式与下面的xml配置方式一样。

spring-config.xml
1
2
3
4
5
6
7
8
9
<bean id="dataBaseService" class="foo.bar.DatabaseService">
       <constructor-arg index="0" value="username"/>
       <constructor-arg index="1" value="password"/>
       <constructor-arg index="2" value="database_url"/>
</bean>
<bean id="justService" class="foo.bar.JustService">
       <constructor-arg value="word" type="java.lang.String"/>
       <constructor-arg value="10" type="int"/>
</bean>

Spring IoC是将Bean的管理交给Spring容器,开发人员只需要配置,当要使用时,告诉Spring,让它注入给你。这样,在写程序时,可以大大降低耦合。

参考资料:

http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-iocannt/

http://www.springbyexample.org

孙鑫《Servlet/JSP深入详解:基于Tomcat的Web开发》

Comments