1.概述
本文简要介绍Spring IOC的概念和用法。
IOC全称是Inversion of Control,即控制反转。
所谓 IOC ,就是由 Spring IOC 容器来负责维护对象的生命周期(创建、初始化、销毁)和对象之间的关系,而不用我们自己来new一个对象和管理它们。
2.Spring容器
在基于Spring的应用中,你的应用对象生存于Spring容器中,如下图:
![](https://img.haomeiwen.com/i17014808/d025a486f8e81ef2.png)
容器是Spring框架的核心,Spring容器使用DI管理构成应用的组件,它会创建相互协作的组件之间的关联,这些对象更加简单干净、易于理解、易于重用并且更易于单元测试。
Spring的容器不是只有一个,Spring自带了多个容器实现,可以归纳为2种类型:
-
bean工厂
bean工程由org.springframework.beans.factory.BeanFactory接口定义,它是最简单的容器,提供了基本的DI支持 -
应用上下文
由org.springframework.context.ApplicationContext接口定义,它是基于BeanFactory构建,并提供应用框架级别的服务
虽然我们可以在bean工程和应用上下文之间任选一种,但bean工厂对大多数应用来说往往太低级了,因此应用上下文比bean工厂更受欢迎,用此我们会把精力集中在应用上下文的使用上。
3.Spring应用上下文
Spring通过应用上下文(ApplicationContext)装载bean的定义并把它们组装起来,Spring应用上下文全权负责对象的创建和组装。
Spring自带了多种应用上下文的实现,它们之间的区别仅仅在于如何加载配置,常见的应用上下文有以下几种:
-
AnnotationConfigApplicationContext
从一个或多个基于Java的配置类中加载Spring应用上下文 -
AnnotationConfigWebApplicationContext
从一个或多个基于Java的配置类中加载Spring Web应用上下文 -
ClassPathXmlApplicationContext
从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源 -
FileSystemXmlApplicationContext
从文件系统下的一个或多个XML配置文件中加载上下文定义 -
XmlWebApplicationContext
从Web应用下的一个或多个XML配置文件中加载上下文定义
例如:
ApplicationContext context = new FileSystemXmlApplicationContext("E:/pandora.xml");
ApplicationContext context = new ClassPathXmlApplicationContext("pandora.xml");
ApplicationContext context = new AnnotationConfigApplicationContext(com.tp.pandora.config.PandoraConfig.class);
使用FileSystemXmlApplicationContext和使用ClassPathXmlApplicationContext的区别在于:FileSystemXmlApplicationContext在指定的文件系统路径下查找pandora.xml文件;而ClassPathXmlApplicationContext是在所有的类路径下(包含JAR文件)下查找pandora.xml文件。
应用上下文准备就绪后,我们就可以调用上下文的getBean()方法从Spring容器中获取bean。
4.bean的生命周期
在传统的Java应用中,bean的生命周期很简单:当我们使用关键字new进行bean实例化,这个bean就可以使用了,一旦bean不再被使用则由Java自动进行垃圾回收。
相比之下,Spring容器中的bean的生命周期就相对复杂多了,正确理解Spring bean的生命周期很重要,因为你或许要利用Spring提供的扩展点来自定义bean的创建过程,下图展示了bean装载到Spring应用上下文的一个典型的生命周期过程:
![](https://img.haomeiwen.com/i17014808/f66912a24e8fcda9.png)
在bean准备就绪之前,bean工厂执行了若干启动步骤:
- Spring对bean进行实例化
- Spring将值和bean的引用注入到bean对应的属性中
- 如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBeanName()方法
- 如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
- 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法将bean所在的应用上下文的引用传进来
- 如果bean实现了BeanPostProcesser接口,Spring将调用它们的postProcessBeforeInitialization()方法
- 如果bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet()方法
- 如果bean实现了BeanPostProcesser接口,Spring将调用它们的postProcessAfterInitialization()方法
- 此时bean已经准备就绪,可以被应用程序使用,它们将一直驻留在应用上下文中,直到该应用上下文被销毁
- 如果bean实现了DisposableBean接口,Spring将调用它的destroy()方法,同样如果bean使用destroy-method声明了销毁方法,该方法也会被调用
5.声明bean
在spring中声明一个bean,将这个bean交给spring容器管理,可以有如下3种方式:
- 在xml中显式声明一个bean
- 在Java中显式声明一个bean(JavaConfig)
- 通过注解自动发现
5.1在xml中声明一个bean
例如我们现在有个Course类,我们想用spring中声明一个由spring容器管理的Course:
package com.tp.beans.xml;
/**
* FileName: Course
* Author: TP
* Description:课程
*/
public class Course {
public Course() {
System.out.println("无参构造:Course被实例化");
}
public Course(String id, String name, int durations) {
this.id = id;
this.name = name;
this.durations = durations;
System.out.println("带参构造:Course被实例化");
}
/**
* 课程ID
*/
private String id;
/**
* 课程名称
*/
private String name;
/**
* 课程时长(单位天)
*/
private int durations;
//==========getter/setter==========
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getDurations() {
return durations;
}
public void setDurations(int durations) {
this.durations = durations;
}
@Override
public String toString() {
return "Course{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", durations=" + durations +
'}';
}
}
我们就可以在spring的配置文件applicationContext.xml中声明这个bean,注意下面的配置文件包含了整章教程的配置信息,对于当前测试内容,请集中关注这一段配置:
<bean id="xmlCourse1" class="com.tp.beans.xml.Course">
<property name="name" value="Java"/>
<property name="durations" value="24"/>
<property name="id" value="222"/>
</bean>
完整的applicationContext.xml如下:
<?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" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--开启注解扫描-->
<context:component-scan base-package="com.tp"/>
<!--############################构造器注入############################-->
<!--Spring xml声明bean-->
<!--构造器注入:构造器参数有List/Set情形-->
<bean id="xmlStudent1" class="com.tp.beans.xml.Student">
<constructor-arg name="name" value="xmlStudent1"/>
<constructor-arg name="age" value="18"/>
<constructor-arg name="courses">
<list>
<ref bean="xmlCourse1"/>
<ref bean="xmlCourse2"/>
</list>
</constructor-arg>
</bean>
<!--构造器注入:构造器参数有Map<String, Object>情形-->
<bean id="xmlMapTestBean" class="com.tp.beans.xml.MapTestBean">
<constructor-arg name="map">
<map>
<entry key="key1" value="123"/>
<entry key="key2" value-ref="xmlCourse1"/>
</map>
</constructor-arg>
</bean>
<!--############################属性注入############################-->
<!--属性注入:字面值常量-->
<bean id="xmlCourse1" class="com.tp.beans.xml.Course">
<property name="name" value="Java"/>
<property name="durations" value="24"/>
<property name="id" value="222"/>
</bean>
<!--属性注入:字面值常量-->
<bean id="xmlCourse2" class="com.tp.beans.xml.Course">
<property name="name" value="Python"/>
<property name="durations" value="25"/>
<property name="id" value="111"/>
</bean>
<!--属性注入:属性有bean情形-->
<bean id="xmlUser1" class="com.tp.beans.componentscan.User">
<property name="name" value="xmlUser1"/>
<property name="book" ref="javaConfigBook"/>
</bean>
<!--属性注入:属性有数组情形-->
<bean id="xmlUser2" class="com.tp.beans.componentscan.User">
<property name="name" value="xmlUser2"/>
<property name="book" ref="javaConfigBook"/>
<property name="favorites">
<array>
<value>篮球</value>
<value>电影</value>
<value>唱歌</value>
</array>
</property>
</bean>
<!--属性注入:使用P-命名空间-->
<bean id="xmlUser3" class="com.tp.beans.componentscan.User" p:name="xmlUser3" p:book-ref="javaConfigBook">
<property name="favorites">
<array>
<value>篮球</value>
<value>电影</value>
<value>唱歌</value>
</array>
</property>
</bean>
<!--属性注入,属性是List<Courses>情形-->
<bean id="xmlStudent2" class="com.tp.beans.xml.Student">
<property name="name" value="xmlStudent2"/>
<property name="courses">
<list>
<ref bean="xmlCourse1" />
<ref bean="xmlCourse2" />
</list>
</property>
</bean>
<!--测试多环境-->
<!--<beans profile="dev">
<bean id="dataSource"
class="com.tp.beans.profile.DataSource"
p:userName="root"
p:passWord="tp123456"
p:url="jdbc:mysql://127.0.0.1:3306/jdbcStudy_dev"/>
</beans>
<beans profile="test">
<bean id="dataSource"
class="com.tp.beans.profile.DataSource"
p:userName="root"
p:passWord="tp123456"
p:url="jdbc:mysql://127.0.0.1:3306/jdbcStudy_test"/>
</beans>
<beans profile="prod">
<bean id="dataSource"
class="com.tp.beans.profile.DataSource"
p:userName="root"
p:passWord="tp123456"
p:url="jdbc:mysql://127.0.0.1:3306/jdbcStudy_prod"/>
</beans>-->
</beans>
测试:
package com.tp;
import com.tp.beans.xml.Course;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* FileName: Main
* Author: TP
* Description:
*/
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("config/applicationContext.xml");
Course course = (Course) ctx.getBean("xmlCourse1");
System.out.println(">>>>" + course);
}
}
我们通过spring上下文提供的getBean(String name)
方法可以从spring容器中获取一个指定名称的bean
效果:
![](https://img.haomeiwen.com/i17014808/43b3c0fb94e1341e.png)
注意:由于项目代码为了讲解本章内容,还声明了很多其他的bean,所以大家可以看到控制台打印出很多信息。
大家可以暂时不关注那些控制台信息,而只关注我们本次测试执行对应的控制台信息即可,下面所有的测试同理。
5.2通过Spring的注解声明一个bean
Spring为了我们提供了@Controller、@Service、@Repository、@Component等注解来声明一个 bean,他们之间并没有什么区别,只不过@Controller、@Service、@Repository在语意上更加明确,而@Component更加通用,日常开发中建议能优先@Controller、@Service、@Repository,@Component次之,保证语意更明确,让人一目了然。
为了演示Spring注解声明bean,我们定义了一个User类:
package com.tp.beans.componentscan;
import com.tp.beans.javaconfig.Book;
import com.tp.service.AnimalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* FileName: User
* Author: TP
* Description:用户类,用来测试注解扫描方式声明bean
*/
@Component("componentUser")
public class User {
public User() {
System.out.println("无参构造:User被实例化");
}
// 构造器注入
// @Autowired
public User(Book book) {
System.out.println("带参构造:User被实例化");
this.book = book;
}
private Integer id;
private String name;
private Integer age;
private String[] favorites;
private Book book;
// 接口注入
@Autowired
private AnimalService animalService;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String[] getFavorites() {
return favorites;
}
public void setFavorites(String[] favorites) {
this.favorites = favorites;
}
public Book getBook() {
return book;
}
// Setter注入
// @Autowired
public void setBook(Book book) {
this.book = book;
}
// 非Setter注入
// @Autowired
public void injectBook(Book book) {
this.book = book;
}
public AnimalService getAnimalService() {
return animalService;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", favorites=" + Arrays.toString(favorites) +
", book=" + book +
'}';
}
}
当我们使用注解时,还需要开启注解扫描,开启注解驱动的方式有2种(见下文6.1.1)我们选择其中一种形式,即在applicationContext.xml中开启注解扫描:
<?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 https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.tp" />
</beans>
Spring会扫描com.tp包及所有子包中带有@Controller、@Service、@Repository、@Component注解的类,并将此类初始化到Spring容器中进行管理。
为了后续的测试方便,我们采用Spring-Junit测试,测试准备如下,项目引入spring-test和junit的jar包,编写一个测试基础类用于指定测试环境下的Spring配置,其他的测试类继承此BaseTestCase即可:
//指定测试用例的运行器 这里是指定了Junit4
@RunWith(SpringJUnit4ClassRunner.class)
//指定Spring的配置文件 /为classpath下
@ContextConfiguration(locations = {"classpath:config/*.xml"})
public class BaseTestCase extends AbstractJUnit4SpringContextTests {
}
public class ComponentScanBeanTest extends BaseTestCase {
/**
* 最简单的基于注解扫描的JavaBean声明
*/
@Test
public void simpleComponentScanStatement() {
User user = (User) applicationContext.getBean("componentUser");
System.out.println(">>>基于注解扫描的JavaBean声明:" + user);
System.out.println(">>>基于注解扫描的JavaBean声明,注入的book:" + user.getBook());
}
}
结果:
![](https://img.haomeiwen.com/i17014808/e88cde21a047e063.png)
5.3通过JavaConfig声明一个bean
当随着JAVA EE 5.0的发布,其中引入了一个非常重要的特性——Annotations(注解),注解是源代码的标签,这些标签可以在源代码层进行处理或通过编译器把它熔入到class文件中。
Java config是指基于java配置的spring,传统的Spring一般都是基本xml配置的,后来spring3.0新增了许多java config的注解,特别是spring boot,基本都是清一色的java config。
Spring使用注解来描述Bean的配置与采用XML相比,因类注解是在一个类源代码中,可以获得类型安全检查的好处,并且可以良好的支持重构。
常见的java config的注解:
-
@Configuration
在类上打上这一标签,表示这个类是配置类 -
@ComponentScan
相当于xml的<context:componentscan basepakage="xxxx">
-
@Bean
bean的定义,相当于xml的
<bean id="objectMapper" class="org.codehaus.jackson.map.ObjectMapper" />
-
@EnableWebMvc
相当于xml的<mvc:annotation-driven>
-
@ImportResource
相当于xml的<import resource="applicationContext-cache.xml">
-
@PropertySource
spring 3.1开始引入,它是基于java config的注解,用于指定读取的properties文件 -
@Profile
spring3.1开始引入,一般用于多环境配置,激活时可用@ActiveProfiles注解,@ActiveProfiles("dev")
等同于xml配置:
<beans profile="dev"> <bean id="beanname" class="com.tp.xxx"/> </beans>
为了演示JavaConfig我们定义了一个Book类如下:
package com.tp.beans.javaconfig;
/**
* FileName: Book
* Author: TP
* Description: Book,用来测试JavaConfig方式声明bean
*/
public class Book {
public Book() {
System.out.println("无参构造:Book被实例化");
}
private Integer id;
private String name;
private Double price;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
'}';
}
}
并且定义一个AutoConfig类帮助我们声明bean:
package com.tp.autoconfig;
import com.tp.beans.condition.Cat;
import com.tp.beans.javaconfig.Book;
import com.tp.beans.componentscan.User;
import com.tp.beans.profile.DataSource;
import com.tp.conditon.LinuxCondition;
import com.tp.conditon.MacCondition;
import com.tp.conditon.WindowsCondition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* FileName: AutoConfig
* Author: TP
* Date: 2020-01-13 13:58
* Description: JavaConfig声明bean
*/
@Configuration
public class AutoConfig {
@Bean("javaConfigBook")
public Book book() {
Book book = new Book();
book.setId(222);
book.setPrice(33.88);
book.setName("简书");
return book;
}
//======================JavaConfig的构造器注入======================
@Bean
public User javaConfigUserConstructorInject(Book book) {
User user = new User(book);
user.setId(666);
user.setAge(33);
user.setName("Tony老师");
return user;
}
//======================JavaConfig的Setter注入======================
@Bean("javaConfigUser")
public User user(Book book) {
User user = new User();
user.setId(4);
user.setAge(18);
user.setName("Tom老师");
//setter注入
user.setBook(book);
return user;
}
//======================用于多环境测试bean声明======================
@Bean("dataSource")
@Profile("dev")
DataSource devDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUserName("root");
dataSource.setUserName("tp123456");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/jdbcStudy_dev");
return dataSource;
}
@Bean("dataSource")
@Profile("test")
DataSource testDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUserName("root");
dataSource.setUserName("tp123456");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/jdbcStudy_test");
return dataSource;
}
@Bean("dataSource")
@Profile("prod")
DataSource prodDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUserName("root");
dataSource.setUserName("tp123456");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/jdbcStudy_prod");
return dataSource;
}
//======================用于条件注解测试bean声明======================
@Bean("cat")
@Conditional(MacCondition.class)
Cat macCat(){
Cat cat = new Cat();
cat.setName("Mac Cat");
return cat;
}
@Bean("cat")
@Conditional(WindowsCondition.class)
Cat windowsCat(){
Cat cat = new Cat();
cat.setName("Windows Cat");
return cat;
}
@Bean("cat")
@Conditional(LinuxCondition.class)
Cat linuxCat(){
Cat cat = new Cat();
cat.setName("Linux Cat");
return cat;
}
}
从该类的book()方法中我们定义了一个id为"javaConfigBook"的bean
测试:
@Test
public void simpleJavaConfigBeanStatement(){
Book book = (Book) applicationContext.getBean("javaConfigBook");
System.out.println(">>>基于JavaConfig的JavaBean声明:" + book);
}
效果:
![](https://img.haomeiwen.com/i17014808/8698594b637fa74e.png)
6.装配Bean
装配:创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质。
在Spring中装配bean有多种方式,Spring提供了三种主要的装配机制:
- 在XML中进行显式配置
- 在JavaConfigg中进行显式配置
- 隐式的bean发现机制和自动配置
Spring的不同配置方式是允许互相搭配的,也就是说这三种方式可以兼容并存,我们可以选择使用XML装配一些bean,使用基于Java的配置(JavaConfig)来装配另一些bean,而将剩余的bean让Spring去自动发现。
官方建议尽可能的使用自动配置的机制,显式配置越少越好。
Spring从两个角度来实现自动化装配:
-
组件扫描(component scanning)
Spring会自动发现应用上下文中所创建的bean -
自动装配(autowiring)
Spring自动满足bean之间的依赖
组件扫描配合自动装配可以发挥巨大的威力,能够将显式配置降到最少
6.1 隐式的bean发现机制和自动配置:组件扫描装配bean
下面以一个示例展示如何自动化装配bean,首先我们定义一个接口Animal:
public interface Animal {
void eat();
}
接下来我们写一个Animal的实现类AnimalServiceImpl:
package com.tp.service.impl;
import com.tp.service.AnimalService;
import org.springframework.stereotype.Service;
/**
* FileName: AnimalServiceImpl
* Author: TP
* Description:
*/
@Service
public class AnimalServiceImpl implements AnimalService {
@Override
public void eat() {
System.out.println("动物在吃饭....");
}
}
注意我们在AnimalServiceImpl类上使用了@Service注解,这个简单的注解表明该类会作为组件类,并告知Spring要为这个类创建bean,Spring会帮我们创建这个bean。
但是组件扫描默认是不启用的,我们仍然需要显示的配置一下以开启Spring的组件扫描,从而命令Spring去寻找带有@Service注解的类,并为其创建bean
6.1.1如何开启组件扫描呢
-
1.使用JavaConfig开启组件扫描
我们先定义一个类:
@Configuration
@ComponentScan
public class ComponentScanConfig {
}
类ComponentScanConfig通过Java代码定义类Spring的装配规则,它使用了@ComponentScan这个注解,这个注解能够在Spring中启用组件扫描。
如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包(包括子包)。
如果你想指定不同的基础包,可以设置@ComponentScan的value属性为你要指定的基础包,例如:
@Configuration
@ComponentScan("com.tp")
public class ComponentScanConfig {
}
如果你想更加清晰地表明你所设置的是基础包,可以通过basePackages属性进行配置:
@Configuration
@ComponentScan(basePackages = "com.tp")
public class ComponentScanConfig {
}
basePackages其实可以指定多个基础包,只需要将basePackages属性设置为一个数组即可:
@Configuration
@ComponentScan(basePackages = {"com.tp.pandora", "com.tp.athena"})
public class ComponentScanConfig {
}
上面的例子中所设置的基础包都是以String类型表示的,可以实现包扫描配置。
但是这种方法是类型不安全(not type-safe)的,如果哪一天我们重构了代码,那么这里指定的基础包就可能出现错误了,其实@ComponentScan还为我们提供了另外一种方法,就是将其指定为包中所包含的类或接口,所指定的类或接口所在的包将会作为组件扫描的基础包,例如:
@Configuration
@ComponentScan(basePackageClasses = {Dog.class, Cat.class})
public class ComponentScanConfig {
}
-
2.使用XML开启组件扫描
在Xml配置文件中加入如下代码即可开启组件扫描
<context:component-scan base-package="com.tp" />
6.1.2为组件扫描的bean命名
Spring应用上下文中所有的bean都会给定一个ID,上述代码中虽然我们没有明确的为AnimalServiceImpl类设置ID,但Spring会根据类型为其指定一个ID,规则是类名且首字母小写(本例即为animalServiceImpl)。
如果你想为这个bean设置一个不同的ID,只需要将你期望的ID传递给@Service即可,例如:
@Service("lonelyDog")
public class AnimalServiceImpl implements AnimalService {
@Override
public void eat() {
System.out.println("动物在吃饭....");
}
}
还有另外一种为bean命名的方式——@Named,@Named是Java依赖注入规范提供的注解。
Spring支持将@Named作为@Component的替代方案,两者之间差距不大,在绝大多数场景二者是可以互相替换的。
但是更推荐使用@Component注解。
@Named("lonelyDog")
public class AnimalServiceImpl implements AnimalService {
@Override
public void eat() {
System.out.println("动物在吃饭....");
}
}
6.1.3为组件扫描的bean进行自动装配
自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。
为了声明要进行自动装配,我们可以借助Spring的@Autowired注解。
对于组件扫描自动装配bean这种方式,Spring可以有3种依赖注入形式:
-
构造器注入
@Autowired使用到构造方法上 -
接口注入
当我们注入一个接口时,实际注入的是这个接口的实现类 -
方法注入
@Autowired使用到非构造方法上
个别文章中会说成setter注入
,个人觉得描述不够准确,因为Spring不仅支持属性的setter方法上进行自动注入,也支持任意非setter方法上进行自动注入,见下文
6.1.3-1 构造器注入
我们改造一下User和类如下,构造器上加入@Autowired注解:
package com.tp.beans.componentscan;
import com.tp.beans.javaconfig.Book;
import com.tp.service.AnimalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* FileName: User
* Author: TP
* Description:用户类,用来测试注解扫描方式声明bean
*/
@Component("componentUser")
public class User {
public User() {
System.out.println("无参构造:User被实例化");
}
// 构造器注入
@Autowired
public User(Book book) {
System.out.println("带参构造:User被实例化");
this.book = book;
}
private Integer id;
private String name;
private Integer age;
private String[] favorites;
private Book book;
// 接口注入
// @Autowired
private AnimalService animalService;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String[] getFavorites() {
return favorites;
}
public void setFavorites(String[] favorites) {
this.favorites = favorites;
}
public Book getBook() {
return book;
}
// Setter注入
// @Autowired
public void setBook(Book book) {
this.book = book;
}
// 非Setter注入
// @Autowired
public void injectBook(Book book) {
this.book = book;
}
public AnimalService getAnimalService() {
return animalService;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", favorites=" + Arrays.toString(favorites) +
", book=" + book +
'}';
}
}
User类的构造器上添加类@Autowired注解,这表明当Spring创建User bean的时候会通过这个构造器进行实例化,并且会在Spring容器中找一个Book类型的bean,传入到构造函数中,测试代码如下:
/**
* 演示构造器注入
*/
@Test
public void simpleComponentScanConstructorInject() {
User user = (User) applicationContext.getBean("componentUser");
System.out.println(">>>基于注解扫描的JavaBean声明:" + user);
System.out.println(">>>基于注解扫描的JavaBean声明,注入的book:" + user.getBook());
}
运行结果:
![](https://img.haomeiwen.com/i17014808/bdd657a62f17d8d9.png)
6.1.3-2 方法注入
@Autowired注解不仅可以用在构造方法上,也可以用在属性的setter方法上。
例如我们重新改造一下User类,这次将构造方法上的@Autowired注释掉,打开setter方法上的@Autowired:
package com.tp.beans.componentscan;
import com.tp.beans.javaconfig.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* FileName: User
* Author: TP
* Description:用户类,用来测试注解扫描方式声明bean
*/
@Component("componentUser")
public class User {
public User() {
System.out.println("无参构造:User被实例化");
}
// 构造器注入
// @Autowired
public User(Book book) {
System.out.println("带参构造:User被实例化");
this.book = book;
}
private Integer id;
private String name;
private Integer age;
private String[] favorites;
// 接口注入
// @Autowired
private AnimalService animalService;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String[] getFavorites() {
return favorites;
}
public void setFavorites(String[] favorites) {
this.favorites = favorites;
}
public Book getBook() {
return book;
}
// Setter注入
@Autowired
public void setBook(Book book) {
this.book = book;
}
// 非Setter注入
// @Autowired
public void injectBook(Book book) {
this.book = book;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", favorites=" + Arrays.toString(favorites) +
", book=" + book +
'}';
}
}
测试:
/**
* 打开User的Setter上的@Autowired注解,演示Setter注入
*/
@Test
public void simpleComponentScanSetterInject() {
User user = (User) applicationContext.getBean("componentUser");
System.out.println(">>>基于注解扫描的JavaBean声明:" + user);
System.out.println(">>>基于注解扫描的JavaBean声明,注入的book:" + user.getBook());
}
运行效果:
![](https://img.haomeiwen.com/i17014808/c22c0754899bc209.png)
在Spring初始化后,它会尽可能去满足bean的依赖。
实际上Setter方法并没有什么特殊之处,@Autowired可以用在类的任意方法上
并发挥完全相同的作用。
例如我们将@Autowired用在下面示例的injectBook(Book book)方法上:
package com.tp.beans.componentscan;
import com.tp.beans.javaconfig.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* FileName: User
* Author: TP
* Description:用户类,用来测试注解扫描方式声明bean
*/
@Component("componentUser")
public class User {
public User() {
System.out.println("无参构造:User被实例化");
}
// 构造器注入
// @Autowired
public User(Book book) {
System.out.println("带参构造:User被实例化");
this.book = book;
}
private Integer id;
private String name;
private Integer age;
private String[] favorites;
// 接口注入
// @Autowired
private AnimalService animalService;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String[] getFavorites() {
return favorites;
}
public void setFavorites(String[] favorites) {
this.favorites = favorites;
}
public Book getBook() {
return book;
}
// Setter注入
// @Autowired
public void setBook(Book book) {
this.book = book;
}
// 非Setter注入
@Autowired
public void injectBook(Book book) {
this.book = book;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", favorites=" + Arrays.toString(favorites) +
", book=" + book +
'}';
}
}
测试:
@Test
public void simpleComponentScanNotSetterInject() {
User user = (User) applicationContext.getBean("componentUser");
System.out.println(">>>基于注解扫描的JavaBean声明:" + user);
System.out.println(">>>基于注解扫描的JavaBean声明,注入的book:" + user.getBook());
}
运行效果:
![](https://img.haomeiwen.com/i17014808/c22c0754899bc209.png)
6.1.3-3 接口注入
User类改造如下,增加一个属性:private AnimalService animalService;
并用@Autowired进行标注
package com.tp.beans.componentscan;
import com.tp.beans.javaconfig.Book;
import com.tp.service.AnimalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* FileName: User
* Author: TP
* Description:用户类,用来测试注解扫描方式声明bean
*/
@Component("componentUser")
public class User {
public User() {
System.out.println("无参构造:User被实例化");
}
// 构造器注入
// @Autowired
public User(Book book) {
System.out.println("带参构造:User被实例化");
this.book = book;
}
private Integer id;
private String name;
private Integer age;
private String[] favorites;
// 接口注入
@Autowired
private AnimalService animalService;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String[] getFavorites() {
return favorites;
}
public void setFavorites(String[] favorites) {
this.favorites = favorites;
}
public Book getBook() {
return book;
}
// Setter注入
// @Autowired
public void setBook(Book book) {
this.book = book;
}
// 非Setter注入
// @Autowired
public void injectBook(Book book) {
this.book = book;
}
public AnimalService getAnimalService() {
return animalService;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", favorites=" + Arrays.toString(favorites) +
", book=" + book +
'}';
}
}
测试:
@Test
public void simpleComponentScanStatement() {
User user = (User) applicationContext.getBean("componentUser");
System.out.println(">>>基于注解扫描的JavaBean声明:" + user);
System.out.println(">>>基于注解扫描的JavaBean声明,注入的book:" + user.getBook());
System.out.println(user.getAnimalService());
user.getAnimalService().eat();
}
运行效果:
![](https://img.haomeiwen.com/i17014808/89275741b8080b1f.png)
不管是构造器注入、Setter方法注入还是其他方法注入,Spring都会尝试满足方法参数上所声明的依赖,加入有且仅有一个bean匹配依赖bean的时候,这个bean就会被装配进来,如果没有匹配的bean,那么在应用上下文创建的时候 Spring会抛出异常,在某种特定场景或业务需求下你可能要避免这个异常,这个时候你可以将@Autowired的required属性设置为false(@Autowired默认值为true)。
将required设置为false时,Spring会 尝试执行自动装配,但是如果没有匹配的bean的话,Spring将会让这个bean处于未装配状态,此时你需要谨慎对待,防止你的代码出现NullPointerException异常。
@Autowired是Spring特有的注解,如果你不想使用@Autowired也可以使用@Inject替代(@Inject注解来源于Java依赖注入规范),尽管二者存在着一些细微的差别,但是在绝大多数情况下二者可以相互替代,个人更推荐@Autowired
6.2JavaConfig装配bean
尽管在很多场景下通过组件扫描和自动装配实现Spirng自动化配置是更为推荐的方式,但是有时候自动化配置的方案行不通,例如你想将第三方库中的组件装配到你的应用中,这种情况下是没法在它的类上添加@Component和@Autowired注解的,这种情况下我们就不能使用上述的自动化装配方案了,只能使用显式装配的方式,显式装配有2种方案:JavaConfig或xml,下面介绍如何使用JavaConfig装配bean
创建JavaConfig类的关键在于为其添加@Configuration注解,该注解表明这个类是一个配置类,更类应该包含在Spring应用上下文中如何创建bean的细节,在上面的Spring声明简单bean的学习中我们已经学会了如何创建一个JavaConfig。
通常在JavaConfig中声明一个bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个bean添加@Bean注解,@Bean注解会告诉Spring这个方法将返回一个对象,这个对象要注册为Spring上下文中的一个bean,方法体中包括了最终产生这个bean的逻辑。
默认情况下,bean的id与方法名一致,如果你想为其设置一个不同的名字的话,你可以选择重命名这个方法,也可以@Bean的name属性指定一个不同的名字,例如 :
package com.tp.autoconfig;
import com.tp.bean.Book;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* FileName: AutoConfig
* Author: TP
* Description: JavaConfig声明一个bean
*/
@Configuration
public class AutoConfig {
@Bean(name = "springInAction")
public Book book(){
Book book = new Book();
return book;
}
}
6.2.1JavaConfig实现注入
我们上面的book是非常简单的bean,它自身没有其他的依赖,但是我们知道User这个bean有个构造函数是需要依赖book的,那么我们怎么样用JavaConfig的方式配置User呢?
-
JavaConfig构造器注入
上面的User类的带参构造中需要一个Book参数,代码片段:
public User(Book book) {
System.out.println("带参构造:User被实例化");
this.book = book;
}
JavaConfig中,可以用如下的方式实现构造器注入:
//======================JavaConfig的构造器注入======================
@Bean
public User javaConfigUserConstructorInject(Book book) {
User user = new User(book);
user.setId(666);
user.setAge(33);
user.setName("Tony老师");
return user;
}
运行测试类:
/**
* JavaConfig声明bean+构造器注入
*/
@Test
public void javaConfigConstructorInject(){
User user = (User) applicationContext.getBean("javaConfigUserConstructorInject");
System.out.println("JavaConfig声明bean+构造器注入:" + user);
}
效果:
![](https://img.haomeiwen.com/i17014808/994ea53261808647.png)
上面我们实现了JavaConfig的构造器注入,实际我们还可以使用setter注入:
- JavaConfig属性Setter注入
//======================JavaConfig的Setter注入======================
@Bean("javaConfigUser")
public User user(Book book) {
User user = new User();
user.setId(4);
user.setAge(18);
user.setName("Tom老师");
//setter注入
user.setBook(book);
return user;
}
测试:
/**
* JavaConfig声明bean+setter注入
*/
@Test
public void javaConfigSetterInject(){
User user = (User) applicationContext.getBean("javaConfigUser");
System.out.println("JavaConfig声明bean+setter注入:" + user);
}
运行结果:
![](https://img.haomeiwen.com/i17014808/715246101e7e0062.png)
6.3通过xml装配bean
Spring的xml只有一种声明bean的方式:通过<bean>标签声明
但是对于DI,却有多种声明DI的方式:
对于构造器注入,有2种方式:
- <constructor-arg>
- 使用Spring3.0引入的C-命名空间
6.3.1构造器采用<constructor-arg>注入示例
<bean id="xmlUser0" class="com.tp.beans.componentscan.User">
<constructor-arg name="book" ref="javaConfigBook"/>
</bean>
从上面的配置我们可以看出,我们声明xmlUser0这个bean时我们通过<constructor-arg name="book" ref="javaConfigBook"/>
指明了构造参数book指向id为"javaConfigBook"的这个bean
测试:
@Test
public void test0(){
User user = (User) applicationContext.getBean("xmlUser0");
System.out.println(">>>>" + user);
}
运行效果:
![](https://img.haomeiwen.com/i17014808/3a93c64e6d00447b.png)
6.3.2 构造器采用C-命名空间注入示例
C-命名空间是为了更加简洁的描述构造器参数,如果我们的bean时多个构造参数,我们就可以不必写多个<constructor-arg>,而使用C-命名空间替代:
<!--构造器注入:构造器参数为bean情形,使用c标签-->
<bean id="xmlUserC" class="com.tp.beans.componentscan.User" c:book-ref="javaConfigBook" />
C-命名空间的写法规则:
![](https://img.haomeiwen.com/i17014808/c41b522feaea3df6.png)
同时C-命名空间支持构造器参数名使用构造器参数位置的索引替代,例如上面的改成:
<bean id="xmlUserC" class="com.tp.beans.componentscan.User" c:_0-ref="javaConfigBook" />
结果是一样的
当你的构造器参数只有一个的时候,你甚至可以不写参数名也不写索引位置,类似这样:
<bean id="xmlUserC" class="com.tp.beans.componentscan.User" c:_-ref="javaConfigBook" />
如果构造器参数想注入字面值常量,使用c:参数名="字面值常量"
即可
但有一点需要注意,并不是所有情况下C-命名空间都能替代<constructor-arg>,当构造参数为字面值常量或某个对象时二者可以相互替代,但当构造参数为集合时,C-命名空间是无能为力的,而<constructor-arg>可以做到,例如我们有个学生类Student和课程类Course分别如下:
package com.tp.beans.xml;
import java.util.List;
import java.util.Properties;
/**
* FileName: Student
* Author: TP
* Description:
*/
public class Student {
public Student() {
System.out.println("无参构造:Student被实例化");
}
public Student(String name, String age, List<Course> courses) {
this.name = name;
this.age = age;
this.courses = courses;
System.out.println("带参构造:Student被实例化");
}
private String name;
private String age;
private List<Course> courses;
private Properties properties;
//==========getter/setter==========
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public List<Course> getCourses() {
return courses;
}
public void setCourses(List<Course> courses) {
this.courses = courses;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
", courses=" + courses +
'}';
}
}
package com.tp.beans.xml;
/**
* FileName: Course
* Author: TP
* Description:课程
*/
public class Course {
public Course() {
System.out.println("无参构造:Course被实例化");
}
public Course(String id, String name, int durations) {
this.id = id;
this.name = name;
this.durations = durations;
System.out.println("带参构造:Course被实例化");
}
/**
* 课程ID
*/
private String id;
/**
* 课程名称
*/
private String name;
/**
* 课程时长(单位天)
*/
private int durations;
//==========getter/setter==========
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getDurations() {
return durations;
}
public void setDurations(int durations) {
this.durations = durations;
}
@Override
public String toString() {
return "Course{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", durations=" + durations +
'}';
}
}
此时如果Student使用带参构造器声明一个bean,我们就可以这样写:
<!--构造器注入:构造器参数有List/Set情形-->
<bean id="xmlStudent1" class="com.tp.beans.xml.Student">
<constructor-arg name="name" value="xmlStudent1"/>
<constructor-arg name="age" value="18"/>
<constructor-arg name="courses"><null/></constructor-arg>
</bean>
当然这里我们将courses设置为类null,所以就不给代码示例了,如果不想设置null可以这样写:
<!--构造器注入:构造器参数有List/Set情形-->
<bean id="xmlStudent1" class="com.tp.beans.xml.Student">
<constructor-arg name="name" value="xmlStudent1"/>
<constructor-arg name="age" value="18"/>
<constructor-arg name="courses">
<list>
<ref bean="xmlCourse1"/>
<ref bean="xmlCourse2"/>
</list>
</constructor-arg>
</bean>
如果构造参数集合的泛型是String等非自定义的bean,比如我们给User这个类增加1个构造函数:
public User(String name, String[] favorites) {
this.name = name;
this.favorites = favorites;
}
那么xml声明时可以这样写:
<!--构造器注入:构造器参数有数组情形-->
<bean id="xmlUser4" class="com.tp.beans.componentscan.User" c:name="小张">
<constructor-arg name="favorites">
<list>
<value>吃饭</value>
<value>睡觉</value>
<value>打豆豆</value>
</list>
</constructor-arg>
</bean>
测试:
/**
* 构造器注入:构造器参数有数组情形
*/
@Test
public void testCnsotructorArray(){
User user = (User) applicationContext.getBean("xmlUser4");
System.out.println(">>>>" + user);
}
结果:
![](https://img.haomeiwen.com/i17014808/ef43d86e2ebd2261.png)
如果是set集合则将<list>改成<set>,如果为<set>则所有的重复值都会被忽略,存放顺序也无法得到保证。
如果是数组Array则将<list>改成<array>,注意不改也是可以的<array>节点可以被<list>节点替代
<!--构造器注入:构造器参数有List/Set情形-->
<bean id="xmlStudent1" class="com.tp.beans.xml.Student">
<constructor-arg name="name" value="xmlStudent1"/>
<constructor-arg name="age" value="18"/>
<constructor-arg name="courses">
<list>
<ref bean="xmlCourse1"/>
<ref bean="xmlCourse2"/>
</list>
</constructor-arg>
</bean>
测试:
/**
* 构造器注入:构造器参数有List/Set情形
*/
@Test
public void test1() {
Student student = (Student) applicationContext.getBean("xmlStudent1");
System.out.println(">>>>" + student);
}
结果:
![](https://img.haomeiwen.com/i17014808/ff6bfb500619bf65.png)
如果构造参数是Map<String, Object>,假设有个bean定义如下:
package com.tp.beans.xml;
import java.util.Map;
/**
* FileName: MapTestBean
* Author: TP
* Description:
*/
public class MapTestBean {
public MapTestBean(Map<String, Object> info) {
this.info = info;
}
private Map<String, Object> info;
public Map<String, Object> getInfo() {
return info;
}
public void setInfo(Map<String, Object> info) {
this.info = info;
}
@Override
public String toString() {
return "MapTestBean{" +
"info=" + info +
'}';
}
}
那么在Spring XML中就可以这么写:
<!--构造器注入:构造器参数有Map<String, Object>情形-->
<bean id="xmlMapTestBean" class="com.tp.beans.xml.MapTestBean">
<constructor-arg name="info">
<map>
<entry key="key1" value="value1"/>
<entry key="key2" value-ref="xmlCourse1"/>
</map>
</constructor-arg>
</bean>
测试:
/**
* 构造器注入:构造器参数有Map<String, Object>情形
*/
@Test
public void test2() {
MapTestBean mapTestBean = (MapTestBean) applicationContext.getBean("xmlMapTestBean");
System.out.println(">>>>" + mapTestBean);
}
结果:
![](https://img.haomeiwen.com/i17014808/81cd0e4ca8b76b89.png)
6.3.3 属性注入
在上面的例子中我们侧重讲解通过构造器注入,接下来我们看一下如何通过Spring XML实现属性注入,属性注入也可以有2种方式:
- <property>
- P-命名空间
使用<property>实现属性注入:
例如Course类:
<!--属性注入:字面值常量-->
<bean id="xmlCourse1" class="com.tp.beans.xml.Course">
<property name="name" value="Java"/>
<property name="durations" value="24"/>
<property name="id" value="222"/>
</bean>
<!--属性注入:字面值常量-->
<bean id="xmlCourse2" class="com.tp.beans.xml.Course">
<property name="name" value="Python"/>
<property name="durations" value="25"/>
<property name="id" value="111"/>
</bean>
如果属性是一个JavaBean可以使用ref进行指定:
<!--属性注入:属性有bean情形-->
<bean id="xmlUser1" class="com.tp.beans.componentscan.User">
<property name="name" value="xmlUser1"/>
<property name="book" ref="javaConfigBook"/>
</bean>
如果属性是一个数组可以使用<array>进行指定,<array>可以被<list>替代:
<bean id="xmlUser2" class="com.tp.beans.componentscan.User">
<property name="name" value="xmlUser2"/>
<property name="book" ref="javaConfigBook"/>
<property name="favorites">
<array>
<value>篮球</value>
<value>电影</value>
<value>唱歌</value>
</array>
</property>
</bean>
如果属性是一个List<对象>:
<!--属性注入,属性是List<Courses>情形-->
<bean id="xmlStudent2" class="com.tp.beans.xml.Student">
<property name="name" value="xmlStudent2"/>
<property name="courses">
<list>
<ref bean="xmlCourse1" />
<ref bean="xmlCourse2" />
</list>
</property>
</bean>
注意:既可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean
如果属性是一个Map<String, Object>:
<property name="map">
<map>
<entry key="age" value="99"/>
<entry key="name" value="javaboy"/>
</map>
</property>
如果属性是一个java.util.Properties:
<property name="info">
<props>
<prop key="age">99</prop>
<prop key="name">javaboy</prop>
</props>
</property>
使用P-命名空间实现属性注入:
属性的P-命名空间
和构造器的C-命名空间
用法是相似的,P-命名空间
的格式为:
![](https://img.haomeiwen.com/i17014808/6e1e1faa4ea48511.png)
与C-命名空间
相似,装配bean应用与装配字面量的唯一区别在于是否带有" -ref "后缀,例如:
<!--属性注入:使用P-命名空间-->
<bean id="user4" class="com.tp.beans.User" p:name="用户4" p:book-ref="book">
<property name="favorites">
<array>
<value>篮球</value>
<value>电影</value>
<value>唱歌</value>
</array>
</property>
</bean>
网友评论