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

90 阅读10分钟

Spring依赖注入实战经验

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

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

把握Bean的作用域

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

如果想要通过注解来设置Bean的作用域,可以使用如代码清单2-26所示的示例代码。

代码清单2-26 设置Bean作用域示例代码

@Configuration

public class AppConfig {

@Bean

@Scope("singleton")

public HealthRecordService createHealthRecordService() {

return new HealthRecordServiceImpl();

}

}

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

代码清单2-27 通过枚举值设置作用域代码

@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注解来帮助容器做出选择,该注解的使用方式如代码清单2-28所示。

代码清单2-28 @Primary注解示例代码

@Component

public class HealthRecordServiceImplA implements HealthRecordService {

...

}

@Component

@Primary

public class HealthRecordServiceImplB implements HealthRecordService {

...

}

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

和@Primary注解的应用场景类似,@Qualifier注解为我们选择实例类进行注入提供了更加灵活的实现方式,如代码清单2-29所示。

代码清单2-29 @Qualifier注解示例代码

@Component

@Qualifier("healthRecordServiceA")

public class HealthRecordServiceImplA implements HealthRecordService {

}

@Component

@Qualifier("healthRecordServiceB")

public class HealthRecordServiceImplB implements HealthRecordService {

}

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

代码清单2-30 通过@Qualifier注解指定不同实例名称的示例代码

@Autowired

@Qualifier("healthRecordServiceB")

private HealthRecordService healthRecordService;

设置组件扫描范围

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

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

代码清单2-31 @ComponentScan注解示例代码

@Configuration

@ComponentScan(basePackages="com.spring.bestpractice")

public class AppConfig { }

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

不同配置的性能分析

在本小节中,我们将讨论不同类型的Bean配置如何影响应用程序性能,

并且我们还将讨论Bean配置的一些最佳实践。

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

代码清单2-32 在@ComponentScan注解中指定的包结构路径示例代码

@Configuration

@ComponentScan(basePackages="com.spring.bestpractice.service","com.spr

ing.bestpractice.controller")

public class AppConfig { }

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

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

代码清单2-33 @Lazy注解示例代码

@Component

@Lazy

public 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的依赖注入完成对这些类的有效管理。