全注解下的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的体系中BeanFactory和ApplicationContext是最为重要的接口设计,在现实中我们使用的大部分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机制使我们可以很方便地实现各个环境(发开环境、测试环境、准生产环境、生产环境)之间的切换。