Spring Boot2.x教程:(十)从Field injection is not recommended谈谈依赖注入

从Field injection is not recommended谈谈依赖注入

大家好,我是欧阳方超,可以扫描下方二维码关注我的公众号"欧阳方超",后续内容将在公众号首发。

1、问题引入

在使用Spring框架时,我们经常会看到IDE给出这样的警告:

bash 复制代码
Field injection is not recommended

这通常出现在我们使用@Autowired注解直接注入字段时:

bash 复制代码
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;  // Field injection
}

2、依赖注入的三种方式

在Spring中,依赖注入(DI)主要有三种方式,我们先来了解一下。

2.1、字段注入(Field Injection)

字段注入是一种依赖注入的方式,通过直接将依赖项(如服务或组件)注入到类的字段中。通常,这种方式通过使用注解(如 @Autowired)来实现。例如:

bash 复制代码
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}

2.2、构造器注入(Constructor Injection)

通过构造器传递依赖项是最推荐的方式。这种方式可以确保所有必需的依赖项在对象创建时就被提供,并且可以轻松进行单元测试。

bash 复制代码
@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired  // 在Spring 4.3+,当类只有一个构造器时,@Autowired可以省略
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

2.3、setter注入(Setter Injection)

Setter 注入是一种依赖注入的方式,通过公开的 setter 方法将依赖项注入到对象中。这种方式允许在对象创建后动态地设置依赖项,提供了比字段注入更好的灵活性和可测试性。Setter 注入通常与 Spring 框架一起使用,依赖项通过 @Autowired 注解标记的 setter 方法进行注入。

bash 复制代码
@Service
public class UserService {
    private UserRepository userRepository;

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

3、为什么不推荐字段注入

3.1、违反单一职责原则

字段注入(Field Injection)在依赖注入的实现中常常被认为违反了单一职责原则(Single Responsibility Principle, SRP)。单一职责原则是面向对象设计中的一个重要原则,旨在确保一个类应该只有一个原因去改变,即一个类应该仅承担一个责任。

使用字段注入时,类的职责往往会变得模糊。具体来说,类不仅需要处理其核心业务逻辑,还需要负责管理其依赖关系的生命周期和注入。这种责任的混淆使得类变得更加复杂,难以理解和维护。

bash 复制代码
public class UserService {
    @Autowired
    private UserRepository userRepository; // 依赖关系的管理

    public void createUser(User user) {
        // 业务逻辑
    }
}

在这个例子中,UserService 类不仅负责用户相关的业务逻辑,还需要处理 UserRepository 的依赖关系。这使得 UserService 的职责超出了其核心功能。

3.2、无法创建不可变对象

使用字段注入时:

bash 复制代码
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}

这种方式下,userRepository无法声明为final,意味着字段可能在运行时被修改、无法保证线程安全、对象状态可能发生变化。

3.3、可测试性差

在单元测试中,我们通常希望能够控制被测试对象的所有依赖项,以便验证其行为。使用字段注入时,无法通过构造函数或方法直接传递模拟对象,这会导致下面的问题。

无法直接提供模拟对象:

由于依赖项是通过字段自动注入的,无法在创建被测试对象时直接提供一个模拟(mock)对象。这意味着必须依赖 Spring 的上下文来创建对象,而这通常会引入不必要的复杂性。

需要 Spring 上下文:

由于依赖项是通过 Spring 容器管理的,需要启动 Spring 上下文才能进行测试。这会增加测试的开销,并可能导致测试运行速度变慢。

难以隔离测试:

字段注入使得类与 Spring 框架紧密耦合,这使得单元测试难以隔离被测类和其依赖项,从而降低了测试的独立性。

4、推荐使用构造器注入的原因

4.2、明确的依赖关系

bash 复制代码
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

通过构造器参数,清晰地表明类的依赖,使得代码更易于维护和理解。

4.2、不可变性保证

依赖可以声明为final,确保运行时不会被修改,保证了线程安全。

4.3、单元测试友好

bash 复制代码
@Test
public void testUserService() {
    // 方便进行mock测试
    UserRepository mockRepo = mock(UserRepository.class);
    EmailService mockEmail = mock(EmailService.class);
    
    UserService service = new UserService(mockRepo, mockEmail);
    // 进行测试...
}

4.4、容器无关性

类可以在Spring容器之外实例化,提高了代码的可复用性。

5、最佳实践示例

bash 复制代码
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final SecurityService securityService;

    public UserService(
            UserRepository userRepository,
            EmailService emailService,
            SecurityService securityService) {
        this.userRepository = Objects.requireNonNull(userRepository, "UserRepository must not be null");
        this.emailService = Objects.requireNonNull(emailService, "EmailService must not be null");
        this.securityService = Objects.requireNonNull(securityService, "SecurityService must not be null");
    }

    public User createUser(UserDTO userDTO) {
        // 使用注入的依赖
        User user = userRepository.save(userDTO.toUser());
        emailService.sendWelcomeEmail(user);
        securityService.grantDefaultPermissions(user);
        return user;
    }
}

6、Lombok的@RequiredArgsConstructor

如果依赖较多,构造器代码会比较冗长。可以使用Lombok的@RequiredArgsConstructor注解简化:

bash 复制代码
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final SecurityService securityService;
    
    // Lombok会自动生成包含所有final字段的构造器
}

7、总结

尽管字段注入在某些情况下看起来更简单,但它带来了许多潜在的问题,特别是在可测试性和可维护性方面。因此,我们建议使用构造函数或方法参数进行依赖注入,以提高代码质量和可读性。通过采取这些最佳实践,将能够编写出更加健壮、易于维护和高效的 Java 应用程序。

我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜欢我的文章,欢迎点赞、转发、评论加关注。我们下次见。

相关推荐
深圳蔓延科技9 分钟前
Kafka的高性能之路
后端·kafka
Barcke10 分钟前
深入浅出 Spring WebFlux:从核心原理到深度实战
后端
JuiceFS10 分钟前
从 MLPerf Storage v2.0 看 AI 训练中的存储性能与扩展能力
运维·后端
大鸡腿同学12 分钟前
Think with a farmer's mindset
后端
Moonbit33 分钟前
用MoonBit开发一个C编译器
后端·编程语言·编译器
Reboot1 小时前
达梦数据库GROUP BY报错解决方法
后端
稻草人22221 小时前
java Excel 导出 ,如何实现八倍效率优化,以及代码分层,方法封装
后端·架构
渣哥1 小时前
原来 Java 里线程安全集合有这么多种
java
间彧2 小时前
Spring Boot集成Spring Security完整指南
java
掘金者阿豪2 小时前
打通KingbaseES与MyBatis:一篇详尽的Java数据持久化实践指南
前端·后端