介绍完Spring框架所具备的三种依赖注入类型,我们发现使用这些类型并不复杂。但不复杂并不代表开发人员能够用得好。接下来,我将和大家分享使用依赖注入的一些实战经验。
在介绍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.spring.bestpractice.controller") public class AppConfig { }
然后要讨论的是单例模式和原型模式对性能的影响。在Spring中,当把Bean范围设置为prototype时,每次请求Bean时,Spring IoC容器都会创建一个新的对象实例。所以,使用原型模式在创建过程中会对性能产生影响,对那些初始化过程需要消耗巨大资源的对象而言尤其如此,这些对象常见的有网络连接对象、数据库连接对象等。因此,对这些对象,应该完全避免使用原型模式。或者,我们应该在使用前仔细设计并对性能进行充分测试。
最后一个值得讨论的性能分析点在于Spring IoC容器的延迟加载(Lazy Loading)和预加载(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实例会非常消耗资源。我们应该根据实际情况选择具体的加载方法。如果需要尽快地加载应用程序,那么就采用延迟加载;如果需要应用程序尽快地运行并更快地为请求提供服务,那么就执行预加载。