Spring DI的艺术:优雅解耦代码!!!

Spring依赖注入(DI)详解:原理、实现与最佳实践

引言

在现代软件开发中,松耦合设计是构建高质量系统的关键。Spring框架通过依赖注入(Dependency Injection, DI)实现了这一目标。DI不仅简化了代码的编写,还提高了系统的可测试性和可维护性。本文将深入探讨Spring DI的核心原理、实现方式以及最佳实践。


一、什么是依赖注入(DI)?

依赖注入是一种设计模式,其核心思想是将对象的依赖关系由外部注入,而不是在类内部自行创建或查找。通过这种方式,代码的耦合度得以降低,系统更加灵活和易于维护。

DI与IoC的关系

依赖注入是控制反转(IoC)的一种实现方式。IoC的核心思想是"让框架控制程序的执行流程",而DI则是实现IoC的具体手段之一。


二、Spring DI的实现方式

在Spring框架中,依赖注入主要通过以下三种方式实现:

1. 构造注入(Constructor Injection)

通过构造方法注入依赖。这种方式在类初始化时完成注入,适合处理不可变依赖。

kotlin 复制代码
@Controller
public class HelloController {
    private final Student student;
    //构造方法注入
    @Autowired
    public HelloController(Student student) {
        this.student = student;
    }
}

123456789

补充代码进行测试:

arduino 复制代码
@Data
public class Student {
    public String name;
    public int age;
}

12345

通过DeBug进行调试:可以看到注入成功了

优点

强制依赖满足 :构造方法在对象创建时必须满足所有依赖,确保了对象在初始化时是完整的。
不可变性 :通过final关键字,可以确保依赖在对象生命周期内不会改变。
提高测试性:在单元测试中,可以轻松地为构造方法注入不同的依赖实现。

缺点

配置复杂性:在需要注入多个依赖时,构造方法的参数列表可能变得冗长


2. 设值注入(Setter Injection)

通过setter方法注入依赖。这种方式适合处理可变依赖,但在Spring推荐使用构造注入。

typescript 复制代码
@Controller
public class HelloController {
    private Student student;
 //Setter 方法注入
    @Autowired
    public void setStudent(Student student){
        this.student=student;
    }
}

123456789

通过DeBug进行观察:可以看到注入成功了


优点

可变依赖:适用于在运行时动态改变依赖的情况。

延迟初始化:依赖可以在对象创建后注入,适用于可选依赖。

缺点

初始化问题:对象可能在依赖注入前被使用,导致空指针异常。

难以控制顺序:多个依赖的注入顺序难以控制,可能导致不一致的状态。


3.属性注入(Field Injection)

属性注入是直接在类的字段上使用@Autowired注解,将依赖注入到字段中.

csharp 复制代码
@Service
public class UserService {
//属性注入
    @Autowired
    private Student s3;
    public void print(){
        System.out.println("do Service");
        System.out.println(s3);
    }
}

12345678910

判断属性注入是否成功:

补充一下代码进行测试:

arduino 复制代码
@Data
public class Student {
    public String name;
    public int age;
}

12345
typescript 复制代码
@Component
public class StudentComponent {
    @Bean
    public Student s1(){
        Student student=new Student();
        student.setAge(18);
        student.setName("张三");
        return student;
    }
@Primary
   @Bean
    public Student s2(){
       Student student=new Student();
       student.setAge(20);
       student.setName("李四");
        return student;
    }
}


12345678910111213141516171819
arduino 复制代码
@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		//上下文管理器
		ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
		UserService userService =context.getBean(UserService.class);
		//使用对象
		userService.print();
		}
}


1234567891011

可以看到注入成功了。


优点

简单直观,代码简洁。

适用于不需要显式控制注入时机的场景。

缺点

破坏封装性:直接暴露了类的字段,违反了面向对象的封装原则。

延迟初始化问题:字段可能在类初始化时未被注入,导致空指针异常。

难以测试:由于字段直接依赖外部注入,单元测试时可能需要额外的配置。


三、Spring DI的核心注解

在Spring中,依赖注入主要通过以下注解实现:

1. @Autowired

@Autowired是最常用的注解,用于自动注入依赖。

csharp 复制代码
@Service
public class UserService {
//属性注入
    @Autowired
    private Student s3;
    public void print(){
        System.out.println("do Service");
        System.out.println(s3);
    }
}

12345678910

2. @Qualifier

当存在多个相同类型的Bean时,可以通过@Qualifier指定具体的Bean。

看代码:

typescript 复制代码
@Component
public class StudentComponent {
    @Bean(name = "s3")
    public Student s1(){
        Student student=new Student();
        student.setAge(18);
        student.setName("张三");
        return student;
    }
    //Spring默认情况下会根据类型来注入Bean,但如果存在多个相同类型的Bean,Spring会不知道该注入哪一个,从而导致注入失败,出现错误。
    // 这时候,就需要使用@Primary注解来指定一个默认的Bean
@Primary
   @Bean(name = "s4")
    public Student s2(){
       Student student=new Student();
       student.setAge(20);
       student.setName("李四");
        return student;
    }
}

1234567891011121314151617181920

