Spring-IoC
条评论IoC概述
IoC(Inversion of Control),顾名思义控制反转,什么是控制反转呢?这里的控制指对象的控制权,包括对象的创建和销毁等操作,控制反转就是对象的创建和销毁本来应该由应用程序自己控制的,现在应用程序把对象的控制权交给了Spring容器来管理,这就是控制反转。
个人理解:Spring容器成为Bean对象的代理或中介。
经常与Ioc一起出现的还有DI(Dependency Injection)依赖注入。依赖注入的意思是应用程序需要的对象是依赖于Spring的IoC容器注入的,不是自己创建的。看上去和IoC是一个意思,个人理解DI是具体实现IoC的一种方法,通过依赖IoC容器注入对象的方式实现了应用程序把对象的控制权转交给了IoC容器。除了DI依赖注入实现IoC以外,还可以DL(Dependency Lookup)主动从IoC容器中读取Bean对象。
如果用租房来比喻,常规方式就是租户自己找房子,IoC就是租户把找房子这件事交给给中介来完成,DI就是中介找好房子把信息Push给租户,DL就是租户主动从中介Pull房子信息。
面向接口编程
自己管理对象不是很好吗,为什么要使用IoC,把控制权交给IoC容器呢?
我理解,目的当然是解耦合。对象由IoC容器管理意味着应用程序不必依赖固定的Bean对象,就像租房只要找到满足条件的房子即可,不必非要指定某一套房子,这就是解耦合。因此可以得出结论:面向接口编程是IoC的基础,如果应用程序声明注入是实体类,那么还是强耦合的;所以通常应用程序声明注入的Bean对象都是接口,这样才能更好发挥IoC的作用。
例如:注入UserService接口后可以方便的切换实现类以实现不同的目标。
public interface UserService { |
应用程序使用时声明注入的是接口,具体实现类可以通过打开和关闭@Component注解切换,引入方无感知。
|
注入Bean通常有两种方式:byType和byName,按照类型注入或者按照名字注入。按照类型注入是典型的面向接口编程思想,byType时一般class类型为接口,容器负责查找实现了接口的Bean对象。
IoC使用
配置Bean对象
既然我们想把管理对象的工作交给IoC容器,那么它就得知道哪些对象需要被管理。随着Spring的发展,告诉IoC容器需要管理哪些Bean对象的方式也在变化,可以使用XML配置文件,也可以使用注解或者JavaConfig。
XML配置
把需要创建的Bean对象定义在一个xml文件里面,每个bean指定ID和对应类。IoC容器读取这个xml文件,通过反射创建Bean对象,通过ID返回Bean对象。同样,Bean对象之间的依赖关系也通过xml文件来配置。
bean标签
具体通过bean标签来配置一个Bean对象,通过class属性反射创建,通过id属性引用Bean对象;
- 对应Bean对象的属性,形成依赖关系,调用set()方法设置; - 也可以通过value属性直接赋值; - 对应Bean对象构造函数的参数,形成依赖关系,通过构造函数设置; - ref - ref就是Reference,引用,值为Bean的id;
例如:配置/resources/spring-context.xml如下:定义了car和wheel两个bean,car依赖wheel
<beans> |
Bean对象如下:
public class Car { |
注意:这里Car类必须实现setWheel()方法。
使用如下:
- ApplicationContext类可以理解为IoC容器,ClassPathXmlApplicationContext类是从classpath目录读取XML文件来解析Bean的IoC容器实现;
- 调用容器类的getBean()方法就可以从容器中获取Bean对象了,传入的参数可以是ID,也可以是类名。
(BlockJUnit4ClassRunner.class) |
以上这种用法非常好理解,ApplicationContext类读取XML文件并构造Bean对象,通过getBean()返回对象,ApplicationContext类就是IoC容器。容器需要做的就是解析XML文件,通过反射构造对象。
以上方法实际上没有实现DI依赖注入,需要自己主动获取,这种实现方式也被称作DL依赖查找。
scope
Bean对象具有scope属性,常用的有两种:Singleton和Prototyp,默认是Singleton:
- Singleton,一个容器只创建一个Bean实例,每次getBean()都返回相同的实例;
- Prototyp,每次getBean()都新创建一个Bean实例;
- Session,每个HTTP Session创建一个Bean实例。
注意:Singleton强调一个容器只有一个Bean实例,不同容器可以有不同Bean实例。
import标签
如上所述,xml配置是非常好理解的。但是,我们需要把所有用到的bean对象都定义在xml文件中,而且还要定义bean对象之间复杂的依赖关系,xml文件将越来越庞大。实战中,会根据类型不同创建多个xml文件,并通过<import>标签引入。
例如:
读取配置文件
类似配置数据源时,用户名和密码等信息我们一般不会直接写在xml文件里面,而是放在单独的properties文件中,使用<content:property-placeholder /> 标签可以读取配置文件。
例如:
<beans> |
注意:
${db.username}如果换成${username}将读取到本地用户。也就是说是由内置变量的,所以通常在properties文件中属性都要加前缀,避免冲突。
自动装配
前面我们使用<property> 属性手工配置了Car对象和Wheel对象的依赖关系,此外,还可以使用autowire 属性自动装配。自动装配意味着IoC自动帮我找到Wheel对象,并赋值到Car对象的wheel属性。
<beans> |
autowire 属性有三个值:
- no - 默认值,默认不自动装配;
- byName - 通过Bean的id自动装配,这里我们把
id="wheel"修改了id="wheel2"装配将会失败; - byType - 通过Bean的class自动装配,最常用。
byType自动装配时,如果没有根据类型找到对象,或者找到多个对象,都会抛出异常。
注解配置
Spring 2.5以后出现了注解方式,使用注解意味着将集中的bean配置(xml文件)分散到各个bean对象中。个人觉得没有两种用法没有好坏之分。
@Component注解
@Component注解起到XML文件中<bean/> 标签一样的作用,下面两种写法是等同的。
("car") |
<bean id="car" class="Car"/> |
@Component注解的value属性等同于bean的id属性,@Component的value默认值为首字母小写的类名,一般可以省略。从@Component又衍生出@Controller、@Service和@Repository注解,本质是一样的。
@Autowired注解
@Autowired/@Resource注解起到XML文件中autowird 属性的作用,@Autowired注解可以修饰类的成员变量,也可以修饰类的构造函数和成员方法。
@Autowired和@Resource的区别:
- @Autowired默认通过bean的类型注入,等价于
autowire="byType"; - @Resource默认通过bean的名字注入,等价于
autowire="byName"。
依赖注入
引入自动装配和@Autowired注解后真正实现了依赖注入,当我们需要使用一个对象时,直接将其声明为@Autowired,容器负责帮我们构造这个对象的实例。@Autowired注解默认是按照类型构造的,也就是说容器负责查找实现类并通过反射进行构造,所以通常@Autowired注解修饰的是接口。
进一步思考一下,如果一个@Autowired注解修饰的接口有多个实现类,那么容器就不知道应该构造哪个类了,怎么办?这个时候我们可以增加@Qualifier注解来指定bean的名称。下面以UserService为例,有UserServiceMemoryImpl和UserServiceDatabaseImpl两个实现类,实现选择数据库实现,代码如下:
|
因为@Component默认bean名称为首字母小写的类名,所以可以使用userServiceDatabaseImpl,如果UserServiceDatabaseImpl在@Component中自定义了bean名称,那么要使用自定义的。
component-scan
虽然加入@Component注解以后我们不需要在xml文件中定义<context:component-scan> 标签实现。
<beans> |
实际上在AutowiredAnnotationBeanPostProcessor中处理自动装配,但是要求开发者在xml文件中自己定义这个类的bean对象显然太low了,所以Spring为我们提供了<context:annotation-config> 标签来加载类似功能的类,由于<context:component-scan>包含了<context:annotation-config> 的功能,所以可以省略。
例子
Bean对象如下:
|
注意:这里Car类不用实现setWheel()方法就可以注入wheel,这和反射的实现有关。
使用如下:我们通过@Autowired注解直接注入Car对象,这才是真正的依赖注入。
(SpringJUnit4ClassRunner.class) |
注意:这里我们没有显式创建ApplicationContext,是 @Runwith(SpringJUnit4ClassRunner.class)和@ContextConfiguration起了相同的作用。
Java配置
总结,到目前为止我们的用法是:
- 在xml文件中定义component-scan,指向bean对象的包名;
- 在类中使用@Component和@Autowired注解。
从Spring 3.0开始提供了Java Config也叫做Java配置,Spring Boot建议使用Java配置替代XML配置。
Java配置引入了@Configuration、@Bean、@ImportResource和@Value等注解。
@Configuration注解
@Configuration注解就相当于一个xml文件,@Bean注解就相当于xml文件中的一个<bean/> ,
例如:Java配置
|
等同于如下xml配置
<beans> |
@ComponentScan注解
@ComponentScan注解相当于xml文件中的<context:component-scan /> 。
所以我们可以把xml文件替换为Java类
|
使用时和前面基本一样,只需要把@ContextConfiguration配置的xml文件替换为class类
(SpringJUnit4ClassRunner.class) |
选择
现在我们有三种方法定义一个Bean,选择哪一个呢?
XML配置
<bean id="car" class="cn.lu.spring.ioc.Car"/> |
注解配置
|
Java配置
|
习惯上,我们使用第二种方法@Component注解(实际更多使用@Controller、@Service等注解)来配置我们的业务Bean,XML不再使用,Java配置用来完成非业务Bean(例如:数据源)的配置。
个人理解:@Component注解是侵入式的,XML和Java配置是不需要侵入源代码的,这是他们的优势。
SpringBoot
下面看看在SpringBoot中如何使用IoC管理Bean对象。
|
|
|
上面的例子代码展示了最常见的用法:
- Spring容器扫描到@Service注解后创建UserServiceImpl类实例对象;
- Spring容器扫描到@Controller注解后创建UserController类实例对象;
- Spring容器扫描到@Autowired注解后将UserServiceImpl类实例对象注入到UserController类实例对象的userService属性中。
上面过程很好理解,但仔细一想你会发现代码中没有出现@ComponentScan注解,Spring容器怎么知道去哪里扫描注解呢?还有,容器类ApplicationContext是如何创建的呢?
@SpringBootApplication
问题的答案就在@SpringBootApplication注解中。
(ElementType.TYPE) |
({ElementType.TYPE}) |
@SpringBootApplication注解组合了@Configuration、@ComponentScan和@EnableAutoConfiguration三个最重要的注解,所以拥有了它们的功能。
我们知道一个@Configuration类文件对应一个XML配置文件,所以当Spring扫描到@Configuration注解后就会创建ApplicationContext容器类,并扫描@Component注解创建Bean对象放入容器类中。
@ComponentScan注解默认扫描当前包及其子目录下的源代码,所以Application类通常都是放在包的最外层,这样才可以保证web/service等目录下的注解可以被扫描到。
例如:新建应用在com.test.demo包下,引用的common项目在com.test.common包下,那么common项目中的@Component等注解是不生效的,因为Spring没有扫描它们。如果希望common包下的注解也生效,需要显式配置scanBasePackages,样例代码如下:
(scanBasePackages = {"com.test.demo", "com.test.common"}) |
@EnableAutoConfiguration是SpringBoot特性,和IoC无关,这里不展开。
@ConfigurationProperties
@PropertySource注解起到和<context:property-placeholder> 标签一样的作用,读取proerties配置文件,并映射到Bean对象中。
|
@PropertySource注解和@Value注解配合使用,@Value可以设置默认值,例如:password默认值为******
("${db.password:******}") |
Spring Boot 提供了@ConfigurationProperties注解,读取配置文件更方便,可以不加@Value注解。prefix属性定义前缀名,连接符转换为驼峰,例如:db.driver-class-name 对应driverClassName。
(prefix = "db") |
为了生效还必须添加@EnableConfigurationProperties注解。
|
扩展使用
前面介绍了IoC的最基本用法,帮助我们的应用程序创建类实例对象,下面看看其他用法。
初始化
实际开发中,有时需要在Bean对象创建后和销毁前做一些操作,可以使用下面两个注解:
- @PostConstruct,在构造函数完成之后执行;
- @PreDestroy,在析构函数执行之前执行;
除了使用@PostConstruct注解,还可以实现InitializingBean接口和afterPropertiesSet()方法。
|
上面代码的输出日志顺序为:
UserController() |
@Profile
@Profile注解为我们提供了在不同环境下创建不同Bean实例的能力。
一般用在配置类中,例如:生产环境MySQL和Redis需要使用集群配置,开发和测试环境使用单点配置就好了,这两种情况下的配置可能是不同的,这个时候@Profile就派上用场了。
|
DataSource是接口,DevDataSource和ProdDataSource是具体实现,根据运行环境的不同返回不同的实现。运行环境通过配置application.properties切换。
spring.profiles.active=dev |
@ImportResource
使用@ImportResource注解可以直接导入xml配置文件,这是从xml配置到java配置过渡的最简单方案。
|
@Import
和@ImportResource注解类似,使用@Import注解可以导入其他配置类
|
IoC容器
JUnit
我们的测试用例中使用ClassPathXmlApplicationContext做IoC容器,需要手工创建,并传入xml文件名。
Tomcat
Spring和Tomcat集成后,使用的是WebApplicationContext,通过ContextLoaderListener创建。
ContextLoaderListener配置在webapp/WEB-INF/web.xml中:
<listener> |
ContextLoaderListener是Tomcat的一个ServletContext,当它初始化的时候创建WebApplicationContext容器类。
package org.springframework.web.context; |
当Servelt容器启动时触发contextInitialized()方法,它执行基类ContextLoader的方法来构造容器
package org.springframework.web.context; |
这里可以看到最终调用createWebApplicationContext()方法返回WebApplicationContext类实例对象,这就是Tomcat中的IoC容器。后面的操作大家都了解了,WebApplicationContext容器负责管理Bean对象。
SpringBoot
我们再来看看目前流行的SpringBoot是如何创建Spring的IoC容器的。
SpringBoot其实更好理解一点,我们从程序入口开始看
|
入口是SpringApplication的run()方法
package org.springframework.boot; |
这里只截取了和IoC相关代码,可以看到run()方法中创建了context容器,并调用refreshContext()方法加载Bean对象。
package org.springframework.boot; |
类名太长,这里省略了包名。
由于我们大部分是web应用,所以使用的是AnnotationConfigEmbeddedWebApplicationContext,从名字上可以看出,这个ApplicationContext(容器)是基于注解创建的,支持嵌入式web应用。