Spring之IoC实践

Spring框架的核心特性——IoC 控制反转,它的出现大大地降低了Java企业级应用开发的复杂度,本文这里对其进行简要介绍并对其用法实践作具体说明

abstract

IoC 控制反转

IoC 控制反转(Inversion of Control),是面向对象编程中的一种设计原则。假设A类中定义了一个类型为B类的属性b1,那么在A类的对象a2中,通常做法是,需要先显式new一个B类的对象b2,然后再将b2对象的引用赋给对象a2的b1属性中;而通过控制反转,对象b2、a2则是通过容器等程序在外部先new出来,然后再自动地将对象b2引用注入到对象a2的b1属性中。该设计原则,将原先对象的构造控制权由该应用开发人员转交到外部容器中,一方面可以大大降低代码的耦合度,另一方面更易于测试与维护

IoC通常有两种实现方式:DI 依赖注入 (Dependency Injection)、Dependency Lookup 依赖查找。两者的区别在于,前者是被动的接收依赖对象,通过类型、名称来将对象依赖注入到合适的属性中,而后者是主动获取相应类型的对象,其可通过相关配置文件等信息来获取,通常可以控制获取依赖对象的时机

创建对象

Component Scanning 组件扫描

在Spring框架中,可通过在类上添加@Component注解标识需要Spring容器new的对象,类似地还有@Service 、@Controller注解,其作用同@Component注解本质上一致,只是后两者一般用于服务层和控制层组件中;然后通过@ComponentScan注解使能组件扫描即可。Spring扫描到我们添加在A类上的@Component注解后,Spring框架即会自动地创建出A类的对象a

@ComponentScan注解默认扫描其所在包及其子包,可支持自定义扫描包路径,如下述代码所示,其中**为通配符,意为扫描com.aaron下所有包及其子包

1
@ComponentScan("com.aaron.**")

JavaConfig Java配置类

而对于一些第三方组件,我们一般无法直接通过在类上添加@Component注解来让Spring容器创建相应的对象。Spring框架自然也是考虑到了这一点,早期开发者可以通过xml文件配置声明我们需要让Spring框架来管理、创建的对象,现在开发中更常见的是通过JavaConfig配置类来自定义声明我们需要的对象

这里结合一个具体实例说明如何通过JavaConfig实现所需对象的声明创建,下面代码即是一个线程池对象的Java配置类,可以看到其与一般的Java类并无太大区别,其关键在于该类上的@Configuration注解,表明其是一个Java配置类,Spring将会根据该类的配置来创建相应的实例对象,该类中的taskExecutor方法用于Spring框架来创建TaskExecutor类的实例,@Bean注解则表明该方法返回的是一个对象,Spring在调用该方法后需要将返回的对象实例注册到Spring容器中来进行管理。默认情况下,对象实例的名称和相应的方法名称是一致的,当然也可以通过@Bean注解的name属性为对象实例自定义名称(e.g., @Bean(name=”executor”))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 配置线程池
*/
@Configuration
public class TaskExecutorConfig {
// Spring 将通过该方法创建一个TaskExecutor类的对象实例,名为taskExecutor
@Bean
public TaskExecutor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程数
executor.setMaxPoolSize(20); // 最大线程数
executor.setQueueCapacity(50); // 任务队列容量
return executor;
}
}

Autowiring 自动装配

@Autowired 自动注入注解

IoC中,通过外部容器创建对象实例是第一步,第二步则是需要解决对象实例之间的相互依赖。Spring框架是通过@Autowire注解来进行对象依赖的注入——即,自动装配。具体地,Spring框架支持下面3种方式完成对象注入:字段注入、构造器注入和set方法注入,具体用法如下所示。比较三种注入方式可以看到,如果属性过多,会使得构造器注入的形参列表非常长,不够简洁;而对于set方法注入来说,有些属性其实并不需要set方法,而如果只是为了注入实例依赖就提供了其set方法,容易在代码中由于误操作而修改了相关属性,造成一定的安全隐患

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
// 1. 字段注入
public class UserController {
@Autowired
private UserService userService;
...
}
// 2. 构造器注入
public class UserController {
private UserService userService;

@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
...
}
// 3. set方法注入
public class UserController {
private UserService userService;

@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
...
}

Note:

Spring在自动装配时,对于对象实例的创建方式并无特殊要求,即无论是通过组建扫描生成的实例还是通过JavaConfig Java配置类声明的实例,亦或是早期的xml文件声明的对象实例,只要是Spring容器创建、管理的对象实例都可用于对象依赖的注入

@Primary 首选注解

Spring在通过实例对象类型的注入对象实例依赖时,符合条件的对象如果只有一个,那么自然直接向属性注入该对象即可,这也是正常情况下的注入过程;但是如果Spring容器发现与属性类型一致的实例对象存在多个时,Spring此时即会无法完成对该属性的实例注入,原因自然也好理解,因为Spring并不知道你到底需要注入的是哪个实例。这个时候,可以在我们期望注入实例的类上添加@Primary注解,标明其是注入该类型对象时首选的实例对象

