Java 5 的推出,加上当年基于纯 Java Annotation 的依赖注入框架 Guice 的出现,使得Spring 框架及其社区也“顺应民意”,推出并持续完善了基于 Java 代码和 Annotation 元信息的依赖关系绑定描述方式,即 JavaConfig 项目。
基于 JavaConfig 方式的依赖关系绑定描述基本上映射了最早的基于 XML 的配置方式,比如:
1)表达形式层面
基于 XML 的配置方式是这样的:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- bean定义 -->
</beans>
而基于 JavaConfig 的配置方式是这样的:
@Configuration
public class MockConfiguration{
// bean定义
}
任何一个标注了 @Configuration 的 Java 类定义都是一个 JavaConfig 配置类。
2)注册 bean 定义层面
基于 XML 的配置形式是这样的:
<bean id="mockService" class="..MockServiceImpl"> ...</bean>
而基于 JavaConfig 的配置形式是这样的:
@Configuration
public class MockConfiguration {
@Bean
public MockService mockService() {
return new MockServiceImpl();
}
}
任何一个标注了 @Bean 的方法,其返回值将作为一个 bean 定义注册到 Spring 的 IoC 容器,方法名将默认成为该 bean 定义的 id。
3)表达依赖注入关系层面
为了表达 bean 与 bean 之间的依赖关系,在 XML 形式中一般是这样的:
<bean id="mockService" class="..MockServiceImpl">
<property name="dependencyService" ref="dependencyService" />
</bean>
<bean id="dependencyService" class="DependencyServiceImpl" />
而在 JavaConfig 中则是这样的:
@Configuration
public class MockConfiguration {
@Bean
public MockService mockService() {
return new MockServiceImpl(dependencyService());
}
@Bean
public DependencyService dependencyService() {
return new DependencyServiceImpl();
}
}
如果一个 bean 的定义依赖其他 bean,则直接调用对应 JavaConfig 类中依赖 bean 的创建方法就可以了。
在 JavaConfig 形式的依赖注入过程中,我们使用方法调用的形式注入依赖,如果这个方法返回的对象实例只被一个 bean 依赖注入,那也还好,如果多于一个 bean 需要依赖这个方法调用返回的对象实例,那是不是意味着我们就会创建多个同一类型的对象实例?
从代码表述的逻辑来看,直觉上应该是会创建多个同一类型的对象实例,但实际上最终结果却不是这样,依赖注入的都是同一个 Singleton 的对象实例,那这是如何做到的?
笔者一开始以为 Spring 框架会通过解析 JavaConfig 的代码结构,然后通过解析器转换加上反射等方式完成这一目的,但实际上 Spring 框架的设计和实现者采用了另一种更通用的方式,这在 Spring 的参考文档中有说明。即通过拦截配置类的方法调用来避免多次初始化同一类型对象的问题,一旦拥有拦截逻辑的子类发现当前方法没有对应的类型实例时才会去请求父类的同一方法来初始化对象实例,否则直接返回之前的对象实例。
所以,原来 Spring IoC 容器中有的特性(features)在 JavaConfig 中都可以表述,只是换了一种形式而已,而且,通过声明相应的 Java Annotation 反而“内聚”一处,变得更加简洁明了了。
那些高曝光率的 Annotation
至于 @Configuration,我想前面已经提及过了,这里不再赘述,下面我们看几个其他比较常见的 Annotation,便于为后面更好地理解 SpringBoot 框架的奥秘做准备。
1. @ComponentScan
@ComponentScan 对应 XML 配置形式中的 <context:component-scan> 元素,用于配合一些元信息 Java Annotation,比如 @Component 和 @Repository 等,将标注了这些元信息 Annotation 的 bean 定义类批量采集到 Spring 的 IoC 容器中。
我们可以通过 basePackages 等属性来细粒度地定制 @ComponentScan 自动扫描的范围,如果不指定,则默认 Spring 框架实现会从声明 @ComponentScan 所在类的 package 进行扫描。
@ComponentScan 是 SpringBoot 框架魔法得以实现的一个关键组件,大家可以重点关注,我们后面还会遇到它。
2. @PropertySource 与 @PropertySources
@PropertySource 用于从某些地方加载 *.properties 文件内容,并将其中的属性加载到 IoC 容器中,便于填充一些 bean 定义属性的占位符(placeholder),当然,这需要 PropertySourcesPlaceholderConfigurer 的配合。
如果我们使用 Java 8 或者更高版本开发,那么,我们可以并行声明多个 @PropertySource:
@Configuration
@PropertySource("classpath:1.properties")
@PropertySource("classpath:2.properties")
@PropertySource("...")
public class XConfiguration{
...
}
如果我们使用低于 Java 8 版本的 Java 开发 Spring 应用,又想声明多个 @PropertySource,则需要借助 @PropertySources 的帮助了,代码如下所示:
@PropertySources({ @PropertySource("classpath:1.properties"), @PropertySource("classpath:2.properties"), ...})
public class XConfiguration{
...
}
3. @Import 与 @ImportResource
在 XML 形式的配置中,我们通过 <import resource="XXX.xml"/> 的形式将多个分开的容器配置合到一个配置中,在 JavaConfig 形式的配置中,我们则使用 @Import 这个 Annotation 完成同样目的:
@Configuration
@Import(MockConfiguration.class)
public class XConfiguration {
...
}
@Import 只负责引入 JavaConfig 形式定义的 IoC 容器配置,如果有一些遗留的配置或者遗留系统需要以 XML 形式来配置(比如 dubbo 框架),我们依然可以通过 @ImportResource 将它们一起合并到当前 JavaConfig 配置的容器中。