2.深入剖析:Spring 中如何巧妙玩转依赖注入的多种方式

一、构造函数注入的深入剖析

1. 严格的依赖初始化

构造函数注入使得依赖关系在对象创建时就被严格初始化。这符合面向对象编程中对象的不变性原则,一旦对象创建完成,其依赖关系就不会再改变。这对于一些需要保证数据一致性和安全性的场景非常重要。例如,在金融系统中,账户服务类AccountService依赖于账户存储库AccountRepository,使用构造函数注入可以确保在AccountService实例创建时就拥有有效的AccountRepository实例,避免在后续操作中出现空指针异常。

java

public class AccountService {
    private final AccountRepository accountRepository;

    public AccountService(AccountRepository accountRepository) {
        if (accountRepository == null) {
            throw new IllegalArgumentException("AccountRepository cannot be null");
        }
        this.accountRepository = accountRepository;
    }

    public void transferMoney(Account from, Account to, double amount) {
        // 业务逻辑
        accountRepository.updateBalance(from, from.getBalance() - amount);
        accountRepository.updateBalance(to, to.getBalance() + amount);
    }
}

2. 构造函数重载与多参数注入

当一个类有多个依赖或者有不同的初始化方式时,可以使用构造函数重载。例如,一个日志服务类LoggingService可能依赖于日志存储库LogRepository和日志配置LogConfig,同时也可以提供一个只依赖LogRepository的构造函数,以支持不同的使用场景。

java

public class LoggingService {
    private final LogRepository logRepository;
    private final LogConfig logConfig;

    public LoggingService(LogRepository logRepository) {
        this(logRepository, new LogConfig());
    }

    public LoggingService(LogRepository logRepository, LogConfig logConfig) {
        this.logRepository = logRepository;
        this.logConfig = logConfig;
    }

    public void logMessage(String message) {
        // 根据配置记录日志
        if (logConfig.isEnabled()) {
            logRepository.saveLog(message);
        }
    }
}

3. 构造函数注入与单元测试

构造函数注入使得单元测试变得更加简单和直观。在编写单元测试时,可以很方便地通过构造函数传入模拟对象(Mock Object)。例如,使用 Mockito 框架为UserService编写单元测试:

java

import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;

public class UserServiceTest {
    @Test
    public void testSaveUser() {
        UserRepository mockUserRepository = mock(UserRepository.class);
        UserService userService = new UserService(mockUserRepository);
        User user = new User();

        userService.saveUser(user);

        verify(mockUserRepository, times(1)).save(user);
    }
}

二、Setter 方法注入的深入分析

1. 动态配置与依赖更新

Setter 方法注入允许在对象创建后动态地更新依赖关系。这在一些需要根据不同环境或运行时条件进行配置的场景中非常有用。例如,一个缓存服务类CacheService可能依赖于不同的缓存实现(如内存缓存、Redis 缓存等),可以通过 Setter 方法在运行时动态切换缓存实现。

java

public class CacheService {
    private Cache cache;

    public void setCache(Cache cache) {
        this.cache = cache;
    }

    public void put(String key, Object value) {
        cache.put(key, value);
    }

    public Object get(String key) {
        return cache.get(key);
    }
}

2. 可选依赖与默认值

Setter 方法注入可以方便地处理可选依赖。如果某个依赖不是必需的,可以在类中为其提供默认值。例如,一个消息通知服务类NotificationService可能依赖于邮件发送器EmailSender,但如果没有配置邮件发送器,也可以使用默认的消息通知方式。

java

public class NotificationService {
    private EmailSender emailSender;

    public void setEmailSender(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    public void sendNotification(String message) {
        if (emailSender != null) {
            emailSender.sendEmail(message);
        } else {
            // 使用默认通知方式
            System.out.println("Sending default notification: " + message);
        }
    }
}

3. 链式调用与流畅接口

Setter 方法可以实现链式调用,从而创建一个流畅的接口。例如,一个配置类AppConfig可以通过链式调用的方式设置多个配置项。

java

public class AppConfig {
    private String databaseUrl;
    private String username;
    private String password;

    public AppConfig setDatabaseUrl(String databaseUrl) {
        this.databaseUrl = databaseUrl;
        return this;
    }

