Spring6框架中依赖注入的多种方式(推荐构造器注入)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 依赖注入(DI)是一种过程,对象通过构造函数参数、工厂方法的参数或在对象实例构建后设置的属性来定义它们的依赖关系(即与其一起工作的其他对象)。

你好,这里是codetrend专栏“Spring6全攻略”。

一个典型的企业应用程序不是由单个对象(或在Spring术语中称为bean)组成的。

即使是最简单的应用程序也有一些对象一起工作,呈现给最终用户看到的内容形成一个连贯的应用程序。

要实现多个bean的连贯工作,这里就要使用到Spring的核心技术:依赖注入(DI)。

依赖注入(DI)是一种过程,对象通过构造函数参数、工厂方法的参数或在对象实例构建后设置的属性来定义它们的依赖关系(即与其一起工作的其他对象)。

容器在创建bean时注入这些依赖关系。这个过程基本上是bean本身不再通过直接构造类或使用Service Locator模式控制其依赖项的实例化或位置,因此被称为控制反转(Inversion of Control)。

遵循DI原则的代码更加清晰,对象提供其依赖关系时解耦更有效。

该对象不会查找其依赖项,也不知道依赖项的位置或类别。

因此类变得更易于测试,特别是当依赖项是接口或抽象基类时,可以在单元测试中使用存根或模拟实现。

依赖注入有两种主要变体:基于构造函数的依赖注入和基于Setter的依赖注入。

基于构造函数的依赖注入

基于构造函数的依赖注入是Spring6中的一种依赖注入策略,主要用于确保在对象创建时其必需依赖已经得到初始化。

在构造函数注入中,对象的依赖关系明确地通过构造函数的参数传递给对象。

这意味着在实例化一个类时,Spring IoC容器会分析构造函数签名中的参数类型,然后从容器中查找并提供相匹配的bean作为依赖注入的目标对象。

下面的代码是一个完整的示例,展示了基于构造函数的依赖注入:

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;
import java.util.List;

/**
 * 基于构造函数的依赖注入
 * @author nine
 * @since 1.0
 */
public class ConstructorDIDemo {
   
    public static void main(String[] args) {
   
        // 创建一个基于 Java Config 的应用上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(ConstructorAppConfig.class);
        // 从上下文中获取名bean,其类型为PetStoreService
        SimpleMovieLister bean = context.getBean(SimpleMovieLister.class);
        // 调用获取的bean的方法
        bean.listMovies();
    }
}

/**
 * App配置
 */
@Configuration
class ConstructorAppConfig{
   

    @Bean
    public MovieFinder movieFinder() {
   
        return new MovieFinder();
    }

    @Bean
    public SimpleMovieLister simpleMovieLister(MovieFinder movieFinder) {
   
        return new SimpleMovieLister(movieFinder);
    }
}

/**
 * 服务代码
 */
@Slf4j
class SimpleMovieLister {
   
    private final MovieFinder movieFinder;
    public SimpleMovieLister(MovieFinder movieFinder) {
   
        this.movieFinder = movieFinder;
    }
    public void listMovies() {
   
        log.info("电影列表打印中");
        movieFinder.findMovies().forEach(log::info);
    }
}
@Slf4j
class MovieFinder {
   
    public List<String> findMovies() {
   
        return Arrays.asList("电影1", "电影2", "电影3");
    }
}

在Spring配置文件或Java配置类中,容器会根据构造函数参数类型找到符合条件的bean,并自动调用带有适当参数的构造函数来实例化SimpleMovieLister。这种方式的优势在于:

  1. 确保对象实例化时就有所有的必需依赖项,增强了对象状态的完整性。
  2. 由于构造函数私有的强制性依赖无法为null,提高了代码健壮性。
  3. 有利于实现不可变对象,也就是在属性上面加了final修饰符,提升多线程环境下对象的安全性。
  4. 使得依赖关系清晰可见,利于阅读和理解代码。

Spring6推荐优先使用构造函数注入,尤其是对于必需的、不可缺失的依赖。而对于可选依赖或易于变更的配置属性,则更适合使用setter方法注入。

