源本科技 | 码上会

Spring Boot 控制反转

2026/03/24
25
0

学习目标

  • 深入理解控制反转(IoC)与依赖注入(DI)的核心概念及其在 Spring 生态中的演变

  • 掌握使用 @Component@Service@Repository 等注解

  • 学会利用构造器注入(Constructor Injection)解决循环依赖并提升代码可测试性

  • 理解 Spring Boot 自动配置机制如何简化 Bean 的管理与加载

  • 能够独立构建一个分层清晰、低耦合的 Spring Boot 业务模块


控制反转与依赖注入

在传统的应用程序开发中,对象通常负责创建和管理其依赖项。这种紧耦合的方式导致代码难以测试、维护和扩展。控制反转(Inversion of Control, IoC)是一种设计原则,它将对象的创建和控制权从程序代码本身转移到了外部容器(在 Spring 中即 Spring 容器)。

依赖注入(Dependency Injection, DI)是实现 IoC 的一种具体模式。通过 DI,对象不再主动查找或创建依赖,而是由容器在运行时将依赖项“注入”到对象中。

在 Spring Boot 出现之前,开发者往往需要编写大量的 XML 文件来定义 Bean 及其依赖关系。随着注解技术的发展,尤其是 Spring Boot 的普及,纯注解配置已成为主流。这种方式不仅减少了样板代码,还利用了 Java 编译时的类型安全检查,极大地提升了开发效率。

核心优势

  • 解耦:业务逻辑与对象创建逻辑分离。

  • 可测试性:可以轻松地将真实实现替换为模拟对象(Mock)进行单元测试。

  • 灵活性:无需修改源代码,仅通过配置即可切换不同的实现类。


Spring Boot 中的 Bean 管理

Spring Boot 主要依靠类路径扫描和注解来识别和管理 Bean。以下是核心的注解体系:

组件扫描与注册

Spring Boot 启动类通常位于根包下,默认会扫描该类所在包及其子包下的所有组件。

  • @Component:通用的组件注解,表示该类是一个 Spring 管理的 Bean。

  • @Service:专用于服务层(Business Logic),语义更明确。

  • @Repository:专用于数据访问层(DAO),除了标记 Bean 外,还能将数据库异常转换为 Spring 的统一数据访问异常。

  • @Controller / @RestController:专用于控制层,处理 HTTP 请求。

依赖注入方式

虽然 Spring 支持字段注入(@Autowired 直接写在字段上)、Setter 注入和构造器注入,但在现代 Spring Boot 最佳实践中,强烈推荐只使用构造器注入

为什么推荐构造器注入?

  1. 不可变性:依赖项可以被声明为 final,确保对象一旦创建,其依赖就不会被改变。

  2. 强制依赖:如果缺少必要的依赖,应用在启动时就会报错,而不是等到运行时调用方法时才抛出空指针异常。

  3. 易于测试:在单元测试中,可以直接通过 new 关键字实例化类并传入 Mock 对象,无需启动 Spring 容器。

  4. 避免循环依赖的隐蔽性:构造器注入能更早地暴露循环依赖问题。

注意:自 Spring Framework 4.3 起,如果一个类只有一个构造函数,则可以省略 @Autowired 注解,Spring 会自动使用该构造函数进行注入。


实战演练

构建用户管理系统

本节将通过一个完整的用户管理模块,演示如何使用纯注解方式实现分层架构。我们将定义数据访问层、服务层和控制层,并通过构造器注入将它们连接起来。

定义数据访问层

首先,我们创建一个接口来定义用户数据的操作规范。在实际项目中,通常会继承 JpaRepository,但为了演示原理,这里手动定义。

package com.example.demo.repository;

import com.example.demo.model.User;
import java.util.Optional;

// 使用 @Repository 标记数据访问层组件
@Repository
public interface UserRepository {

    Optional<User> findById(Long id);

    User save(User user);
}

接下来,提供一个简单的内存实现类(模拟数据库操作):

package com.example.demo.repository.impl;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

