京东自营 + 国补 iPhone 历史最低价          国家补贴 享8折

Spring Boot 4.0.0新特性:简化版BeanRegistrar,开启便捷Bean注入新时代

Spring Boot 4.0.0 简介

Spring Boot 作为一款在 Java 开发领域广受欢迎的框架,以其 “约定优于配置” 的原则,极大地简化和加速了 Spring 应用程序的开发过程。而 Spring Boot 4.0.0 作为该框架的重要版本迭代,更是带来了一系列令人瞩目的新特性与改进,进一步巩固了其在 Java 开发中的地位。

在新特性方面,Spring Boot 4.0.0 首次全面支持 JDK 17 ,这使得开发者能够在应用程序中充分利用 JDK 17 的新特性和性能优化,比如 Pattern Matching for instanceof、Sealed Classes 等特性,为代码编写带来更多便利和灵活性。在云原生应用开发盛行的当下,4.0.0 版本进一步加强了与云原生组件的集成,像 Kubernetes、Docker 和 OpenShift 等,开发者可以更轻松地将 Spring Boot 应用程序部署到云环境中,借助云原生的优势实现弹性扩展、容器化部署和服务发现等功能。在响应式编程方面,它也加强了支持,引入了响应式 Web 框架和响应式数据访问,方便开发者构建高性能、可伸缩的响应式应用程序 ,以应对高并发、高吞吐量的应用场景。安全层面同样得到强化,新增了一系列安全特性,涵盖更强大的身份验证和授权机制、OAuth 2.0 的改进支持以及对微服务架构的安全性增强,为应用程序和数据提供更可靠的保护。配置方面也更加简化,通过 application.yml 或 application.properties 文件,开发者能够更清晰地定义应用程序的配置,减少了 XML 或 Java 配置的编写量。

其中,Bean 注入功能的提升在众多改进中尤为突出。Bean 注入是 Spring 框架中的核心功能之一,它实现了对象之间依赖关系的自动管理,让开发者无需手动创建和管理对象实例及其依赖,极大地提高了代码的可维护性和可测试性。在以往版本中,虽然 Bean 注入已经相对便捷,但开发过程中仍存在一些繁琐的配置和使用步骤。而 Spring Boot 4.0.0 通过引入简化版 BeanRegistrar 等方式,对 Bean 注入进行了优化,使得 Bean 的注册与注入过程更加简洁高效,进一步减少了开发人员的工作量,提高了开发效率,让开发者能够将更多的精力投入到业务逻辑的实现上 。

什么是 Bean 注入

在 Spring 框架的体系中,Bean 注入是极为关键的核心功能,它是依赖注入(Dependency Injection,简称 DI)原则的一种具体实现方式。从本质上来说,Bean 注入指的是 Spring 容器在运行期间,能够自动将某个对象所依赖的其他对象,精准地注入到该对象当中 。

打个比方,在一个电商系统里,订单服务(OrderService)需要依赖商品服务(ProductService)来获取商品的相关信息,以便完成订单的创建与处理。在传统的编程模式下,开发人员可能需要在 OrderService 类中,通过手动编写代码来创建 ProductService 的实例,如下所示:

public class OrderService {


   private ProductService productService = new ProductService();


   // 其他业务方法

   public void createOrder() {


       // 使用productService获取商品信息

       Product product = productService.getProductById(1);


       // 处理订单创建逻辑

   }


}

但这种方式存在明显的弊端,它使得 OrderService 与 ProductService 之间形成了紧密的耦合关系。一旦 ProductService 的实现类发生改变,比如需要切换为另一种获取商品信息的方式,或者需要对 ProductService 进行重构,那么 OrderService 的代码也必须随之修改,这无疑增加了代码维护的难度和成本 。

而借助 Spring 的 Bean 注入机制,情况就大不相同了。开发人员只需在 OrderService 中声明对 ProductService 的依赖,无需关心 ProductService 实例的创建过程,Spring 容器会自动负责将合适的 ProductService 实例注入到 OrderService 中 。使用注解方式实现 Bean 注入的代码示例如下:

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


