4、Spring 中的依赖注入与单元测试全解析

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];
  1. 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>
  1. 启动应用上下文 :使用 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)
  1. 指定上下文配置文件的位置:
@ContextConfiguration(locations = {"/BusinessApplicationContext.xml" })
  1. 自动装配 BusinessService bean:
@Autowired
private BusinessService service;
  1. 编写测试方法:
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;
  1. 使用 @InjectMocks 注解将模拟对象注入到被测试的类 BusinessServiceImpl 中:
@InjectMocks
private BusinessService service = new BusinessServiceImpl();
  1. 在测试方法中,使用 Mockito 的 BDD 风格方法来模拟 retrieveData 方法:
BDDMockito.given(dataService.retrieveData(Matchers.any(User.class)))
        .willReturn(Arrays.asList(new Data(10), new Data(15), new Data(25)));
  1. 使用 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 依赖注入有所帮助,如果你在实际应用中遇到任何问题,欢迎留言讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值