上面由于存在Spring默认情况下会根据类型来注入Bean,但如果存在多个相同类型的Bean,Spring会不知道该注入哪一个,从而导致注入失败,出现错误。然后通过@Primary来指定一个默认的Bean。上面现在默认的是s4。

less 复制代码
@Service
public class UserService {
    @Autowired
    @Qualifier("s3")
    private Student s3;
    public void print(){
        System.out.println("do Service");
        System.out.println(s3);
    }
}

12345678910

然后上面这段代码:通过@Qualifier指定具体的Bean。

arduino 复制代码
@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		//上下文管理器
		ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
		UserService userService=context.getBean(UserService.class);
		//使用对象
		userService.print();
		}
}

12345678910

启动看结果:

可以看到Bean对象的内容从s4变成了s3。

如果把@Qualifier("s3)去掉那么再看运行结果:

现在可以看到Bean的内容还是s4,得以验证@Qualifier的作用。


3. @Resource

@Resource是JDK提供的注解,也可用于依赖注入。

less 复制代码
@Service
public class UserService {
    @Autowired
    @Resource(name="s3")
    private Student s3;
    public void print(){
        System.out.println("do Service");
        System.out.println(s3);
    }
}

12345678910

原理同上,就不再演示了。

4. @Value

@Value用于注入配置文件中的属性值。

kotlin 复制代码
@Service
public class UserService {
    @Value("${my.key}")
   private  String MyKey;
   public  void print(){
       System.out.println("配置文件属性值" + MyKey);
   }
}

12345678

这里通过DeBug进行调试观察:

可以看到注入成功了。


四、Spring DI的高级特性

1. 条件注入

通过@Conditional系列注解,可以根据条件动态注入Bean。

typescript 复制代码
@Configuration
public class AppConfig {
    
    @Bean
    @ConditionalOnProperty(name = "env", havingValue = "dev")
    public UserDAO devUserDAO() {
        return new DevUserDAO();
    }
    
    @Bean
    @ConditionalOnProperty(name = "env", havingValue = "prod")
    public UserDAO prodUserDAO() {
        return new ProdUserDAO();
    }
}

123456789101112131415

2. 循环依赖

在Spring中,循环依赖是通过构造注入解决的。通过@Autowired注解的required属性,可以控制依赖是否为必选。

typescript 复制代码
@Service
public class ServiceA {
    
    @Autowired(required = false)
    private ServiceB serviceB;
    
    public void doSomething() {
        if (serviceB != null) {
            serviceB.doSomething();
        }
    }
}

123456789101112

3. 注入范围

通过@Scope注解,可以控制Bean的作用域(如singletonprototype等)。

less 复制代码
@Service
@Scope("prototype")
public class UserService {
    
    public void saveUser(User user) {
        System.out.println("Saving user in prototype scope");
    }
}

12345678

五、三种注入方式的比较

六、最佳实践

  1. 优先使用构造注入:构造注入在处理不可变依赖时更加安全和直观。
  2. 避免过度注入:只注入真正需要的依赖,避免"饥饿注入"。
  3. 合理使用@Qualifier :在存在多个相同类型Bean时,通过@Qualifier明确指定。
  4. 结合配置文件 :通过@Value注解将配置文件中的属性注入到Bean中。
  5. 测试驱动开发:通过依赖注入,更容易编写单元测试。

优先使用构造方法注入 :构造方法注入能够确保对象在初始化时满足所有依赖,同时符合面向对象的设计原则。
避免使用属性注入 :属性注入破坏了封装性,容易导致代码难以维护和测试。
谨慎使用Setter注入:Setter注入适用于可变依赖或需要延迟初始化的场景,但在大多数情况下,构造方法注入更为推荐

七、总结

通过本文,你不仅了解了Spring依赖注入(DI)的核心原理,还掌握了如何用它实现代码的优雅解耦。不再让紧耦的代码阻碍发展,从今天起,让Spring DI帮助你实现模块化设计、提升可测试性和增强扩展性。优雅的代码不仅是技术的体现,更是开发者的艺术表达。用Spring DI,让你的代码更干净、更易维护,也让开发过程更具乐趣吧!

相关推荐
鱼樱前端2 分钟前
Navicat17基础使用
java·后端
黑风风20 分钟前
深入理解Spring Boot Starter及如何自定义Starter
java·spring boot·后端
uhakadotcom32 分钟前
BM25 算法入门与实践
后端
鱼樱前端37 分钟前
Mac M1安装MySQL步骤
java·后端
uhakadotcom1 小时前
Istio 服务网格:连接、保护和优化微服务的利器
后端·面试·github
Asthenia04122 小时前
Spring事务分析:@Transactional用久了,是不是忘了编程式事务了?
后端
无名指的等待7122 小时前
SpringBoot实现一个Redis限流注解
spring boot·redis·后端
张志翔的博客3 小时前
RK3588 openssl-3.4.1 编译安装
开发语言·后端·scala
计算机-秋大田3 小时前
基于Spring Boot的小区疫情购物系统的设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·课程设计
李长渊哦3 小时前
Spring Boot 约定大于配置:实现自定义配置
java·spring boot·后端