import org.springframework.stereotype.Service;


@Service

public class OrderService {


   private final ProductService productService;


   @Autowired

   public OrderService(ProductService productService) {


       this.productService = productService;


   }


   // 其他业务方法

   public void createOrder() {


       // 使用productService获取商品信息

       Product product = productService.getProductById(1);


       // 处理订单创建逻辑

   }


}

在上述代码中,通过@Autowired注解标记构造函数,Spring 容器会自动查找并注入一个符合类型要求的 ProductService 实例。这种方式实现了对象之间依赖关系的解耦,使得代码更加灵活、可维护和可测试 。

依赖注入背后的核心思想是控制反转(Inversion of Control,简称 IoC)。在传统编程中,对象的创建和依赖关系的管理由应用程序代码自身负责,对象对其依赖的对象拥有控制权。而在采用依赖注入和控制反转的 Spring 框架中,对象的创建和依赖关系的管理被转移到了 Spring 容器中,应用程序代码不再直接控制这些过程,而是由 Spring 容器来负责创建、配置和组装对象,对象只需被动地接受 Spring 容器注入的依赖对象 。这就好比在一场音乐会上,指挥家(Spring 容器)负责协调各个乐器演奏者(对象)之间的配合,每个演奏者只需专注于自己的演奏任务,而无需操心整个演奏的组织和协调工作 。

通过 Bean 注入实现的依赖注入机制,具有诸多显著的优势。它能够极大地降低代码之间的耦合度,提高代码的可维护性和可扩展性。当某个依赖对象的实现发生变化时,只需在 Spring 容器的配置中进行相应调整,而无需修改大量的业务代码 。依赖注入还使得代码的单元测试变得更加简单和方便,开发人员可以轻松地为被测试对象注入模拟的依赖对象,从而独立地测试业务逻辑,提高测试的准确性和效率 。

Spring Boot 中传统的 Bean 注入方式

在 Spring Boot 开发过程中,传统的 Bean 注入方式主要包含构造函数注入、Setter 方法注入以及字段注入这三种,它们各自具备独特的特性和应用场景 。下面将对这三种传统的 Bean 注入方式进行详细的介绍。

构造函数注入

构造函数注入是通过类的构造函数来完成依赖注入的操作。当 Spring 容器创建目标类的实例时,会调用该类带有依赖参数的构造函数,将对应的依赖对象传递进去,从而实现依赖注入 。这种注入方式的显著特点在于,依赖对象在创建目标对象的同时就完成了注入,确保了对象在初始化时就具备了所需的依赖,使得对象的状态在创建后就处于完整且可用的状态 。

以一个常见的业务场景为例,假设我们正在开发一个用户管理系统,其中UserService负责处理用户相关的业务逻辑,而UserService依赖于UserRepository来进行用户数据的持久化操作 。下面是使用构造函数注入的代码示例:

import org.springframework.stereotype.Service;


@Service

public class UserServiceImpl implements UserService {


   private final UserRepository userRepository;


   public UserServiceImpl(UserRepository userRepository) {


       this.userRepository = userRepository;


   }


   @Override

   public User getUserById(Long id) {


       return userRepository.findById(id);


   }


   @Override

   public void saveUser(User user) {


       userRepository.save(user);


   }


}

在上述代码中,UserServiceImpl类通过构造函数接收一个UserRepository类型的参数,Spring 容器在创建UserServiceImpl实例时,会自动查找并注入一个符合类型要求的UserRepository实例 。这种方式使得UserServiceImplUserRepository之间的依赖关系一目了然,并且由于userRepository被声明为final,保证了依赖的不可变性,增强了代码的健壮性和安全性 。在进行单元测试时,也能够很方便地通过构造函数传入模拟的UserRepository对象,从而实现对UserServiceImpl业务逻辑的独立测试 。

Setter 方法注入