// 具体的实现类,同样需要被扫描为 Bean
// 为了区分接口和实现,我们可以给实现类指定特定的 Bean 名称
@Repository("memoryUserRepository")
public class MemoryUserRepository implements UserRepository {

    private final Map<Long, User> database = new HashMap<>();

    @Override
    public Optional<User> findById(Long id) {
        return Optional.ofNullable(database.get(id));
    }

    @Override
    public User save(User user) {
        if (user.getId() == null) {
            // 简单模拟自增 ID
            long nextId = database.size() + 1;
            user.setId(nextId);
        }
        database.put(user.getId(), user);
        return user;
    }
}

定义服务层

服务层包含核心业务逻辑。它依赖于 UserRepository 来获取数据。我们将使用构造器注入

package com.example.demo.service;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
public class UserService {

    // 声明为 final 以确保不可变性
    private final UserRepository userRepository;

    // 构造器注入:Spring 会自动寻找匹配的 UserRepository Bean
    // 如果有多个实现类,需要配合 @Qualifier 使用
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional(readOnly = true)
    public Optional<User> getUserById(Long id) {
        // 可以在这里添加业务逻辑,如权限校验、日志记录等
        System.out.println("正在执行获取用户的业务逻辑...");
        return userRepository.findById(id);
    }

    @Transactional
    public User registerUser(String name, String email) {
        User newUser = new User(name, email);
        // 业务规则:邮箱必须包含 @
        if (!email.contains("@")) {
            throw new IllegalArgumentException("无效的邮箱地址");
        }
        return userRepository.save(newUser);
    }
}

定义控制层

控制层负责接收 HTTP 请求,调用服务层,并返回响应。

package com.example.demo.controller;

import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    // 构造器注入
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        Optional<User> user = userService.getUserById(id);
        return user.map(ResponseEntity::ok)
                   .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody UserCreateRequest request) {
        User savedUser = userService.registerUser(request.getName(), request.getEmail());
        return ResponseEntity.ok(savedUser);
    }
    
    // 内部静态类用于接收请求参数,保持代码整洁
    public static class UserCreateRequest {
        private String name;
        private String email;
        
        // Getters and Setters
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getEmail() { return email; }
        public void setEmail(String email) { this.email = email; }
    }
}

架构流程图

以下图表展示了请求如何在纯注解配置的 Spring Boot 应用中流转,以及 IoC 容器如何管理依赖关系:


高级配置

处理多实现与条件装配

在大型项目中,一个接口可能有多个实现类(例如:UserRepository 可能有 MemoryUserRepositoryDatabaseUserRepository)。此时,Spring 容器在注入时会因为找不到唯一的 Bean 而抛出 NoUniqueBeanDefinitionException

1. @Qualifier

指定 Bean 名称

在注入点明确指定需要使用哪个 Bean。

@Service
public class UserService {
    private final UserRepository userRepository;

    // 明确指定使用名为 "memoryUserRepository" 的 Bean
    public UserService(@Qualifier("memoryUserRepository") UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

2. @Primary

标记首选 Bean

如果在大多数情况下都使用某一个实现,可以在该实现类上添加 @Primary 注解。

@Repository
@Primary // 标记为首选,当有多个实现且未指定 @Qualifier 时,优先注入此实现
public class DatabaseUserRepository implements UserRepository {
    // ... 实现细节
}

3. @Conditional

条件装配

Spring Boot 提供了强大的条件注解,可以根据环境配置动态决定加载哪个 Bean。

@Configuration
public class RepositoryConfig {

    @Bean
    @ConditionalOnProperty(name = "app.storage.type", havingValue = "memory")
    public UserRepository memoryUserRepository() {
        return new MemoryUserRepository();
    }

    @Bean
    @ConditionalOnProperty(name = "app.storage.type", havingValue = "database", matchIfMissing = true)
    public UserRepository databaseUserRepository() {
        return new JpaUserRepository(); // 假设这是基于 JPA 的实现
    }
}