在《Spring Boot Hello World》中介绍了一个简单的 spring boot 例子,体验了 spring boot 中的诸多特性,其中的自动配置特性极大的简化了程序开发中的工作 (不用写一行 XML)。本文我们就来看一下 spring boot 是如何做到 自动配置 的。
首先阐明,spring boot 的自动配置是基于 spring framework 提供的特性实现的,所以在本文中,我们先介绍 spring framework 的相关特性,在了解了这些基础知识后,我们再来看 spring boot 的自动配置是如何实现的。
基于 Java 代码对 Spring 进行配置
在以往使用 spring framework 进行程序开发时,相信大家也只是使用 XML 搭配注解的方式对 spring 容器进行配置,例如在 XML 文件中使用 <context:component-scan base-package="**"/>
指定 spring 需要扫描 package 的根路径。
除了使用 XML 对 spring 进行配置,还可以使用 Java 代码执行完全相同的配置。下面我们详细看一下如何使用 Java 代码对 spring 容器进行配置,详细内容可参考这里。
使用 Java 代码进行 spring 配置,有两个核心注解 @Configuration
和@Bean
:
@Configuration
public class AppConfig {
@Bean
public SampleService sampleService() {return new SampleServiceImpl();
}
}
@Bean
注解用于修饰方法,方法的返回值会作为一个 bean 装载到 spring 容器中。bean 的 id 就是方法的名字。@Configuration
注解用于修饰一个类,它表明这个类的作用是用来对 spring 容器进行配置的。
上面的 Java 代码相当于下面的 XML 配置:
<beans>
<bean id="sampleService" class="com.**.SampleServiceImpl"/>
</beans>
使用 AnnotationConfigApplicationContext 类构建一个 spring 容器,从容器中取出对应的 bean 的测试代码如下:
public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
SampleService myService = ctx.getBean("sampleService" ,SampleService.class);
myService.doService();}
Java 代码配置 ComponentScan
使用 @ComponentScan
注解指定需要扫描 package 的根路径:
@Configuration
@ComponentScan(basePackages = "com.**.service.impl")
public class AppConfig {}
上面的 Java 代码相当于下面的 XML 配置:
<beans>
<context:component-scan base-package="com.**.service.impl"/>
</beans>
此外,AnnotationConfigApplicationContext 类还提供了 scan 方法用于指定要扫描的包路径。我们可以删除 AppConfig 类上的 @ComponentScan
注解,在构造 spring 容器时使用下面代码:
public static void main(String[] args) {AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.**.service.impl");
ctx.refresh();
SampleService myService = ctx.getBean("sampleService" ,SampleService.class);
myService.doService();}
使用 @Import 组合多个配置
将所有的 spring 配置全部放在同一个类中肯定是不合适的,这会导致那个配置类非常复杂。通常会创建多个配置类,再借助 @Import 将多个配置类组合成一个。@Import 的功能类似于 XML 中的<import/>
。
@Configuration
public class ConfigA {
@Bean
public A a() {return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {return new B();
}
}
上面的代码分别创建了两个配置类 ConfigA 和 ConfigB,它们分别定义了 a 和 b 两个 Bean。在 ConfigB 上使用 @Import
注解导入 ConfigA 的配置,此时应用代码如果加载 ConfigB 的配置,就自动也加载了 ConfigA 的配置。如下代码所示:
public static void main(String[] args) {
// 只加载 ConfigB 一个配置类,但同时也包含了 ConfigA 的配置
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
System.out.println(a);
System.out.println(b);
}
@Import 还可以同时导入多个配置类。当有多个配置类需要同时导入时,示意代码如下:
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {// return new DataSource}
}
条件注解 @Conditional
@Conditional 注解根据某一个条件是否成立来判断是否构建 Bean。借助 Condition 接口可以表示一个特定条件。例如下面代码实现了一个条件,当然这个条件始终成立:
public class SampleCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
// 如果条件成立返回 true,反之返回 false
return true;
}
}
有了表达条件的类 SampleCondition,接下来我们就可以通过 @Conditional 注解对创建 bean 的函数进行配置:
请输入代码 @Configuration
public class ConditionConfig {
// 只有当满足 SampleCondition 指定的条件时,参会构造 id 时 sampleBean 这个 bean。当然这里的条件始终成立
@Conditional(SampleCondition.class)
@Bean
public SampleBean sampleBean() {return new SampleBean();
}
}
由于 SampleCondition 的 matches 方法返回 true,表示创建 bean 的条件成立,所以 sampleBean 会被创建。如果 matches 返回 false,sampleBean 就不会被构建。
在 spring boot 中,根据这个原理提供了很多 @ConditionOnXXX 的注解,这些注解都在包 org.springframework.boot.autoconfigure.condition 下面。例如比较常见的 @ConditionalOnClass 注解,这个注解的判断逻辑是只有指定的某个类在 classpath 上存在时,判断条件才成立。@ConditionalOnClass 的具体代码如下:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {Class<?>[] value() default {};
String[] name() default {};}
@ConditionalOnClass 具体的判断逻辑可参看 OnClassCondition 类。
@SpringBootApplication 注解
介绍完前面这些基础的知识后,我们来看 Spring Boot 是如何实现自动装配的。
《Spring Boot 官方文档第 14 章》推荐在程序的 main class 上使用注解 @SpringBootApplication 对 Spring 应用进行自动配置,我们就从分析这个注解开始。下面是 @SpringBootApplication 主要代码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM,
classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {// ...
@SpringBootApplication 是一个组合注解,主要由 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan 三个注解构成。
@SpringBootConfiguration 表明被标注的类提供了 Spring Boot 应用的配置,其实这个注解与 @Configuration 注解的功能类似。
@ComponentScan 指定需要扫描 package 的路径,@SpringBootApplication 也提供了相应属性,指定需要扫描哪些 package 或不扫描哪些 package。《Spring Boot 官方文档》建议将应用的 main class 放置于整个工程的根路径,并用 @SpringBootApplication 注解修饰 main class,这样整个项目的子 package 就都会被自动扫描包含。建议的工程结构如下所示,其中 Application 就是应用的 main class。
com
+- example
+- myapplication
+- Application.java
|
+- customer
| +- Customer.java
| +- CustomerController.java
| +- CustomerService.java
| +- CustomerRepository.java
|
+- order
+- Order.java
+- OrderController.java
+- OrderService.java
+- OrderRepository.java
@EnableAutoConfiguration 是这里最重要的注解,它实现了对 Spring Boot 应用自动装配的功能。@EnableAutoConfiguration 是利用 SpringFactoriesLoader 机制加载自动装配配置的,它的配置数据在 META-INF/spring.factories 中,我们打开 spring-boot-autoconfigure jar 中的该文件,发现 EnableAutoConfiguration 对应着 N 多 XXXAutoConfiguration 配置类,我们截取几个重要的配置类如下(已经删除了很多):
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
可以看到 Spring Boot 提供了 N 多 XXXAutoConfiguration 类,有 Spring Framework 的、Web 的、redis 的、JDBC 的等等。
我们从其中选择 HttpEncodingAutoConfiguration 这个类来看下它是如何实现自动配置的:
@Configuration
@EnableConfigurationProperties(HttpProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled",
matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
private final HttpProperties.Encoding properties;
public HttpEncodingAutoConfiguration(HttpProperties properties) {this.properties = properties.getEncoding();
}
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
return filter;
}
上面代码表示,只有在满足如下条件时,才会注入 characterEncodingFilter 这个 bean:
- 只有在 WebApplication 的情况
- classpath 上必须存在 CharacterEncodingFilter 类
- 配置文件中配置了 spring.http.encoding.enabled 为 true 或者没有配置
- Spring 容器中不存在类型为 CharacterEncodingFilter 的 bean
总结
Spring Boot 自动装配的原理并不是非常复杂,其实背后的主要原理就是条件注解。
当我们使用 @EnableAutoConfiguration
注解激活自动装配时,实质对应着很多 XXXAutoConfiguration
类在执行装配工作,这些 XXXAutoConfiguration
类是在 spring-boot-autoconfigure jar 中的 META-INF/spring.factories 文件中配置好的,@EnableAutoConfiguration
通过 SpringFactoriesLoader 机制创建 XXXAutoConfiguration
这些 bean。XXXAutoConfiguration
的 bean 会依次执行并判断是否需要创建对应的 bean 注入到 Spring 容器中。
在每个 XXXAutoConfiguration
类中,都会利用多种类型的条件注解 @ConditionOnXXX
对当前的应用环境做判断,如应用程序是否为 Web 应用、classpath 路径上是否包含对应的类、Spring 容器中是否已经包含了对应类型的 bean。如果判断条件都成立,XXXAutoConfiguration
就会认为需要向 Spring 容器中注入这个 bean,否则就忽略。