Setter 方法注入是借助 Java 类中的 Setter 方法来实现依赖注入。在这种注入方式中,Spring 容器会在创建目标对象之后,调用其 Setter 方法,并将依赖对象作为参数传入,以此完成依赖注入的过程 。这种方式相较于构造函数注入,具有更高的灵活性,因为它允许在对象创建后的运行时阶段动态地修改依赖对象 。

以下是使用 Setter 方法注入的代码示例,仍以上述用户管理系统为例,UserServiceImpl类通过 Setter 方法注入UserRepository

import org.springframework.stereotype.Service;


@Service

public class UserServiceImpl implements UserService {


   private UserRepository userRepository;


   public void setUserRepository(UserRepository userRepository) {


       this.userRepository = userRepository;


   }


   @Override

   public User getUserById(Long id) {


       return userRepository.findById(id);


   }


   @Override

   public void saveUser(User user) {


       userRepository.save(user);


   }


}

在这个示例中,UserServiceImpl类中定义了一个setUserRepository方法,用于接收UserRepository对象 。在 Spring 配置文件中,或者通过注解配置,Spring 容器会识别到这个 Setter 方法,并将相应的UserRepository实例注入进去 。这种方式适用于那些依赖关系在对象创建时可能还未确定,或者需要在运行时根据不同的业务场景进行动态切换的情况 。但需要注意的是,使用 Setter 方法注入时,如果必需的依赖项没有被设置,可能会导致运行时错误,因此在代码设计和使用过程中需要格外小心,确保依赖项能够被正确设置 。

字段注入

字段注入是一种最为直接的依赖注入方式,它直接在类的字段上使用@Autowired注解,Spring 容器会通过反射机制,将匹配的依赖对象直接注入到该字段中,而无需显式地调用构造函数或 Setter 方法 。这种方式使得代码看起来简洁明了,实现起来也非常简单 。

下面是字段注入的代码示例:

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


import org.springframework.stereotype.Service;


@Service

public class UserServiceImpl implements UserService {


   @Autowired

   private UserRepository userRepository;


   @Override

   public User getUserById(Long id) {


       return userRepository.findById(id);


   }


   @Override

   public void saveUser(User user) {


       userRepository.save(user);


   }


}

在上述代码中,UserServiceImpl类中的userRepository字段使用@Autowired注解进行标注,Spring 容器在创建UserServiceImpl实例时,会自动查找并将UserRepository实例注入到该字段中 。尽管字段注入具有代码简洁的优点,但它也存在一些明显的局限性 。由于依赖关系是通过字段直接注入的,在单元测试中,很难对依赖对象进行模拟和替换,因为无法通过构造函数或方法来明确传入依赖,这使得类的可测试性较差 。字段注入还可能导致依赖关系不够清晰,因为依赖项没有在构造函数或 Setter 方法中显式声明,不利于代码的维护和理解 。字段注入不能用于final字段和静态字段,因为final字段必须在构造函数中初始化,而静态字段属于类级别,不属于对象实例,无法通过对象注入的方式进行依赖注入 。

Spring Boot 4.0.0 中 BeanRegistrar 实现便捷的 Bean 注入

BeanRegistrar 接口介绍

在 Spring Framework 7.0.0 中,新增的 BeanRegistrar 接口为开发者带来了一种全新的、更加灵活的编程式 Bean 注册方式 。BeanRegistrar 接口的主要作用在于,它允许开发者在 Spring 容器的初始化过程中,以编程的方式动态地注册 Bean,这为处理复杂的依赖关系和动态配置提供了强大的支持 。

BeanRegistrar 接口的核心方法是register,该方法接收两个参数:BeanRegistryEnvironment 。其中,BeanRegistry是 Spring 容器中用于注册和管理 Bean 定义的核心接口,通过它,开发者可以方便地注册新的 Bean;Environment则提供了对 Spring 应用程序环境的访问,开发者可以从中获取各种配置信息,如配置文件中的属性、环境变量等,这使得根据不同的环境条件动态注册 Bean 成为可能 。

