一、依赖注入的基本概念
依赖注入(Dependency Injection,简称 DI)是一种设计模式,它让对象之间的依赖关系由外部容器来管理,而不是由对象自己创建或管理依赖。这种模式可以降低代码的耦合度,提高可测试性和可维护性。
二、为什么需要依赖注入
假设我们有一个UserService
类,它需要使用UserRepository
类来访问数据库:
public class UserService {
private UserRepository userRepository;
public UserService() {
// 在类内部创建依赖对象,导致紧耦合
this.userRepository = new UserRepository();
}
public void createUser(String username) {
userRepository.save(username);
}
}
这种实现方式存在以下问题:
UserService
和UserRepository
紧耦合,难以替换UserRepository
的实现。- 难以进行单元测试,因为无法注入模拟的
UserRepository
。
三、依赖注入的三种实现方式
1. 构造函数注入
依赖通过构造函数传递:
public class UserService {
private final UserRepository userRepository;
// 通过构造函数注入依赖
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void createUser(String username) {
userRepository.save(username);
}
}
使用示例:
// 创建依赖对象
UserRepository repository = new UserRepository();
// 通过构造函数注入依赖
UserService service = new UserService(repository);
service.createUser("test");
2. Setter 方法注入
依赖通过 Setter 方法传递:
public class UserService {
private UserRepository userRepository;
// 通过Setter方法注入依赖
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void createUser(String username) {
userRepository.save(username);
}
}
使用示例:
UserService service = new UserService();
// 创建依赖对象
UserRepository repository = new UserRepository();
// 通过Setter方法注入依赖
service.setUserRepository(repository);
service.createUser("test");
3. 接口注入
依赖通过实现特定接口来注入:
// 定义注入接口
public interface UserRepositoryAware {
void setUserRepository(UserRepository userRepository);
}
public class UserService implements UserRepositoryAware {
private UserRepository userRepository;
@Override
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void createUser(String username) {
userRepository.save(username);
}
}
四、依赖注入的优势
- 降低耦合度:对象不负责创建自己的依赖,依赖关系由外部管理。
- 提高可测试性:可以轻松注入模拟对象进行单元测试。
- 遵循单一职责原则:对象只需关注自己的业务逻辑,而不是依赖的创建和管理。
五、依赖注入容器
手动管理依赖关系在大型项目中会变得非常繁琐,因此出现了依赖注入容器,如 Spring 和 Google Guice。
Spring 框架示例
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
// 使用Spring注解标记为服务组件
@Service
public class UserService {
private final UserRepository userRepository;
// 使用@Autowired注解进行构造函数注入
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void createUser(String username) {
userRepository.save(username);
}
}
// 使用Spring注解标记为数据访问组件
@Repository
public class UserRepository {
public void save(String username) {
System.out.println("Saving user: " + username);
}
}
配置和使用:
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
// 配置类,启用组件扫描
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
public static void main(String[] args) {
// 创建Spring应用上下文
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 从容器中获取Bean
UserService service = context.getBean(UserService.class);
service.createUser("test");
}
}
六、进阶概念:依赖查找与依赖注入的对比
依赖查找(Dependency Lookup)是另一种管理依赖的方式,它让对象主动从容器中查找依赖:
public class UserService {
private UserRepository userRepository;
public UserService() {
// 主动从容器中查找依赖
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
this.userRepository = context.getBean(UserRepository.class);
}
public void createUser(String username) {
userRepository.save(username);
}
}
依赖查找的缺点:
- 代码与容器紧密耦合。
- 难以进行单元测试。
七、Java 中的依赖注入规范
Java EE 6 引入了 JSR 330(Dependency Injection for Java)规范,定义了标准的依赖注入注解,如@Inject
、@Named
和@Provider
:
import javax.inject.Inject;
import javax.inject.Named;
@Named("userService")
public class UserService {
private final UserRepository userRepository;
@Inject
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void createUser(String username) {
userRepository.save(username);
}
}
八、依赖注入的高级应用场景
1. 作用域管理
Spring 框架支持多种 Bean 作用域,如单例(Singleton)、原型(Prototype)、请求(Request)和会话(Session):
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
@Service
@Scope("prototype") // 每次请求都创建新实例
public class UserService {
// ...
}
2. 条件注入
根据条件决定注入哪个实现:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
@Conditional(DevEnvironmentCondition.class)
public UserRepository devUserRepository() {
return new DevUserRepository();
}
@Bean
@Conditional(ProdEnvironmentCondition.class)
public UserRepository prodUserRepository() {
return new ProdUserRepository();
}
}
3. 限定符(Qualifiers)
当有多个相同类型的 Bean 时,使用限定符来区分:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(@Qualifier("mysqlRepository") UserRepository userRepository) {
this.userRepository = userRepository;
}
}
九、依赖注入的最佳实践
- 优先使用构造函数注入:确保对象创建后即处于可用状态,避免 NullPointerException。
- 使用接口或抽象类定义依赖:提高代码的灵活性和可替换性。
- 避免循环依赖:循环依赖会导致依赖注入容器无法正确解析依赖关系。
- 合理使用作用域:根据对象的生命周期选择合适的作用域。