Spring 中的依赖注入与单元测试全解析
1. Spring 依赖注入基础
在 Spring 应用中,依赖注入是一项核心特性。假设有两个文件,分别是带有
@Service
注解的
BusinessServiceImpl
和带有
@Repository
注解的
DataServiceImpl
,并且
BusinessServiceImpl
中的
DataService
实例使用了
@Autowired
注解。当启动 Spring 上下文时,会发生以下操作:
1. 扫描
com.mastering.spring
包,找到
BusinessServiceImpl
和
DataServiceImpl
这两个 bean。
2. 由于
DataServiceImpl
没有任何依赖,所以会先创建该 bean。
3.
BusinessServiceImpl
依赖于
DataService
,而
DataServiceImpl
是
DataService
接口的实现,满足自动装配条件,因此会创建
BusinessServiceImpl
的 bean,并通过 setter 方法将
DataServiceImpl
的 bean 自动装配到其中。
2. 启动 Java 配置的应用上下文
可以使用 Java 配置来启动 Spring 应用上下文,示例代码如下:
public class LaunchJavaContext {
private static final User DUMMY_USER = new User("dummy");
public static Logger logger = Logger.getLogger(LaunchJavaContext.class);
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringContext.class);
BusinessService service = context.getBean(BusinessService.class);
logger.debug(service.calculateSum(DUMMY_USER));
}
}
上述代码中,通过
AnnotationConfigApplicationContext
创建应用上下文,然后使用
getBean
方法获取
BusinessService
的 bean,并调用其
calculateSum
方法。
3. 启动上下文后的日志分析
启动上下文后,日志会显示组件扫描和 bean 创建的过程:
1. 组件扫描:
Looking for matching resources in directory tree
[/target/classes/com/mastering/spring]
Identified candidate component class: file
[/in28Minutes/Workspaces/SpringTutorial/mastering-spring-example-1/target/classes/com/mastering/spring/business/BusinessServiceImpl.class]
Identified candidate component class: file
[/in28Minutes/Workspaces/SpringTutorial/mastering-spring-example-1/target/classes/com/mastering/spring/data/DataServiceImpl.class]
defining beans [******OTHERS*****,businessServiceImpl,dataServiceImpl];
-
bean 创建:
-
先尝试创建
businessServiceImpl,但它有自动装配的依赖。 -
接着创建
dataServiceImpl。 -
最后将
dataServiceImpl自动装配到businessServiceImpl中。
-
先尝试创建
4. XML 配置的应用上下文
Spring 也支持使用 XML 配置来启动应用上下文,具体步骤如下:
1.
定义 XML 配置
:在
src/main/resources
目录下创建
BusinessApplicationContext.xml
文件,内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans> <!-Namespace definitions removed-->
<context:component-scan base-package ="com.mastering.spring"/>
</beans>
-
启动应用上下文
:使用
ClassPathXmlApplicationContext来启动上下文,示例代码如下:
public class LaunchXmlContext {
private static final User DUMMY_USER = new User("dummy");
public static Logger logger = Logger.getLogger(LaunchJavaContext.class);
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("BusinessApplicationContext.xml");
BusinessService service = context.getBean(BusinessService.class);
logger.debug(service.calculateSum(DUMMY_USER));
}
}
5. 使用 Spring 上下文编写 JUnit 测试
可以在单元测试中使用 Spring 上下文,步骤如下:
1. 使用
SpringJUnit4ClassRunner
作为测试运行器:
@RunWith(SpringJUnit4ClassRunner.class)
- 指定上下文配置文件的位置:
@ContextConfiguration(locations = {"/BusinessApplicationContext.xml" })
-
自动装配
BusinessServicebean:
@Autowired
private BusinessService service;
- 编写测试方法:
long sum = service.calculateSum(DUMMY_USER);
assertEquals(30, sum);
完整的测试类如下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/BusinessApplicationContext.xml" })
public class BusinessServiceJavaContextTest {
private static final User DUMMY_USER = new User("dummy");
@Autowired
private BusinessService service;
@Test
public void testCalculateSum() {
long sum = service.calculateSum(DUMMY_USER);
assertEquals(30, sum);
}
}
6. 单元测试的问题与解决方案
上述 JUnit 测试并不是真正的单元测试,因为它使用了
DataServiceImpl
的真实实现,实际上测试了
BusinessServiceImpl
和
DataServiceImpl
的功能。为了进行真正的单元测试,可以采用以下两种方案:
1.
创建存根实现
:在
src\test\java
文件夹中创建数据服务的存根实现,并使用单独的测试上下文配置将存根实现自动装配,而不是使用真实的
DataServiceImpl
类。但这种方法需要创建额外的类和上下文,并且随着单元测试中数据变化的增多,存根的维护会变得更加困难。
2.
使用模拟对象
:使用 Mockito 框架创建
DataService
的模拟对象,并将其自动装配到
BusinessServiceImpl
中。
7. 使用模拟对象进行单元测试
Mocking 是创建模拟真实对象行为的对象。在单元测试中,可以使用 Mockito 框架来模拟
DataService
的行为。具体步骤如下:
1. 使用
@Mock
注解创建
DataService
的模拟对象:
@Mock
private DataService dataService;
-
使用
@InjectMocks注解将模拟对象注入到被测试的类BusinessServiceImpl中:
@InjectMocks
private BusinessService service = new BusinessServiceImpl();
-
在测试方法中,使用 Mockito 的 BDD 风格方法来模拟
retrieveData方法:
BDDMockito.given(dataService.retrieveData(Matchers.any(User.class)))
.willReturn(Arrays.asList(new Data(10), new Data(15), new Data(25)));
-
使用
MockitoJUnitRunner作为测试运行器:
@RunWith(MockitoJUnitRunner.class)
完整的测试类如下:
@RunWith(MockitoJUnitRunner.class)
public class BusinessServiceMockitoTest {
private static final User DUMMY_USER = new User("dummy");
@Mock
private DataService dataService;
@InjectMocks
private BusinessService service = new BusinessServiceImpl();
@Test
public void testCalculateSum() {
BDDMockito.given(dataService.retrieveData(Matchers.any(User.class)))
.willReturn(Arrays.asList(new Data(10), new Data(15), new Data(25)));
long sum = service.calculateSum(DUMMY_USER);
assertEquals(10 + 15 + 25, sum);
}
}
8. 容器管理的 bean
在 Spring 中,容器可以接管 bean 及其依赖的创建和管理,这些由容器管理的 bean 称为容器管理的 bean。将 bean 的创建和管理委托给容器有以下优点:
- 类不负责创建依赖,因此它们是松散耦合的,易于测试,有助于良好的设计并减少缺陷。
- 容器管理 bean 时,可以以更通用的方式引入一些围绕 bean 的钩子。通过面向切面编程(AOP),可以将诸如日志记录、缓存、事务管理和异常处理等横切关注点编织到这些 bean 中,从而使代码更易于维护。
9. 依赖注入的类型
常见的依赖注入类型有两种:
| 注入类型 | 描述 | 示例代码 |
| ---- | ---- | ---- |
| setter 注入 | 通过 setter 方法注入依赖 |
public class BusinessServiceImpl {
private DataService dataService;
@Autowired
public void setDataService(DataService dataService) {
this.dataService = dataService;
}
}
或
public class BusinessServiceImpl {
@Autowired
private DataService dataService;
}
|
| 构造函数注入 | 通过构造函数注入依赖 |
public class BusinessServiceImpl {
private DataService dataService;
@Autowired
public BusinessServiceImpl(DataService dataService) {
super();
this.dataService = dataService;
}
}
|
10. 构造函数注入与 setter 注入的比较
-
在基于 XML 的应用上下文中,通常对强制依赖使用构造函数注入,对非强制依赖使用 setter 注入。但在 Java 应用上下文中,当使用
@Autowired注解时,依赖默认是必需的,如果没有可用的候选者,自动装配将失败并抛出异常,因此选择不再那么明确。 - 使用 setter 注入会导致对象在创建过程中状态发生变化,对于喜欢不可变对象的开发者来说,构造函数注入可能是更好的选择。此外,setter 注入有时可能会隐藏类有很多依赖的事实,而构造函数注入会使依赖更加明显,因为构造函数的参数数量会增加。
11. Spring bean 的作用域
Spring bean 可以有多种作用域,默认作用域是单例(singleton)。可以使用
@Scope
注解为 Spring bean 指定作用域,示例如下:
@Service
@Scope("singleton")
public class BusinessServiceImpl implements BusinessService
不同的 bean 作用域及其用途如下表所示:
| 作用域 | 用途 |
| ---- | ---- |
| Singleton | 默认情况下,所有 bean 的作用域都是单例。每个 Spring IoC 容器实例只使用一个这样的 bean 实例。即使有多个对 bean 的引用,每个容器也只创建一次该 bean。单例实例会被缓存,并用于后续所有使用该 bean 的请求。需要注意的是,Spring 的单例作用域是每个 Spring 容器一个对象。如果在单个 JVM 中有多个 Spring 容器,则同一个 bean 可能会有多个实例。因此,Spring 的单例作用域与典型的单例定义略有不同。 |
| Prototype | 每次从 Spring 容器请求 bean 时都会创建一个新实例。如果 bean 包含状态,建议使用原型作用域。 |
| request | 仅在 Spring Web 上下文中可用。每个 HTTP 请求都会创建一个新的 bean 实例。请求处理完成后,该 bean 会被丢弃。适用于保存特定于单个请求的数据的 bean。 |
| session | 仅在 Spring Web 上下文中可用。每个 HTTP 会话都会创建一个新的 bean 实例。适用于特定于单个用户的数据,例如 Web 应用程序中的用户权限。 |
| application | 仅在 Spring Web 上下文中可用。每个 Web 应用程序有一个 bean 实例。适用于特定环境的应用程序配置等。 |
12. Java 配置与 XML 配置的比较
随着 Java 5 中注解的出现,基于 Java 的配置在 Spring 应用中得到了广泛应用。在选择 Java 配置还是 XML 配置时,可以考虑以下因素:
-
注解的优点
:注解可以使 bean 定义更简短和简单,并且更接近其适用的代码。
-
注解的缺点
:使用注解的类不再是简单的 POJO,因为它们使用了特定于框架的注解。此外,使用注解时的自动装配问题可能更难解决,因为装配不再集中且没有明确声明。
-
XML 配置的优点
:如果 Spring 上下文 XML 打包在应用程序包(WAR 或 EAR)之外,可能会有更灵活的装配优势,例如可以为集成测试提供不同的设置。
13.
@Autowired
注解深入解析
当在依赖项上使用
@Autowired
注解时,应用上下文会搜索匹配的依赖项。默认情况下,所有自动装配的依赖项都是必需的,可能的结果如下:
- 找到一个匹配项:这就是你要找的依赖项。
- 找到多个匹配项:自动装配失败。
- 没有找到匹配项:自动装配失败。
当有多个候选者时,可以通过以下两种方式解决:
-
使用
@Primary
注解
:当在 bean 上使用
@Primary
注解时,在自动装配特定依赖项时,如果有多个候选者,该 bean 将成为主要使用的 bean。例如:
interface SortingAlgorithm {
}
@Component
class MergeSort implements SortingAlgorithm {
// Class code here
}
@Component
@Primary
class QuickSort implements SortingAlgorithm {
// Class code here
}
在上述示例中,如果组件扫描同时找到
QuickSort
和
MergeSort
,由于
@Primary
注解,
QuickSort
将用于自动装配任何对
SortingAlgorithm
的依赖。
-
使用
@Qualifier注解 :@Qualifier注解可以为 Spring bean 提供引用,该引用可用于限定需要自动装配的依赖项。例如:
@Component
@Qualifier("mergesort")
class MergeSort implements SortingAlgorithm {
// Class code here
}
@Component
class QuickSort implements SortingAlgorithm {
// Class code here
}
@Component
class SomeService {
@Autowired
@Qualifier("mergesort")
SortingAlgorithm algorithm;
}
在上述示例中,由于
SomeService
类中使用了
@Qualifier("mergesort")
,
MergeSort
(也定义了
mergesort
限定符)将成为自动装配的候选依赖项。
Spring 中的依赖注入与单元测试全解析
14. 依赖注入流程总结
为了更清晰地理解 Spring 中的依赖注入过程,下面给出一个 mermaid 格式的流程图:
graph TD;
A[启动 Spring 上下文] --> B[组件扫描];
B --> C{找到候选 bean};
C -->|是| D{bean 是否有依赖};
C -->|否| E[结束];
D -->|是| F{依赖是否匹配};
D -->|否| G[创建 bean 实例];
F -->|是| H[创建依赖 bean 实例];
F -->|否| I[自动装配失败];
H --> J[将依赖 bean 装配到目标 bean];
G --> K[完成 bean 创建];
J --> K;
K --> L[继续处理其他 bean];
L --> C;
这个流程图展示了 Spring 上下文启动后,组件扫描、bean 创建和依赖装配的主要步骤。从启动上下文开始,首先进行组件扫描,找到候选的 bean。对于有依赖的 bean,会检查依赖是否匹配,如果匹配则创建依赖 bean 并进行装配,最终完成 bean 的创建。
15. 不同配置方式的使用场景建议
根据前面介绍的 Java 配置和 XML 配置的特点,以下是一些不同场景下的使用建议:
-
小型项目
:对于小型项目,Java 配置通常是更好的选择。因为 Java 配置使用注解,代码更简洁,开发效率更高。例如,一个简单的 Web 应用,使用 Java 配置可以快速搭建起 Spring 上下文,减少配置文件的编写。
-
大型项目
:在大型项目中,可能会有多个团队协作开发,不同的模块有不同的配置需求。此时,XML 配置可以提供更灵活的装配方式,方便进行统一管理和配置。例如,在企业级应用中,不同的业务模块可能需要不同的配置,使用 XML 配置可以将这些配置分离,便于维护。
-
测试环境
:在测试环境中,为了方便进行不同的配置和模拟,可以结合使用 Java 配置和 XML 配置。例如,使用 XML 配置来定义测试上下文,使用 Java 配置来进行一些特定的测试配置。
16. 依赖注入在实际开发中的应用案例
假设我们正在开发一个电商系统,其中有一个商品服务类
ProductService
,它依赖于商品数据访问类
ProductDao
。以下是使用依赖注入的实现示例:
// 商品数据访问接口
interface ProductDao {
List<Product> getAllProducts();
}
// 商品数据访问实现类
@Component
class ProductDaoImpl implements ProductDao {
@Override
public List<Product> getAllProducts() {
// 模拟从数据库获取商品列表
return Arrays.asList(new Product("iPhone", 999.99), new Product("iPad", 599.99));
}
}
// 商品服务类
@Service
class ProductService {
private ProductDao productDao;
@Autowired
public ProductService(ProductDao productDao) {
this.productDao = productDao;
}
public List<Product> getProducts() {
return productDao.getAllProducts();
}
}
// 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/applicationContext.xml"})
public class ProductServiceTest {
@Autowired
private ProductService productService;
@Test
public void testGetProducts() {
List<Product> products = productService.getProducts();
assertNotNull(products);
assertFalse(products.isEmpty());
}
}
在这个示例中,
ProductService
通过构造函数注入了
ProductDao
的依赖。在测试类中,使用 SpringJUnit4ClassRunner 启动 Spring 上下文,并自动装配
ProductService
进行测试。
17. 依赖注入的最佳实践
-
使用接口编程
:在依赖注入中,尽量使用接口来定义依赖。这样可以提高代码的可维护性和可测试性。例如,在上面的电商系统示例中,
ProductService依赖于ProductDao接口,而不是具体的实现类。这样在测试时可以轻松地使用模拟对象来替代真实的ProductDao实现。 - 明确依赖关系 :使用构造函数注入可以使依赖关系更加明确。在类的构造函数中声明所有必需的依赖,这样可以让其他开发者清楚地了解该类的依赖情况。
- 合理使用作用域 :根据业务需求合理选择 bean 的作用域。对于无状态的服务类,使用单例作用域可以提高性能;对于有状态的类,如与用户会话相关的类,使用会话作用域更合适。
18. 总结
通过本文的介绍,我们深入了解了 Spring 中的依赖注入机制,包括依赖注入的基础概念、不同的注入类型、bean 的作用域、配置方式以及单元测试等方面的内容。依赖注入是 Spring 框架的核心特性之一,它可以帮助我们实现代码的解耦,提高代码的可维护性和可测试性。在实际开发中,我们应该根据项目的特点和需求,选择合适的依赖注入方式和配置方式,并遵循最佳实践来编写高质量的代码。
希望本文对你理解 Spring 依赖注入有所帮助,如果你在实际应用中遇到任何问题,欢迎留言讨论。
1249

被折叠的 条评论
为什么被折叠?