下面实例代码说明了该注解的用法,Cat、Dog类均实现了Animal接口且都使用了@Component注解创建了各自的实例,而在AnimalService类的Animal属性上的@Autowire注解表明Spring会注入一个Animal类型的实例到该属性中,但是此时Spring会发现符合Animal类型注入要求的实例对象不止一个——cat、dog实例对象。由于Spring无法选择,故无法正常完成注入。现在,我们在Dog类添加一个@Primary注解,来告诉Spring当发现多个符合类型要求的实例时选择Dog类的实例进行注入,现在实例对象dog即会被注入到AnimalService类的Animal属性中

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
28
29
@Service
public class AnimalService {
@Autowired
private Animal animal; // 通过设置首选实例,dog对象被注入到animal属性中

public void findAnimal() {
animal.getMsg();
return;
}
}
...
// 猫类
@Component
public class Cat implements Animal {
@Override
public String getMsg() {
return "This is a Cat";
}
}
...
// 狗类
@Primary
@Component
public class Dog implements Animal {
@Override
public String getMsg() {
return "This is a Dog";
}
}

@Qualifier 限定符注解

@Primary注解只是在Spring注入实例依赖时发现多个符合的实例时,告诉其可以选择哪个实例来进行注入。但是如果存在多个首选实例时(例如在上文代码的Cat类上也添加@Primary注解),Spring依然会无法选择合适的实例从而导致注入失败。而这个时候,可以通过@Qualifier限定符注解来满足我们更加多样化的注入需求

我们知道,Spring创建的实例名称默认是首字母小写的类名,即Cat、Dog类创建的实例名称分别为cat、dog。实例在被创建的同时会生成一个限定符,默认与实例名称一致,即生成一个名为cat的Cat类实例,其限定符也是cat。而@Qualifier注解则可以指定所注入实例的限定符,这样通过实例类型、实例限定符即可进一步缩小可注入实例的范围,下面代码即是一个通过指定注入实例的限定符来表明注入哪个实例的示例

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
28
29
30
31
32
33
34
@Service
public class AnimalService {
@Qualifier("cat")
@Autowired
private Animal animal1; // 通过指定注入实例的限定符,cat对象被注入到animal1属性中

@Qualifier("dog")
@Autowired
private Animal animal2; // 通过指定注入实例的限定符,dog对象被注入到animal2属性中

public void findAnimal() {
animal1.getMsg();
animal2.getMsg();
return;
}
}
...
// 猫类
@Component
public class Cat implements Animal {
@Override
public String getMsg() {
return "This is a Cat";
}
}
...
// 狗类
@Component
public class Dog implements Animal {
@Override
public String getMsg() {
return "This is a Dog";
}
}

前面说到,实例的限定符默认即为实例的名称,也正是基于这个特点,上述代码即实现了所谓的实例的按名注入。但是同时又会产生一个新的问题,即实例默认生成限定符是和类名强耦合的。一旦后面我们将Cat类的类名更改为BlackCat,也必须同时将上面代码中的@Qualifier(“cat”)同步修改为@Qualifier(“blackCat”)。这样不仅麻烦,而且还有可能因为遗漏修改,而造成注入失败(因为没有限定符为cat的Animal实例了),所以@Qualifier注解还可以直接添加在类上用于自定义所创建实例的限定符

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
28
29
30
31
32
33
34
35
@Service
public class AnimalService {
@Qualifier("Cat")
@Autowired
private Animal animal1; // 通过指定注入实例的限定符,blackCat对象被注入到animal1属性中

@Qualifier("dog")
@Autowired
private Animal animal2; // 通过指定注入实例的限定符,dog对象被注入到animal2属性中

public void findAnimal() {
animal1.getMsg();
animal2.getMsg();
return;
}
}
...
// 猫类
@Qualifier("Cat") // 指定该类创建的实例的限定符为Cat
@Component
public class BlackCat implements Animal {
@Override
public String getMsg() {
return "This is a Black Cat";
}
}
...
// 狗类
@Component
public class Dog implements Animal {
@Override
public String getMsg() {
return "This is a Dog";
}
}

Note:

如果实例是在JavaConfig配置类中被声明的,则@Qualifier注解与@Bean注解配合使用,同样可以自定义所创建实例的限定符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 配置线程池
*/
@Configuration
public class TaskExecutorConfig {
// Spring 将通过该方法创建一个TaskExecutor类的对象实例,名为taskExecutor
@Bean
@Qualifier("Executor") // 指定实例taskExecutor的限定符为Executor
public TaskExecutor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程数
executor.setMaxPoolSize(20); // 最大线程数
executor.setQueueCapacity(50); // 任务队列容量
return executor;
}
}

参考文献

  1. Spring实战 Craig Walls著、张卫滨译
0%