全注解下的Spring IoC

一、IoC容器简介

? IoC容器是Spring的核心,可以说Spring是一种基于IoC容器编程的框架。IoC是一种通过描述来生成或者获取对象的技术。Java初学者更多的时候熟悉的是使用new关键字来创建对象,而Spring是通过描述来创建对象的。

? 在Spring中把每一个需要管理的对象称为Spring Bean(简称Bean),而Spring管理这些Bean的容器,被我们称为Spring IoC容器(或者简称IoC容器)。IoC容器具备两个基本的功能:

  • 通过描述管理Bean,包括发布和获取Bean;
  • 通过描述完成Bean之间的依赖关系。

? Spring IoC容器是一个管理Bean的容器,在Spring的定义中,所有的IoC容器都需要实现接口BeanFactory,它是一个顶级容器接口。我们需要注意接口中的几个方法:

  • 首先是多个getBean方法,这是IoC容器中最重要的方法之一,它的作用是从IoC容器中获取Bean,我们可以通过Bean的类型或者名称来获取Bean。
  • isSingleton方法用来判断Bean是否在Spring IoC中为单例。这里需要记住的是在Spring IoC容器中,默认的情况下Bean都是一单例存在的,也就是说getBean方法返回的都是同一个对象。
  • 与isSingleton方法相反的是isPrototype方法,如果它返回的是true,那么当我们使用getBean方法获取Bean的时候,Spring IoC容器就会创建一个新的Bean返回给调用者。

? 由于BeanFactory的功能还不够强大,因此Spring在BeanFactory的基础上,还设计了一个更为高级的接口ApplicationContext。它是BeanFactory的子接口之一,在Spring的体系中BeanFactoryApplicationContext是最为重要的接口设计,在现实中我们使用的大部分Spring IoC容器都是ApplicationContext接口的实现类。

? 在Spring Boot当中我们主要是通过注解来装配Bean到Spring IoC容器中,为了贴近Spring Boot的需要,这里不再介绍与XML相关的IoC容器,而主要介绍一个基于注解的IoC容器,它就是AnnotationConfigApplicationContext类,从名称就可以看出它是一个基于注解的IoC容器。

下面来演示一个简单的例子:

首先定义一个Java简单对象(Plain Ordinary Java Object)简称POJO:

public class User{
	private Long id;
	private String userName;
	private String note;
	
	/**setter and getter **/
}

接着定义一个Java配置文件:

//@Configuration代表这是一个Java配置文件,Spring的容器会根据它来生成IoC容器去装配Bean
@Configuration 
public class AppConfig{
	/*
		@Bean代表将initUser方法返回的POJO装配到IoC容器中,而其属性name定义这个Bean的名称,如果没有指定		 名称,则将方法名initUser作为Bean的名称保存到Spring IoC容器中。
	/*
	@Bean(name = "user")
	public User initUser(){
		User user = new User();
		user.setId(1L);
		user.setUserName("user_name_1");
		user.setNote("note_1");
		return user;
	}
}

最后使用AnnotationConfigApplicationContext类来构建自己的IoC容器:

public class IoCTest{
	public static void main(String[] args){
		/*
			将Java配置文件AppConfig传递给AnnotationConfigApplicationContext类的构造方法,这样它就			  可以读取配置了,然后将配置里面的Bean装配到IoC容器中,接着就可以使用getBean方法获取对应的				POJO了。
		/*
		ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
		User user = ctx.geBean(User.class);
		System.out.println(user.getId);
	}
}

二、装配你的Bean

1. 通过扫描装配你的Bean

? 如果Bean使用注解@Bean一个个地注入到IoC容器中,那将是一件很麻烦的事。好在Spring还允许我们通过扫描装配Bean到IoC容器中。对于扫描装配而言需要用到两个注解@Component和@ComponentScan。其中@Component用来标明哪个类被扫描到IoC容器中,而@ComponentScan用来标明采用何种策略去扫描装配Bean。

这里我们通过对上面例子的修改来演示如何通过扫描的方式来装配Bean:

/*
	注解@Component表明这个类将被IoC容器扫描装配,其中配置的"user"作为Bean的名称,当然你也可以不配置这个	 字符串,那么IoC容器就会把类名的第一个字母小写,其他不变作为Bean的名称放入到IoC容器中。
*/
@Component("user")
public class User{
	// 注解@Value则是指定具体的值,使得IoC容器给对应的属性注入相应的值。
	@Value("1")
	private Long id;
	@Value("user_name_1")
	private String userName;
	@Value("note_1")
	private String note;
	