基于Setter的依赖注入

基于Setter方法的依赖注入是Spring6框架中另一种常用的依赖注入策略。

它允许在对象实例化之后通过调用setter方法来设置依赖关系。

这种方法允许对象在构造完成后继续接受依赖注入,这在依赖不是必须的情况下特别有用,因为对象可以先创建一个默认状态,然后再通过setter方法补充注入依赖。

把构造函数注入修改为如下代码,这是一个完整的示例,展示了基于Setter的依赖注入:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 基于Setter的依赖注入
 * @author nine
 * @since 1.0
 */
public class SetterDIDemo {
   
    public static void main(String[] args) {
   
        // 创建一个基于 Java Config 的应用上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(SetterAppConfig.class);
        // 从上下文中获取名bean,其类型为PetStoreService
        SimpleMovieListerSet bean = context.getBean(SimpleMovieListerSet.class);
        // 调用获取的bean的方法
        bean.listMovies();
    }
}

/**
 * App配置
 */
@Configuration
class SetterAppConfig{
   
    @Bean
    public MovieFinder movieFinder() {
   
        return new MovieFinder();
    }

    @Bean
    public SimpleMovieListerSet simpleMovieLister() {
   
        return new SimpleMovieListerSet();
    }
}

@Slf4j
class SimpleMovieListerSet {
   
    private MovieFinder movieFinder;
    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
   
        this.movieFinder = movieFinder;
    }
    public void listMovies() {
   
        log.info("电影列表打印中");
        movieFinder.findMovies().forEach(log::info);
    }
}

在这种情况下,Spring容器会在创建完SimpleMovieListerSet实例后,查找类型匹配的MovieFinder bean,并调用setMovieFinder()方法将其注入。

setter注入的优点包括:

  1. 可以延迟注入可选依赖,允许类在没有所有依赖的情况下也能创建实例。
  2. 更容易适应配置变化,因为可以在运行时重新配置或替换已注入的依赖项。
  3. 有时候对于第三方类库或不能更改源代码的情况,如果只能通过setter暴露依赖,则setter注入可能是唯一可行的DI方式。

然而,相比于构造函数注入,setter注入的一个潜在缺点是可能导致对象在未完全初始化时就被使用,增加了代码理解和维护的难度,以及可能引入运行时错误的风险。

其它依赖注入方式

  • 属性注入(Field Injection)

属性注入是指直接在类的成员变量上使用@Autowired@Inject注解来声明依赖。Spring容器会在bean初始化时自动为这些字段赋值。例如:

public class UserService {
   
    @Autowired
    private UserRepository userRepository;
    // ...
}
  • 方法注入(Method Injection)

方法注入允许在非构造函数的方法中注入依赖。这包括像Spring Test框架中测试方法的参数注入,以及在方法级别处理依赖,如Spring的@PostConstruct@PreDestroy生命周期回调方法。例如:

@Component
public class MyService {
   
    private SomeDependency someDependency;

    @Autowired
    public void init(SomeDependency someDependency) {
   
        this.someDependency = someDependency;
    }
    // ...
}
  • 注解驱动的配置(Annotation-based Configuration)

使用@Configuration@Bean等注解编写Java配置类,以声明式的方式来定义bean及其依赖关系。例如:

@Configuration
public class AppConfig {
   
    @Bean
    public UserService userService(UserRepository userRepository) {
   
        return new UserService(userRepository);
    }

    @Bean
    public UserRepository userRepository() {
   
        return new UserRepositoryImpl();
    }
}
  • JSR-330注解(Java Dependency Injection)

Spring同时支持JSR-330规范中的注解,如@javax.inject.Inject,可以用它代替Spring的@Autowired来实现依赖注入。

Dependency Resolution Process 依赖注入解析过程

Spring框架中的依赖注入解析过程主要包括以下几个步骤:

配置元数据加载

  • 应用程序启动时,Spring IoC容器首先读取和解析配置元数据,这些元数据可以来自于XML配置文件、Java配置类(通过@Configuration注解)或组件类上的注解(如@Component@Service@Repository@Controller等)。