    public AppConfig setUsername(String username) {
        this.username = username;
        return this;
    }

    public AppConfig setPassword(String password) {
        this.password = password;
        return this;
    }

    // 其他方法
}

使用时可以这样调用:

java

AppConfig config = new AppConfig()
   .setDatabaseUrl("jdbc:mysql://localhost:3306/mydb")
   .setUsername("root")
   .setPassword("password");

三、基于注解的自动装配的高级应用

1. @Autowired@Primary注解

当存在多个相同类型的 bean 时,除了使用@Qualifier注解指定具体的 bean,还可以使用@Primary注解指定一个默认的 bean。例如,有两个数据源DataSource的实现MySqlDataSourceOracleDataSource,可以将MySqlDataSource标记为@Primary

java

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class MySqlDataSource implements DataSource {
    // 实现方法
}

@Component
public class OracleDataSource implements DataSource {
    // 实现方法
}

然后在需要注入DataSource的地方,直接使用@Autowired即可:

java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DatabaseService {
    @Autowired
    private DataSource dataSource;

    // 业务方法
}

2. @Resource@Inject对比

@Resource是 Java EE 规范中的注解,@Inject是 JSR - 330 规范中的注解。它们都可以用于依赖注入,功能上有一些相似之处,但也有一些细微的差别。@Resource默认按名称匹配,而@Inject按类型匹配。例如:

java

import javax.annotation.Resource;
import javax.inject.Inject;
import org.springframework.stereotype.Service;

@Service
public class ProductService {
    @Resource(name = "productRepository")
    private ProductRepository productRepository1;

    @Inject
    private ProductRepository productRepository2;

    // 业务方法
}

3. 条件注入与@Conditional注解

Spring 提供了@Conditional注解,可以根据条件来决定是否进行依赖注入。例如,根据不同的操作系统来注入不同的文件服务实现。

java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    @Conditional(WindowsCondition.class)
    public FileService windowsFileService() {
        return new WindowsFileService();
    }

    @Bean
    @Conditional(LinuxCondition.class)
    public FileService linuxFileService() {
        return new LinuxFileService();
    }
}

其中WindowsConditionLinuxCondition是自定义的条件类,实现Condition接口:

java

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class WindowsCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return System.getProperty("os.name").toLowerCase().contains("windows");
    }
}

public class LinuxCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return System.getProperty("os.name").toLowerCase().contains("linux");
    }
}

四、不同注入方式的综合运用

在实际的 Spring 项目中,通常会综合运用多种依赖注入方式。例如,对于核心业务类,使用构造函数注入保证依赖的稳定性;对于一些可配置的组件,使用 Setter 方法注入实现动态配置;对于一些辅助类或工具类,使用基于注解的自动装配简化配置。

java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BusinessService {
    private final CoreRepository coreRepository;
    private AuxiliaryService auxiliaryService;

    @Autowired
    public BusinessService(CoreRepository coreRepository) {
        this.coreRepository = coreRepository;
    }

    @Autowired
    public void setAuxiliaryService(AuxiliaryService auxiliaryService) {
        this.auxiliaryService = auxiliaryService;
    }

    public void performBusinessLogic() {
        // 使用核心仓库
        coreRepository.saveData();
        // 使用辅助服务
        auxiliaryService.doSomething();
    }
}

通过综合运用不同的依赖注入方式,可以充分发挥它们各自的优势,构建出更加灵活、可维护和可测试的 Spring 应用程序。

欢迎点赞关注哦,本专栏深入剖析 30 个关键面试点,通过趣味解读、源码分析、实战技巧分享,
将晦涩的知识变得通俗易懂。无论是 Spring 事务管理的复杂机制,还是 Spring MVC 请求处理的高效流程,在这里都能找到清晰且深入的讲解。无论你是正在为面试发愁,渴望系统梳理 Spring 知识;还是想在工作中更好地运用 Spring 提升开发效率,本专栏都将是你的不二之选。跟随我们,踏上 Spring 面试通关之旅,让 Spring 知识成为你职场进阶的有力武器吧。【每周不定期更新喔】

你可能感兴趣的:(spring,java)