	/**setter and getter **/
}

为了让IoC容器装配这个类,需要改造AppConfig类:

@Configuration
// 这里加入了@ComponentScan注解,意味着它会进行扫描,但是它只会扫描AppConfig类所在的包及其子包中的Bean
@ComponentScan
public class AppConfig{
}

测试扫描:

ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
User user = ctx.geBean(User.class);
System.out.println(user.getId);

这样就能够运行了。然而之前为了使得User类能够被扫描,我们需要把它放到AppConfig类所在的包中,这样显然是不合理的。因此@ComponentScan注解还允许我们自定义扫描的包。

// 修改AppConfig中的注解,使它扫描com.springboot.chapter3包及其子包
@ComponentScan("com.springboot.chapter3.*")

过滤器:

// 这样可以使得com.springboot.chapter3包下的UserService类不被IoC容器扫描注入
@ComponentScan(basePackages = "com.springboot.chapter3.*",
	excludeFilters = {@Filter(classes = {UserService.class})})

2. 通过@Bean注解将第三方Bean放入到IoC容器中

? 现实中Java的应用往往需要引入许多来自第三方的包,并且很有可能希望把第三方包中的类也存放到IoC容器中,这时就又可以使用@Bean注解来完成了。

三、依赖注入(DI)

这里我们通过一个人类依赖于动物的例子来讲解依赖注入。

首先定义人类和动物接口:

public interface Person{
	// 使用动物服务
	public void service();
	
	// 设置动物
	public void setAnimal(Animal animal);
}

public interface Animal{
	public void use();
}

接着定义两个实现类:

@Component
public class BussinessPerson implements Person{
	/*
		@Autowired注解会根据属性的类型(这里是Animal类)找到对应的Bean进行注入。狗是动物的一种,所以IoC		   容器会把Dog的实例注入BussinessPerson实例中。这样当通过IoC容器获取BussinessPerson实例的时候就		   能够使用Dog实例来提供服务了。
	*/
	@Autowired
	private Animal animal = null;
	
	@Override
	public void service(){
		this.animal.use();
	}
	