Bean定义注册

  • 容器根据配置元数据创建Bean Definition对象,这些对象包含了如何创建Bean的全部信息,如Bean的类型(类)、构造器参数、属性值、依赖关系和其他生命周期回调方法等。

依赖解析

  • 当Spring容器创建一个Bean时,它会查看Bean Definition中关于依赖的描述。如果是构造器注入,容器会识别并获取构造器参数所需的Bean,通过调用构造器来注入依赖。
  • 如果是Setter注入,容器会在Bean实例化后遍历其setter方法,找到那些带有@Autowired或其他相关注解的setter方法,然后查找并注入相应的依赖Bean。
  • 若是字段注入,容器则会直接找到类中带有@Autowired等注解的字段,为它们注入合适的Bean。

依赖注入

  • 容器根据Bean定义中定义的依赖关系,从IoC容器中查找或创建需要注入的Bean,并将这些依赖注入到目标Bean中。
  • 注入过程中,容器会解决依赖的循环引用问题,保证依赖链的完整性,并可以处理多种作用域的Bean之间的依赖关系。

Bean生命周期管理

  • 容器除了注入依赖外,还会执行Bean生命周期的相关回调方法,如@PostConstruct@PreDestroy等,以确保Bean在初始化和销毁时能正确执行相应操作。

整个过程体现了控制反转(IoC)的原则,Spring容器扮演了协调者角色,负责创建、装配和管理应用程序中的所有对象,使得对象之间相互解耦,提高了代码的可测试性和可维护性。

整个过程都包含在 BeanFactory 中,这里的代码示例就是这行代码 ApplicationContext context = new AnnotationConfigApplicationContext(SetterAppConfig.class);

// 构造函数分为3个步骤
public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
   
    this();
    register(componentClasses);
    refresh();
}
//在this()初始化Spring相关的工具库,一个reader和一个scanner
public AnnotationConfigApplicationContext() {
   
    StartupStep createAnnotatedBeanDefReader = getApplicationStartup().start("spring.context.annotated-bean-reader.create");
    this.reader = new AnnotatedBeanDefinitionReader(this);
    createAnnotatedBeanDefReader.end();
    this.scanner = new ClassPathBeanDefinitionScanner(this);
}
// register(componentClasses); 是代码的核心,注册配置类里面的相关信息,主要调用了私有方法doRegisterBean

doRegisterBean的核心代码如下:

// 1. 加载配置元数据
// 此方法负责将给定的类转换为AnnotatedGenericBeanDefinition,从而提取类上的元数据信息
private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
        @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier,
        @Nullable BeanDefinitionCustomizer[] customizers) {
   

    // 创建一个基于给定类的AnnotatedGenericBeanDefinition对象
    AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);

    // 2. 判断是否需要跳过此Bean的注册(条件评估)
    if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
   
        return;
    }

    // 标记候选Bean属性
    abd.setAttribute(ConfigurationClassUtils.CANDIDATE_ATTRIBUTE, Boolean.TRUE);

    // 设置实例供应商,用于懒加载或延迟初始化
    abd.setInstanceSupplier(supplier);

    // 3. 解析作用域元数据并设置Bean的作用域
    ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
    abd.setScope(scopeMetadata.getScopeName());

    // 生成或使用指定的Bean名称
    String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));

    // 处理通用定义注解(如@Component, @Service等)
    AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);

    // 处理限定符注解(如@Primary, @Lazy等)
    if (qualifiers != null) {
   
        for (Class<? extends Annotation> qualifier : qualifiers) {
   
            if (Primary.class == qualifier) {
   
                abd.setPrimary(true); // 设置为主Bean
            } else if (Lazy.class == qualifier) {
   
                abd.setLazyInit(true); // 设置为懒加载
            } else {
   
                abd.addQualifier(new AutowireCandidateQualifier(qualifier)); // 添加自定义限定符
            }
        }
    }

    // 4. 应用自定义Bean定义配置
    if (customizers != null) {
   
        for (BeanDefinitionCustomizer customizer : customizers) {
   
            customizer.customize(abd); // 根据用户提供的定制器调整Bean定义
        }
    }

    // 创建BeanDefinitionHolder对象,封装了最终的Bean定义和名称
    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);

    // 根据作用域元数据应用代理模式(如果需要)
    definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);

    // 5. 注册Bean定义到BeanDefinitionRegistry中
    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