BeanRegistrar 接口支持根据活动配置文件等条件动态注册多个 Bean 。例如,在开发一个多环境的应用程序时,可能需要在开发环境中注册一些用于调试和测试的 Bean,而在生产环境中则注册不同的 Bean 来满足性能和安全要求 。借助 BeanRegistrar 接口,开发者可以轻松实现这一需求,极大地提高了应用程序的灵活性和可配置性 。

代码示例

以下是一个基于 BeanRegistrar 接口实现动态注册 Bean 的完整代码示例 。假设我们正在开发一个电商应用程序,其中有UserServiceOrderService,并且希望根据配置文件中的环境信息来动态注册OrderService的不同实现 。

首先,定义UserServiceOrderService接口及其实现类:

public interface UserService {


   void sayHello();


}


public class UserServiceImpl implements UserService {


   @Override

   public void sayHello() {


       System.out.println("UserService says hello!");


   }


}


public interface OrderService {


   void placeOrder();


}


public class OrderServiceImpl implements OrderService {


   @Override

   public void placeOrder() {


       System.out.println("Default OrderService places an order!");


   }


}


public class DevOrderServiceImpl implements OrderService {


   @Override

   public void placeOrder() {


       System.out.println("Dev OrderService places an order! with more debug information.");


   }


}

然后,创建一个实现BeanRegistrar接口的类MyBeansRegistrar,在其中根据配置文件条件注册不同的 Bean:

import org.springframework.beans.factory.BeanRegistrar;


import org.springframework.beans.factory.BeanRegistry;


import org.springframework.context.annotation.Configuration;


import org.springframework.core.env.Environment;


@Configuration

public class MyConfiguration {


   // 配置类,用于触发BeanRegistrar的注册逻辑

}


class MyBeansRegistrar implements BeanRegistrar {


   @Override

   public void register(BeanRegistry registry, Environment env) {


       // 注册UserService

       registry.registerBean("userService", UserServiceImpl.class);


       // 根据环境条件注册OrderService

       if (env.matchesProfiles("dev")) {


           registry.registerBean("orderService", DevOrderServiceImpl.class);


       } else {


           registry.registerBean("orderService", OrderServiceImpl.class);


       }


   }


}

在上述代码中,MyConfiguration类是一个配置类,通过它来触发MyBeansRegistrar的注册逻辑 。在MyBeansRegistrarregister方法中,首先使用BeanRegistryregisterBean方法注册了UserService的实现类UserServiceImpl 。接着,通过EnvironmentmatchesProfiles方法判断当前环境是否为开发环境(”dev”),如果是,则注册DevOrderServiceImpl作为OrderService的实现;否则,注册OrderServiceImpl作为OrderService的实现 。这样,在不同的环境下,Spring 容器中注册的OrderService实现类会有所不同,从而实现了根据环境条件动态注册 Bean 的功能 。

与传统方式对比

与传统的 Bean 注入方式相比,BeanRegistrar 在灵活性、代码简洁性和可维护性等方面具有明显的优势 。

从灵活性角度来看,传统的构造函数注入、Setter 方法注入和字段注入方式,主要是在类的定义阶段就确定了依赖关系,并且依赖对象的注入是静态的,一旦配置完成,在运行时很难根据不同的条件进行动态调整 。而 BeanRegistrar 接口允许在 Spring 容器初始化过程中,根据各种条件(如环境变量、配置文件属性、运行时状态等)动态地注册 Bean,这使得应用程序在面对复杂多变的业务需求和环境差异时,能够更加灵活地进行配置和调整 。例如,在微服务架构中,不同的微服务实例可能需要根据自身的部署环境和业务负载情况,动态地注册不同的服务实现或中间件组件,BeanRegistrar 就能够很好地满足这种动态配置的需求 。

在代码简洁性方面,传统的注入方式通常需要在类中显式地声明构造函数、Setter 方法或字段,并使用注解进行标注,这在一定程度上增加了代码的冗余度 。特别是当一个类依赖的对象较多时,构造函数或 Setter 方法的参数列表会变得冗长,代码的可读性和维护性也会随之降低 。而使用 BeanRegistrar 接口,通过在一个实现类中集中处理 Bean 的注册逻辑,可以使业务类的代码更加简洁,专注于业务逻辑的实现,减少了与依赖注入相关的代码量 。例如,在上述电商应用的示例中,UserServiceOrderService的实现类中无需再包含任何与依赖注入相关的代码,只需要关注自身的业务方法,使得代码结构更加清晰简洁 。

