前端视角 Java Web 入门手册 5.5:真实世界 Web 开发——控制反转与 @Autowired

高层模块管理底层模块

在传统的编程模式中,高层模块需要直接创建和管理低层模块的实例,我们称之为高层模块控制底层模块的生命周期,这种控制关系导致模块之间高度耦合

java 复制代码
public class UserService {
    private UserRepository userRepository = new UserRepository(); // 高层直接控制低层

    public void createUser(User user) {
        userRepository.save(user);
    }
}

UserService 直接创建并依赖于 UserRepository 的具体实现,这样 UserRepository 无法直接替换为不同的实现,比如从本地存储切换到远程存储,需要同时修改 UserService 的代码。

同时这样的写法也带来测试的复杂性,为 UserService 编写单元测试,但由于它直接创建了 UserRepository 的实例,无法轻松地替换为一个 Mock 对象

java 复制代码
public class UserServiceTest {
    @Test
    public void testCreateUser() {
        UserService userService = new UserService();
        User user = new User("John Doe", "john@example.com");
        userService.createUser(user);
        // 无法验证或模拟 UserRepository 的行为
    }
}

控制反转与依赖注入

控制反转是指将对象的创建和依赖管理交由外部容器(如 Spring IoC 容器)来实现,从而降低模块之间的耦合度

  • 所有组件的实例化和生命周期管理由 IoC 容器处理
  • 上层模块不再自行创建下层模块的实例,而是通过构造器、Setter 方法或字段注入的方式获得依赖。
java 复制代码
// 下层模块接口
public interface UserRepository {
    void save(User user);
}

// 下层模块实现
@Repository
public class UserRepositoryImpl implements UserRepository {
    @Override
    public void save(User user) {
        // 保存用户到数据库
    }
}

// 上层模块
@Service
public class UserService {
    private final UserRepository userRepository;
    
    @Autowired // Spring 自动注入依赖
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public void createUser(User user) {
        userRepository.save(user);
    }
}

在上述示例中:

  • UserRepository 接口定义了保存用户的方法,而 UserRepositoryImpl 提供了具体实现,UserService 依赖 UserRepository 接口,而不依赖其实现。
  • UserService 通过构造器注入的方式获得 UserRepository 的实例,由 Spring IoC 容器负责注入具体实现 UserRepositoryImpl。

控制反转(IoC) 是设计原则,依赖注入(DI) 是实现控制反转的具体方式。

这样 UserRepositoryImpl 的修改不影响 UserService 的代码,并且在单元测试中,可以使用 Mockito 替换 UserRepository 为一个模拟对象,实现隔离测试

java 复制代码
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    public void testCreateUser() {
        User user = new User("John Doe", "john@example.com");
        
        userService.createUser(user);
        
        verify(userRepository, times(1)).save(user);
    }
}

@Autowired 做了什么

@Autowired 是 Spring Framework 提供的一个核心注解,主要用于实现依赖注入。@Autowired 注解用于标注在类的构造器、Setter 方法或字段上,表示 Spring 容器需要为这些位置提供相应的依赖对象。Spring 容器在启动时会扫描应用上下文中的所有 Bean,自动完成依赖的注入。

构造器注入

将依赖通过构造器参数传递,这是推荐的注入方式,因为它确保依赖在对象创建时就被提供,符合不可变性原则。

java 复制代码
@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired // 从 Spring 4.3 开始如果只有一个构造器,可以省略 @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 业务逻辑方法
}

Setter 注入

通过 Setter 方法注入依赖对象,适用于可选依赖或需要在运行时修改依赖的场景。

java 复制代码
@Service
public class UserService {

    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 业务逻辑方法
}

字段注入

直接在字段上注入依赖,是最简洁的注入方式,但不推荐用于生产代码,因为它不利于测试和违反了依赖注入的设计原则。

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 业务逻辑方法
}

注入优先级

当存在多个符合条件的 Bean 时,Spring 无法确定注入哪个 Bean,这时需要结合使用 @Qualifier@Primary 来明确指定。

可以在注入时候使用 @Qualifier 指定要注入的 Bean 的名称

java 复制代码
@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired 
    public UserService(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 业务逻辑方法
}

也可以使用@Primary 注解用于标记某个 Bean 为默认 Bean,当存在多个 Bean 时,优先选择被标记的 Bean。

java 复制代码
@Repository
@Primary
public class UserRepositoryImpl implements UserRepository {
    // 实现细节
}

@Repository
public class AlternativeUserRepository implements UserRepository {
    // 其他实现
}
相关推荐
狂炫冰美式7 分钟前
人均配了AI, 为什么公司还是没变快? 🤔 本质还是分布式系统问题
前端·后端·架构
她的男孩2 小时前
Spring Boot 接 Flowable 工作流:用 3 个注解搭一个请假审批流程
java·后端·架构
爱读源码的大都督2 小时前
Claude Code源码分析(三):为什么系统提示词中需要有tools呢?
前端·人工智能·后端
爱勇宝2 小时前
Claude Code 被曝暗藏“隐形检测”代码:封代理不是最可怕的,可怕的是你根本不知道它在干什么
前端·后端·程序员
ITOM运维行者3 小时前
从零搭建企业级服务器监控体系:踩坑实录与架构设计
前端·后端
用户4099322502123 小时前
Vue状态管理入门第四章:组合式store和SSR风险
前端·vue.js·后端
用户34232323763173 小时前
SPI 通信与高速外设驱动详解
后端
魏祖潇3 小时前
SDD 完整指南——Spec 端打底、Story 端交付、留白区
人工智能·后端
feelmylife594 小时前
消息队列可靠投递与幂等消费 -- 从"消息丢了"到"消息别重复"的完整工程实践
后端
雪隐4 小时前
个人电脑玩AI-10让5060 Ti给你打工——部署 Odysseus:终于有个能打的"AI管家"了
人工智能·后端