doRegisterBean主要执行以下逻辑:

  1. 配置元数据加载:从给定的类beanClass中提取元数据,并封装成AnnotatedGenericBeanDefinition对象。
  2. Bean定义注册前的准备工作:判断Bean是否满足注册条件,设置候选属性、作用域元数据和Bean名称,处理通用定义注解和限定符注解,以及应用用户自定义的Bean定义配置。
  3. 依赖解析和注入:这部分主要是通过设置作用域、限定符和自定义配置来预备Bean的依赖解析和注入过程,但具体的依赖注入发生在后续的Bean实例化阶段。在这里,Bean定义已经被完善并准备注册到BeanDefinitionRegistry中,后续容器在初始化Bean时会根据这些定义信息完成依赖注入。

关于作者

来自全栈程序员nine的探索与实践,持续迭代中。

欢迎关注或者点个小红心~

目录
相关文章
|
18天前
|
XML 安全 Java
|
21天前
|
缓存 NoSQL Java
什么是缓存?如何在 Spring Boot 中使用缓存框架
什么是缓存?如何在 Spring Boot 中使用缓存框架
28 0
|
10天前
|
Java Spring
一键注入 Spring 成员变量,顺序编程
介绍了一款针对Spring框架开发的插件,旨在解决开发中频繁滚动查找成员变量注入位置的问题。通过一键操作(如Ctrl+1),该插件可自动在类顶部添加`@Autowired`注解及其成员变量声明,同时保持光标位置不变,有效提升开发效率和代码编写流畅度。适用于IntelliJ IDEA 2023及以上版本。
一键注入 Spring 成员变量,顺序编程
|
4天前
|
IDE Java 测试技术
互联网应用主流框架整合之Spring Boot开发
通过本文的介绍,我们详细探讨了Spring Boot开发的核心概念和实践方法,包括项目结构、数据访问层、服务层、控制层、配置管理、单元测试以及部署与运行。Spring Boot通过简化配置和强大的生态系统,使得互联网应用的开发更加高效和可靠。希望本文能够帮助开发者快速掌握Spring Boot,并在实际项目中灵活应用。
23 5
|
14天前
|
缓存 Java 数据库连接
Spring框架中的事件机制:深入理解与实践
Spring框架是一个广泛使用的Java企业级应用框架,提供了依赖注入、面向切面编程(AOP)、事务管理、Web应用程序开发等一系列功能。在Spring框架中,事件机制是一种重要的通信方式,它允许不同组件之间进行松耦合的通信,提高了应用程序的可维护性和可扩展性。本文将深入探讨Spring框架中的事件机制,包括不同类型的事件、底层原理、应用实践以及优缺点。
46 8
|
12天前
|
Java 数据库 数据安全/隐私保护
轻松掌握Spring依赖注入:打造你的登录验证系统
本文以轻松活泼的风格,带领读者走进Spring框架中的依赖注入和登录验证的世界。通过详细的步骤和代码示例,我们从DAO层的创建到Service层的实现,再到Spring配置文件的编写,最后通过测试类验证功能,一步步构建了一个简单的登录验证系统。文章不仅提供了实用的技术指导,还以口语化和生动的语言,让学习变得不再枯燥。
28 2
|
24天前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
44 6
|
28天前
|
Java Kotlin 索引
学习Spring框架特性及jiar包下载
Spring 5作为最新版本,更新了JDK基线至8,修订了核心框架,增强了反射和接口功能,支持响应式编程及Kotlin语言,引入了函数式Web框架,并提升了测试功能。Spring框架可在其官网下载,包括文档、jar包和XML Schema文档,适用于Java SE和Java EE项目。
31 0
|
2月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
69 1
|
Java 测试技术 开发者
Spring构造器注入有多好?
Spring构造器注入有多好?
1126 0
Spring构造器注入有多好?
下一篇
DataWorks