写点什么

难得可贵的 Spring 依赖注入实战经验,是程序员就该吸收一下!

  • 2023-06-17
    湖南
  • 本文字数:3728 字

    阅读完需:约 12 分钟

Spring 依赖注入实战经验

介绍完 Spring 框架所具备的三种依赖注入类型,我们发现使用这些类型并不复杂。但不复杂并不代表开发人员能够用得好。


接下来,我将和大家分享使用依赖注入的一些实战经验。

把握 Bean 的作用域

在介绍 Setter 方法注入时,我们已经提到了 Spring 中的 Bean 作用域的概念。作用域描述了 Bean 在 Spring IoC 容器上下文中的生命周期和可见性。在这里,我们将讨论 Spring 框架中不同类型的 Bean 作用域及其在使用上的指导规则。


如果想要通过注解来设置 Bean 的作用域,可以使用如下所示的示例代码:

@Configurationpublic class AppConfig {	@Bean	@Scope("singleton")	public HealthRecordService createHealthRecordService() {		return new HealthRecordServiceImpl();	}}
复制代码

可以看到这里使用了一个 @Scope 注解来指定 Bean 的作用域为单例的 singleton。在 Spring 中,除了单例作用域之外,还有一个 prototype,即原型作用域,也可以称之为多例作用域,以与单例作用域进行区别。使用方式上,我们同样可以使用如下所示的枚举值来对它们进行设置。

@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
复制代码

在 Spring IoC 容器中,Bean 的默认作用域是单例,也就是说不管有多少个对 Bean 的引用,容器只会创建一个实例。而原型作用域则不同,每次请求 Bean 时,Spring IoC 容器都会创建一个新的对象实例。


从两种作用域的效果而言,我们总结出一条开发上的经验,即对于有状态的 Bean,我们应该使用原型作用域,反之则应该使用单例作用域。


那么,什么样的 Bean 是有状态的呢?结合 Web 应用程序,我们可以明确对于每次 HTTP 请求而言,我们都应该创建一个 Bean 来代表这一次的请求对象。


同样,对于会话而言,我们也需要针对每个会话创建一个会话状态对象。这些都是常见的有状态的 Bean。为了更好地管理这些 Bean 的生命周期,Spring 还专门针对 Web 开发场景提供了对应的 request 和 session 作用域。

灵活使用注解配置

在使用 Spring 依赖注入类型时,通常可以使用 XML 配置、Java 代码配置以及注解配置这三种方式。随着 Spring Boot 框架的流行,使用注解配置已经成为目前最主流的开发方式。除了前面已经给出的最常见的 @Autowired 注解,Spring Boot 框架还提供了一组非常有用的注解帮助我们更好地管理所注入的对象,包括 @Primary 注解和 @Qualifier 注解。


在 Spring IoC 容器中,针对 HealthRecordService 这样一种接口类型,原则上容器只允许注入一个实现类。如果存在该类型的多个对象实例,那么容器就会报 NoUniqueBean-DefinitionException,意味着容器无法决定选择哪一个实例来进行注入。这时候就可以使用 @Primary 注解来帮助容器做出选择,该注解的使用方式如下所示:

@Componentpublic class HealthRecordServiceImplA implements HealthRecordService {	...}@Component@Primarypublic class HealthRecordServiceImplB implements HealthRecordService {	...}
复制代码

这时候,Spring IoC 容器只会注入 HealthRecordServiceImplB 这个实例类,这在管理针对某种类型的多个实例时非常有用。


和 @Primary 注解的应用场景类似,@Qualifier 注解为我们选择实例类进行注入提供了更加灵活的实现方式,如下所示:

@Component@Qualifier("healthRecordServiceA")public class HealthRecordServiceImplA implements HealthRecordService {}@Component@Qualifier("healthRecordServiceB")public class HealthRecordServiceImplB implements HealthRecordService {}
复制代码

可以看到,这里对不同的实现类,我们通过 @Qualifier 注解设置了不同的名称,这样在使用时就可以通过该名称获取不同的实例,如下所示:

@Autowired@Qualifier("healthRecordServiceB")private HealthRecordService healthRecordService;
复制代码

设置组件扫描范围

在 Spring 中,我们可以通过设置组件扫描范围来简化 Bean 的注入配置。


因为任何类都位于某一个包结构之下,所以 Spring 提供了一个 @ComponentScan 注解,该注解在需要大规模对象注入的场景下非常有用,其基本用法如下所示:

@Configuration@ComponentScan(basePackages="com.spring.bestpractice")public class AppConfig {}
复制代码

在这个示例中,Spring 会扫描由 basePackages 指定的包路径 com.spring.bestpractice 及其子路径下的所有 Bean,并把它们注入到容器中。当然,我们首先需要在这些类上添加 @Component 注解以及由该注解衍生的 @Service、@Repository、@Controller 等注解。

不同配置的性能分析

在本小节中,我们将讨论不同类型的 Bean 配置如何影响应用程序性能,并且我们还将讨论 Bean 配置的一些最佳实践。


首先要讨论的是前面介绍的 @ComponentScan 注解。因为该注解会扫描 basePackages 指定的包中的所有组件,所以如果所指定包中的组件并不需要在应用程序启动时就全部加载到容器中,那么对包路径进行精细化设计是一个实践技巧。例如,我们可以通过设置一个列表来细化具体的包结构路径,如下所示:

