你好,这里是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
。这种方式的优势在于:
Spring6推荐优先使用构造函数注入,尤其是对于必需的、不可缺失的依赖。而对于可选依赖或易于变更的配置属性,则更适合使用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注入的优点包括:
然而,相比于构造函数注入,setter注入的一个潜在缺点是可能导致对象在未完全初始化时就被使用,增加了代码理解和维护的难度,以及可能引入运行时错误的风险。
属性注入是指直接在类的成员变量上使用@Autowired
或@Inject
注解来声明依赖。Spring容器会在bean初始化时自动为这些字段赋值。例如:
public class UserService {
@Autowired
private UserRepository userRepository;
// ...
}
方法注入允许在非构造函数的方法中注入依赖。这包括像Spring Test框架中测试方法的参数注入,以及在方法级别处理依赖,如Spring的@PostConstruct
、@PreDestroy
生命周期回调方法。例如:
@Component
public class MyService {
private SomeDependency someDependency;
@Autowired
public void init(SomeDependency someDependency) {
this.someDependency = someDependency;
}
// ...
}
使用@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();
}
}
Spring同时支持JSR-330规范中的注解,如@javax.inject.Inject
,可以用它代替Spring的@Autowired
来实现依赖注入。
Spring框架中的依赖注入解析过程主要包括以下几个步骤:
配置元数据加载:
@Configuration
注解)或组件类上的注解(如@Component
、@Service
、@Repository
和@Controller
等)。Bean定义注册:
依赖解析:
@Autowired
或其他相关注解的setter方法,然后查找并注入相应的依赖Bean。@Autowired
等注解的字段,为它们注入合适的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
主要执行以下逻辑:
beanClass
中提取元数据,并封装成AnnotatedGenericBeanDefinition
对象。BeanDefinitionRegistry
中,后续容器在初始化Bean时会根据这些定义信息完成依赖注入。来自全栈程序员nine的探索与实践,持续迭代中。
欢迎关注或者点个小红心~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。