在可维护性方面,传统注入方式下,如果需要修改依赖关系或注入的对象,往往需要在多个地方进行修改,包括类的定义、配置文件或注解等,这增加了维护的难度和出错的风险 。而 BeanRegistrar 将 Bean 的注册逻辑集中在一个地方,当需要调整依赖关系或根据不同条件注册不同的 Bean 时,只需要在实现BeanRegistrar接口的类中进行修改,无需在大量的业务类中进行繁琐的调整,从而提高了代码的可维护性和可扩展性 。例如,当电商应用的业务需求发生变化,需要在生产环境中引入一种新的OrderService实现时,只需要在MyBeansRegistrar类中修改注册逻辑,而不会影响到其他业务类的代码,使得系统的维护更加方便和高效 。

完整应用实例代码

项目结构介绍

基于 Spring Boot 4.0.0 创建的项目结构通常遵循约定俗成的规范,具有清晰的层次结构,以便于开发、维护和管理 。以下是一个典型的 Spring Boot 项目结构:

my-spring-boot-project


├── src


│   ├── main


│   │   ├── java


│   │   │   └── com


│   │   │       └── nowjava


│   │   │           └── myapp


│   │   │               ├── Application.java           # 主启动类


│   │   │               ├── config


│   │   │               │   └── MyConfiguration.java   # 配置类,用于触发BeanRegistrar的注册逻辑


│   │   │               ├── controller


│   │   │               │   └── MyController.java      # 控制器层,处理HTTP请求


│   │   │               ├── service


│   │   │               │   ├── UserService.java       # 服务接口


│   │   │               │   └── impl


│   │   │               │       └── UserServiceImpl.java # 服务接口实现类


│   │   │               ├── dao


│   │   │               │   └── UserDao.java           # 数据访问层接口


│   │   │               ├── entity


│   │   │               │   └── User.java              # 实体类,与数据库表映射


│   │   │               └── util


│   │   │                   └── MyUtils.java           # 工具类


│   │   └── resources


│   │       ├── application.yml                        # 配置文件


│   │       ├── static                                 # 静态资源目录,存放CSS、JS、图片等


│   │       └── templates                              # 模板文件目录,存放HTML等模板


│   └── test


│       └── java


│           └── com


│               └── nowjava


│                   └── myapp


│                       └── MyAppTest.java             # 测试类


├── pom.xml                                             # Maven项目对象模型文件,管理项目依赖和构建配置


└── README.md                                           # 项目说明文件,介绍项目功能、使用方法等

在这个项目结构中,src/main/java目录存放 Java 源代码,按照包名进行组织,各个模块的代码各司其职 。Application.java是 Spring Boot 应用的主启动类,通过@SpringBootApplication注解开启 Spring Boot 的自动配置和组件扫描功能 。config包用于存放各种配置类,MyConfiguration.java在这里负责触发BeanRegistrar的注册逻辑 。controller包中的控制器类负责处理 HTTP 请求,接收客户端的输入并返回相应的响应 。service包定义了业务逻辑接口及其实现类,UserService.java是接口,UserServiceImpl.java是具体的实现,实现了业务逻辑的处理 。dao包用于定义数据访问层接口,UserDao.java用于与数据库进行交互,执行数据的增删改查操作 。entity包中的实体类User.java与数据库表进行映射,通过注解等方式定义表结构和字段映射关系 。util包存放工具类,MyUtils.java可以包含一些通用的工具方法,如字符串处理、日期处理等 。src/main/resources目录存放资源文件,application.yml是 Spring Boot 的核心配置文件,用于配置应用的各种属性,如数据库连接、服务器端口等 。static目录用于存放静态资源,如 CSS、JS 文件和图片等,这些资源可以直接被浏览器访问 。templates目录存放模板文件,如 HTML 模板,结合模板引擎(如 Thymeleaf)可以动态生成 HTML 页面 。src/test/java目录存放测试代码,MyAppTest.java用于编写单元测试和集成测试,验证各个模块的功能是否正常 。pom.xml文件是 Maven 项目的核心配置文件,用于管理项目的依赖关系、构建配置和插件等,通过引入各种依赖,项目可以使用 Spring Boot 及其相关组件 。README.md文件是项目的说明文档,提供项目的基本信息、功能介绍、使用方法和部署说明等,方便其他开发者了解和使用项目 。