@Configuration@ComponentScan(basePackages="com.spring.bestpractice.service","com.spring.bestpractice.controller")public class AppConfig {}
复制代码

然后要讨论的是单例模式和原型模式对性能的影响。在 Spring 中,当把 Bean 范围设置为 prototype 时,每次请求 Bean 时,Spring IoC 容器都会创建一个新的对象实例。所以,使用原型模式在创建过程中会对性能产生影响,对那些初始化过程需要消耗巨大资源的对象而言尤其如此,这些对象常见的有网络连接对象、数据库连接对象等。因此,对这些对象,应该完全避免使用原型模式。或者,我们应该在使用前仔细设计并对性能进行充分测试。


最后一个值得讨论的性能分析点在于 Spring IoC 容器的延迟加载(LazyLoading)和预加载(Preloading)机制。通过 @Autowired 注入的 Bean 都是在 Spring IoC 容器启动时被创建和初始化的,这个过程被称为预加载。但有时候,我们希望能够延迟 Bean 的加载时机,这时候就可以使用 @Lazy 注解,使用方法如下所示:

@Component@Lazypublic class HealthRecordServiceImpl implements HealthRecordService {}
复制代码

添加了 @Lazy 注解的效果是只有在使用到这个 Bean 时它才会去初始化,而不是在 Spring IoC 容器启动时直接初始化,这样就可以节省容器资源。


延迟加载确保在请求时动态加载 Bean,预加载确保在使用 Bean 之前加载 Bean。Spring IoC 容器默认使用预加载。然而,在容器启动时就加载所有类(即使它们没有被使用)并不是一个明智的决定,因为有些 Bean 实例会非常消耗资源。我们应该根据实际情况选择具体的加载方法。如果需要尽快地加载应用程序,那么就采用延迟加载;如果需要应用程序尽快地运行并更快地为请求提供服务,那么就执行预加载。

Spring 依赖注入面试题分析

面试题 1:Spring 框架提供了哪几种依赖注入类型,推荐使用哪种注入类型?

答案:Spring 框架提供了分别基于字段、构造器和 Setter 方法的三种依赖注入类型,其中,Spring 官方推荐使用的是构造器注入类型。


面试题 2:Spring 中 Bean 的作用域有哪些,如何正确选择作用域?

答案:Spring 中 Bean 的作用域常见的有单例和原型两种,默认的是单例。在选择作用域时,基本原则就是对有状态的 Bean,我们应该使用原型作用域,反之则应该使用单例作用域。


面试题 3:如果针对某个接口你需要提供多个实现类,但又希望它们都能够被注入到 Spring IoC 容器中,你有什么办法?

答案:默认情况下,Spring IoC 容器在启动时不允许某个接口存在多个实现类,但我们可以通过 @Primary 注解来设置主实现类。另外,我们还可以使用 @Qualifier 注解来为不同的实现类命名,从而根据不同的名称来注入目标对象。


面试题 4:如果想要缩短 Spring IoC 容器的启动时间,你有什么办法?

答案:针对这个问题,解决的基本思想就是减少容器启动时所需要初始化的 Bean 的数量。我们可以从三个方面来回答这个问题,首先通过 @ComponentScan 注解来控制组件扫描的范围,其次通过合理设置 Bean 的作用域来降低大对象的创建成本,最后还可以使用延迟加载机制来控制 Bean 的初始化时机。


面试题 5:Spring Bean 的注册流程是怎么样的?

答案:Bean 的注册流程主要围绕 BeanDefinition 对象展开,包含构建 BeanDefinition、设置 BeanDefinition 属性以及注册 BeanDefinition 等步骤。这个流程涉及 ApplicationContext 和 BeanFactory 这两个 Spring IoC 容器的核心组件之间的协作和交互。


面试题 6:Spring 中 Bean 的实例化过程包含哪些核心步骤?

答案:Spring 中 Bean 的实例化过程包含三大核心步骤,即基于构造器的反射方法创建 Bean,通过属性注入实例化 Bean,以及通过回调机制扩展 Bean。Bean 的分阶段实例化过程与解决循环依赖问题也有紧密关联。


面试题 7:Spring 如何解决循环依赖问题?

答案:三级缓存机制是 Spring 用来解决循环依赖问题的基本方法。结合 Bean 实例化的生命周期,需要理解这种方法无法消除基于构造器注入的循环依赖,而只能应用于 Setter 方法注入的场景。


面试题 8:如果业务代码出现了循环依赖,有哪些应对的方案?

答案:消除循环依赖的策略有很多,其中通过调整类与类之间的协作关系可以很好地把循环依赖调整为间接依赖。在日常开发过程中,提取中介者、转移业务逻辑以及引入回调机制都是非常常见的解决方案。通常,这些方案能够运作的前提是合理地提取业务接口,并通过 Spring 的依赖注入完成对这些类的有效管理。


用户头像

加VX:bjmsb02 凭截图即可获取 2020-06-14 加入

公众号:程序员高级码农

评论

发布
暂无评论
难得可贵的Spring依赖注入实战经验,是程序员就该吸收一下!_Java_互联网架构师小马_InfoQ写作社区