	@Override
	public void setAnimal(Animal animal){
		this.animal = animal;
	}
}

@Component
public class Dog implements Animal{
	@Override
	public void use(){
		System.out.println("狗是用来看门的。");
	}
}

测试代码:

ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
Person person = ctx.getBean(BussinessPerson.class);
person.service();// 会输出:"狗是用来看门的。"

1. 注解@Autowired

? 此时我们来思考一个问题。假如我们再定义一个Cat类:

@Component
public class Cat implements Animal{
	@Override
	public void use(){
		System.out.println("猫是用来抓老鼠的。");
	}
}

? 好了,如果我们还是使用之前的BussinessPerson类,那么麻烦来了,因为这个类只是定义了一个动物属性(Animal),而我们却有两个动物,一个狗,一个猫,IoC容器该如何注入呢?如果进行测试,我们会发现IoC容器抛出异常。因为IoC容器并不能知道你想要注入什么动物(是狗?是猫?)给BussinessPerson类对象。

? 假设我们目前想要狗来提供服务,此时我们需要修改代码:

@Autowired
private Animal animal = null;

? 修改为

@Autowired
private Animal dog = null;

? 注解@Autowired首先会根据类型找到对应的Bean,如果对应类型的Bean不是唯一的,那么它会根据其属性名称和Bean的名称进行匹配。如果匹配得上,就会使用该Bean;如果还无法匹配,就会抛出异常。

? 注解@Autowired除了可以标注属性外,还可以标注方法,如setAnimal方法,如下所示:

@Override
@Autowired
public void setAnimal(Animal animal){
	this.animal = animal;
}

? 这样它会使用setAnimal方法从IoC容器中找到对应的动物进行注入。

? 我们还可以把注解@Autowired使用在方法的参数上。

2. 消除歧义性——@Primary和@Qualifier

? 在上面我们发现有猫有狗的时候,为了使@Autowired能够继续使用,我们将BussinessPerson类的属性名称从animal修改为dog。这种做法显然不是很合理。所以我们需要有更好的方式来解决歧义性问题。

? 首先是注解@Primary,它是一个用来修改优先权的注解。当有猫有狗的时候,假设这次需要使用猫,那么只需要在猫类的定义上加上@Primary就可以了,Cat的实例会被优先注入。但是当有多个类都被添加了@Primary注解时,IoC容器还是无法区分应该采用哪个Bean的实例进行注入,看样子我们还需要一种更加灵活的机制来实现注入。

? 注解@Qualifier可以实现这个愿望。它的配置项value需要一个字符串去定义,它将与@Autowired组合在一起,通过类型和名称一起找到Bean。我们知道Bean名称在IoC容器中是唯一的标识,通过这个就可以消除歧义性了。

? 下面我们假设猫已经标注了@Primary,而我们需要的是狗提供服务,因此需要修改BussinessPerson类的属性animal的标注来满足我们的需求,如下所示:

@Autowired
@Qualifier("dog")
private Animal animal = null;

? 一旦这样声明,IoC容器就会以类型和名称去寻找对应的Bean进行注入。那么根据类型Animal,名称dog,显然也只能找到狗为我们服务了。

3. 带有参数的构造方法类的装配

? 在之前,我们都基于一个默认的情况,那就是不带参数的构造方法下实现依赖注入。但事实上,有些类只有带参数的构造方法,于是上述的方法都不能再使用了。为了满足这个功能,我们可以使用@Autowired注解对构造方法的参数进行注入。例如,修改BussinessPerson类来满足这个功能:

@Component
public class BussinessPerson implements Person{

	private Animal animal = null;
	
	public BussinessPerson(@Autowired @Qualifier("dog") Animal animal){
		this.animal = animal;
	}
	
	@Override
	public void service(){
		this.animal.use();
	}
	
	@Override
	public void setAnimal(Animal animal){
		this.animal = animal;
	}
}

四、Bean的生命周期

Bean的生命周期大致可以分为Bean的定义、Bean的初始化、Bean的生存期和Bean的销毁这4个部分。

1. Bean的定义过程

  • 资源定位:Spring通过我们的配置,如@ComponentScan定义的扫描路径去找到带有@Component的类,这个过程就是一个资源定位的过程。
  • Bean定义:一旦找到了资源,那么它就开始解析,并且将定义的信息保存起来。注意,此时还没有初始化Bean,也就没有Bean的实例,它有的仅仅是Bean的定义。
  • 发布Bean定义:然后就会把Bean的定义发布到IoC容器中。此时IoC容器中也只有Bean的定义,还是没有Bean的实例生成。

完成了这3步只是一个资源定位并将Bean的定义发布到IoC容器的过程,还没有Bean实例的生成,更没有完成依赖注入。

2. Bean的初始化过程

  • 实例化:创建Bean的实例对象。
  • 依赖注入(DI):完成依赖注入,例如@Autowired注入的各类资源。

? 假如我们希望只是将Bean的定义发布到IoC容器而不做实例化和依赖注入,只有当我们取出Bean的时候才去完成实例化和依赖注入,即延迟初始化。我们需要用到注解@ComponentScan中的一个配置项lazyInit,我们需要将该配置项的值设置为true,这样就可以实现延迟初始化了。

五、使用属性文件

六、条件装配Bean

七、Bean的作用域

1. singleton

? 默认值,IoC容器只存在单例。

2. prototype

? 每当从IoC容器中取出一个Bean,则创建一个新的Bean。

? 如果想要让一个类的作用域为prototype,则需要在类上加上以下注解:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

八、使用@Profile

? Spring提供的Profile机制使我们可以很方便地实现各个环境(发开环境、测试环境、准生产环境、生产环境)之间的切换。

九、引入XML配置Bean

十、使用Spring EL

相关推荐