配置文件

在 Spring Boot 项目中,application.yml是常用的配置文件,它以简洁的 YAML 格式存储应用程序的各种配置信息 。以下是一个application.yml配置文件的示例,适用于上述项目结构,并结合了 BeanRegistrar 的使用场景:

server:


 port: 8080 # 配置服务器端口号,应用启动时将监听此端口


spring:


 application:


   name: my-spring-boot-app # 配置应用程序名称,用于标识应用


 profiles:


   active: dev # 激活开发环境配置文件,可根据不同环境切换


 datasource:


   driver-class-name: com.mysql.cj.jdbc.Driver # 配置数据库驱动类


   url: jdbc:mysql://localhost:3306/mydb # 配置数据库连接URL

   username: root # 配置数据库用户名


   password: root # 配置数据库密码


 jpa:


   hibernate:


     ddl-auto: update # 配置Hibernate的DDL自动更新策略,update表示启动时自动更新数据库表结构


   properties:


     hibernate:


       dialect: org.hibernate.dialect.MySQL8Dialect # 配置Hibernate的数据库方言,指定为MySQL 8的方言

在上述配置文件中,server.port配置项指定了应用程序启动时监听的端口号为 8080 。如果不配置此项,Spring Boot 会使用默认端口号 8080 。spring.application.name配置项设置了应用程序的名称为my-spring-boot-app,这个名称在一些监控和管理工具中用于标识应用 。spring.profiles.active配置项激活了开发环境(dev)的配置文件 。通过这种方式,可以根据不同的环境(如开发、测试、生产)创建多个配置文件(如application-dev.ymlapplication-test.ymlapplication-prod.yml),并在启动时通过修改此配置项来切换不同环境的配置 。spring.datasource相关配置项用于配置数据库连接信息 。driver-class-name指定了使用的数据库驱动类为com.mysql.cj.jdbc.Driver,这是 MySQL 8 的 JDBC 驱动类 。url配置了数据库的连接 URL,指定连接到本地的 MySQL 数据库,数据库名为mydbusernamepassword分别配置了数据库的用户名和密码,这里假设用户名和密码均为rootspring.jpa相关配置项用于配置 JPA(Java Persistence API)的相关属性 。hibernate.ddl-auto配置为update,表示在应用启动时,Hibernate 会自动检查实体类和数据库表结构的差异,并根据需要更新数据库表结构,这在开发阶段非常方便,可以避免手动创建和更新数据库表 。properties.hibernate.dialect配置了 Hibernate 使用的数据库方言为org.hibernate.dialect.MySQL8Dialect,这确保了 Hibernate 能够正确地生成针对 MySQL 8 的 SQL 语句 。

关键代码展示

下面提供使用 BeanRegistrar 进行 Bean 注入的核心业务代码,包含接口定义、实现类以及配置类的代码,并说明它们之间的关系 。

首先,定义UserService接口和UserServiceImpl实现类:

// UserService接口定义

public interface UserService {


   void sayHello();


}


// UserServiceImpl实现类

public class UserServiceImpl implements UserService {


   @Override

   public void sayHello() {


       System.out.println("UserService says hello!");


   }


}

在上述代码中,UserService接口定义了一个sayHello方法,用于提供特定的业务功能 。UserServiceImpl类实现了UserService接口,并具体实现了sayHello方法,在方法中打印出问候信息 。

接着,创建实现BeanRegistrar接口的类MyBeansRegistrar,用于动态注册 Bean:

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java