应届生必问:Spring Boot 为什么不需要写 XML?一文看懂自动配置原理
应届生必问:Spring Boot 为什么不需要写 XML?一文看懂自动配置原理
告别死记硬背!手把手带你扒掉Spring Boot自动配置的"外衣",从API调用工程师蜕变为真正的开发者
前言:被"魔法"宠坏的我们
大家好,我是小李,一名刚毕业三个月的Java后端开发。说出来不怕大家笑话,在学校的时候,我是个坚定的"SSH党"——Spring、SpringMVC、Hibernate三大框架用得那叫一个溜。直到有一天,导师让我新建一个Spring Boot项目,我整个人都懵了。
事情是这样的。那天导师说:"小明啊,现在公司都用Spring Boot了,你也学一学,下午给我个Demo看看。"我心想,这有什么难的,不就是Spring的升级版吗?结果打开IDEA,新建项目,选中Spring Initializr,点点点,几秒钟一个项目就建好了。导师问我:"配置好Tomcat了吗?"我说:"啥?Tomcat?不是内置的吗?"导师微微一笑:“那你去跑起来看看。”
我怀着忐忑的心情点了运行,控制台一阵输出,然后显示Started XxxApplication in X seconds。等等,这就完了?没有web.xml?没有applicationContext.xml?没有DispatcherServlet的配置?我引入了Spring Security依赖,连密码都没配,系统就能跑了?
那一刻,我突然意识到:我好像只会用注解,背后的原理一概不知。这种感觉就像是被魔法宠坏的孩子,突然有一天魔法消失了,完全不知道该怎么办。
带着这个困惑,我开始研究Spring Boot的自动配置原理。这篇文章,就是我学习两周的总结,希望能帮助到和我一样曾经"只会用注解"的朋友们。
一、回忆往事:XML配置时代的噩梦
在开始讲解自动配置之前,让我先带大家回顾一下XML配置时代的"美好时光"——之所以打引号,是因为那段时间想起来都是泪。
1.1 一个简单的Web应用需要多少配置?
在Spring Boot出现之前,如果你要搭建一个简单的Web应用,你需要配置以下内容:
web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 配置Spring的ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 配置Spring ApplicationContext的配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
applicationContext.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">
<!-- 配置组件扫描 -->
<context:component-scan base-package="com.example.service"/>
<!-- 配置数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!-- 配置SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!-- 配置MapperScanner -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.mapper"/>
</bean>
</beans>
spring-mvc.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:mvc="http://www.springframework.org/schema/mvc"
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/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.springframework.org/schema/context">
<!-- 配置组件扫描 -->
<context:component-scan base-package="com.example.controller"/>
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- 配置静态资源处理 -->
<mvc:default-servlet-handler/>
<mvc:resources mapping="/static/**" location="/static/"/>
<!-- 配置拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.example.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
<!-- 开启MVC注解驱动 -->
<mvc:annotation-driven/>
</beans>
看着这些配置文件,我只想说:这还是"简单"配置吗?光是粘贴这些代码就要累死了!更别说还要考虑各个版本之间的兼容性问题,jar包冲突问题了。
1.2 Spring Boot带来了什么?
然后Spring Boot来了。它带来了什么?让我们看看创建一个Spring Boot项目有多简单:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, Spring Boot!";
}
}
就这么多?一个注解,一个main方法,一个Controller,就能跑起来?Tomcat呢?DispatcherServlet呢?数据源呢?
当时的我满脑子问号。后来我才知道,Spring Boot用了两个核心魔法来简化配置:
魔法一:自动配置
Spring Boot会根据你引入的依赖,自动配置相应的Bean。比如你引入了spring-boot-starter-web,它就自动配置了Tomcat、DispatcherServlet、视图解析器等等。
魔法二:约定大于配置
Spring Boot有很多默认的约定,比如配置文件叫application.properties或application.yml,放在src/main/resources目录下, Controller扫描默认是启动类所在包及其子包。
但魔法之所以是魔法,是因为我们不知道它是怎么实现的。接下来的章节,我就带大家一层一层揭开Spring Boot自动配置的神秘面纱。
二、入口分析:@SpringBootApplication的秘密
既然要研究自动配置,我们首先要找到入口。这个入口就是几乎每个Spring Boot项目都有的启动类,而启动类上面的那个注解就是关键。
2.1 @SpringBootApplication的构成
让我们先看看@SpringBootApplication注解的定义。我建议大家打开IDE,跟着我一起Ctrl+Click进去看看:
@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 {
@AliasFor("com.example.Application")
String[] value() default {};
@AliasFor("value")
String[] scanBasePackages() default {};
Class<?>[] scanBasePackageClasses() default {};
Class<? extends ExclusionFilter>[] excludeFilters() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "excludeFilters")
AutoConfigurationExcludeFilter[] autoConfigurationExcludeFilter() default {};
boolean excludeName() default {};
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "exclude")
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "excludeName")
String[] excludeName() default {};
}
从源码中我们可以发现,@SpringBootApplication其实是一个组合注解,它由三个核心注解组成:
@SpringBootConfiguration
这个注解其实就是@Configuration注解的特殊形式。在Spring Boot中,启动类本身就是一个配置类。这意味着我们可以在启动类中使用@Bean注解来定义Bean。
@ComponentScan
这是组件扫描注解,默认会扫描启动类所在包及其子包下的所有组件。这就是为什么我们的Controller、Service、Repository放在启动类包下就能被扫描到的原因。
@EnableAutoConfiguration
这个注解是自动配置的关键,也是我们接下来要重点讲解的内容。它负责根据类路径中的依赖自动配置相应的Bean。
2.2 ComponentScan的秘密
@ComponentScan注解看起来很简单,但它有一个非常重要的特性——默认扫描启动类所在包及其所有子包。
什么意思呢?假设我们的项目结构是这样的:
src/main/java
└─ com
└─ example
└─ demo
├─ DemoApplication.java # 启动类
├─ controller
│ └─ UserController.java
├─ service
│ └─ UserService.java
├─ mapper
│ └─ UserMapper.java
└─ entity
└─ User.java
因为UserController、UserService、UserMapper都在com.example.demo包及其子包下,所以@ComponentScan会自动扫描并注册这些类。这就是Spring Boot约定大于配置的体现。
如果你想让扫描范围更大,可以在@SpringBootApplication注解上指定scanBasePackages属性:
@SpringBootApplication(scanBasePackages = "com.example")
public class DemoApplication {
// ...
}
三、核心解密:@EnableAutoConfiguration的工作原理
如果说@SpringBootApplication是入口,那么@EnableAutoConfiguration就是自动配置的发动机。这个注解到底是怎么工作的?让我们一层一层剥开它。
3.1 @EnableAutoConfiguration的定义
首先,让我们看看@EnableAutoConfiguration的定义:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
这个注解上有两个关键信息:
@AutoConfigurationPackage
这个注解负责把启动类所在的包作为自动配置的包。它使用了@Import注解导入了AutoConfigurationPackages.Registrar类, Registrar会记录启动类所在的包,后续会自动配置扫描这个包下的组件。
@Import(AutoConfigurationImportSelector.class)
这个是重点中的重点!@Import注解把AutoConfigurationImportSelector类导入到Spring容器中。AutoConfigurationImportSelector实现了ImportSelector接口,它的方法selectImports的返回值就是需要自动配置的类名列表。
3.2 AutoConfigurationImportSelector的魔法
AutoConfigurationImportSelector是自动配置的核心执行者。让我们看看它的selectImports方法是怎么工作的:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 1. 判断自动配置是否开启
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 2. 获取自动配置的元数据
AutoConfigurationMetadata autoConfigurationMetadata =
AutoConfigurationMetadataLoader.loadMetadata(this.applicationContext.getClassLoader());
// 3. 获取需要自动配置的候选类名
AutoConfigurationEntry autoConfigurationEntry =
getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
// 4. 返回需要导入的类名数组
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
这个方法的核心是getAutoConfigurationEntry,它做了两件事:获取候选配置类、过滤掉不需要的配置类。
第一步:获取候选配置类
protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
// 判断是否启用自动配置
if (!isEnabled(annotationMetadata)) {
return AutoConfigurationEntry.EMPTY_ENTRY;
}
// 获取注解的属性,比如exclude、excludeName等
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 核心:获取候选配置类列表
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 去重
configurations = removeDuplicates(configurations);
// 获取需要排除的配置类
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查排除项是否合法
checkExcludedClasses(configurations, exclusions);
// 从候选列表中移除需要排除的配置类
configurations.removeAll(exclusions);
// 根据AutoConfigurationMetadata过滤配置类
configurations = filter(configurations, autoConfigurationMetadata);
// 触发自动配置导入事件
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
第二步:获取候选配置类列表
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
// 使用SpringFactoriesLoader加载META-INF/spring.factories文件
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader()
);
return configurations;
}
看!这里出现了一个关键类:SpringFactoriesLoader。这个类负责加载META-INF/spring.factories文件,这个文件就是自动配置的秘密所在!
3.3 SpringFactoriesLoader与spring.factories文件
SpringFactoriesLoader是Spring Framework提供的一个工厂加载器,它的设计理念类似于Java SPI(Service Provider Interface),但使用的是Spring自己的方式。
spring.factories文件在哪里?
这个文件位于各个starter包的META-INF目录下。比如spring-boot-autoconfigure包的路径是:
spring-boot-autoconfigure-2.7.x.RELEASE.jar
└─ META-INF
└─ spring.factories
让我们看看这个文件里有什么(截取部分内容):
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
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.condition.ConditionEvaluationReportAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration,\
org.springframework.boot.autoconfigure.http.NettyConfiguration,\
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.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.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.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.FilterRegistrationBeanAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletRegistrationBeanAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration
看到了吗?这就是spring.factories文件的全部内容。它定义了所有需要自动配置的类。当我们引入spring-boot-starter-web依赖时,Spring Boot就会从这些配置类中选择合适的来加载。
3.4 为什么不会全部生效?
看到这里,你可能会有一个疑问:既然有这么多配置类,那Spring Boot启动的时候是不是要把它们全部加载一遍?那启动速度不就很慢吗?
答案是否定的。虽然spring.factories文件里定义了上百个配置类,但真正被加载的只有少数几个。这就是@Conditional系列注解的功劳。
@ConditionalOnClass
@Configuration
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfiguration {
// 只有当类路径中存在Tomcat相关的类时,这个配置才会生效
}
这个注解的意思是:只有当类路径中存在指定类时,当前配置类才会被加载。比如HttpEncodingAutoConfiguration:
@Configuration
@ConditionalOnClass(CharacterEncodingFilter.class)
public class HttpEncodingAutoConfiguration {
// ...
}
只有引入了spring-boot-starter-web,CharacterEncodingFilter类才会在类路径中存在,HttpEncodingAutoConfiguration才会被加载。
@ConditionalOnProperty
@Configuration
@ConditionalOnProperty(prefix = "server", name = "port")
public class ServerPortAutoConfiguration {
// 只有当配置文件中存在server.port属性时,这个配置才会生效
}
这个注解的意思是:只有当配置文件中存在指定属性时,当前配置类才会被加载。
@ConditionalOnBean
@ConditionalOnBean(DataSource.class)
public class DataSourceAutoConfiguration {
// 只有当容器中存在DataSource类型的Bean时,这个配置才会生效
}
通过这些条件注解,Spring Boot实现了"按需加载"——只加载当前环境需要的配置类,不会把所有配置类都加载一遍。
四、实战演示:以WebMvcAutoConfiguration为例
光说不练假把式。让我们以WebMvcAutoConfiguration为例,看看一个自动配置类是怎么工作的。
4.1 WebMvcAutoConfiguration的源码
WebMvcAutoConfiguration是Spring MVC自动配置的核心类。当我们引入spring-boot-starter-web时,这个配置类就会被加载:
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
// 配置默认的静态资源处理器
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(CACHE_PERIOD_WEB_JARS)).setCacheControl(CacheControl.maxAge(Duration, TimeUnit)));
}
if (!registry.hasMappingForPattern("/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/**")
.addResourceLocations(getResourceLocations(getStaticLocations()))
.setCachePeriod(getSeconds(CACHE_PERIOD_STATIC)).setCacheControl(CacheControl.maxAge(Duration, TimeUnit)));
}
}
// 配置欢迎页
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
return new WelcomePageHandlerMapping(templateConfigurations.getResourceLocations(), welcomePage);
}
// 配置ICO图标
@Configuration
@ConditionalOnProperty(name = "spring.mvc.favicon.enabled", havingValue = "true", matchIfMissing = true)
public static class FaviconConfiguration implements ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
faviconRequestHandler()));
return mapping;
}
@Bean
public HttpRequestHandler faviconRequestHandler() {
SimpleUrlHandlerMapping faviconRequestHandler = new SimpleUrlRequestHandler();
faviconRequestHandler.setLocations(resources.getResources("classpath:/"));
return faviconRequestHandler;
}
}
}
4.2 自动配置属性绑定
WebMvcAutoConfiguration不仅配置了默认的组件,还会读取application.properties或application.yml中的配置。这就是为什么我们可以通过配置文件来定制Spring MVC的行为:
# application.properties
server.port=8080
server.servlet.context-path=/api
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/
这些配置是如何绑定到自动配置类中的呢?答案是@EnableConfigurationProperties注解和@ConfigurationProperties注解。
@Configuration
@EnableConfigurationProperties(WebMvcProperties.class)
public static class WebMvcAutoConfigurationAdapterConfiguration {
// ...
}
WebMvcProperties类定义了所有与Web MVC相关的配置属性:
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
private String staticPathPattern = "/**";
private Locale locale;
private View view;
private MessageCodes.Format messageCodesFormat;
// 省略getter和setter
}
通过这种方式,application.properties中的spring.mvc.*配置会自动绑定到WebMvcProperties对象中,然后在WebMvcAutoConfiguration中使用这些配置。
五、自定义自动配置:如何编写自己的Starter
学会了自动配置的原理,我们就可以尝试自己写一个自动配置模块了。这对于在公司内部封装公共组件非常有用。
5.1 创建自动配置模块
首先,创建一个普通的Maven项目,引入必要的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!-- 引入Spring Boot autoconfigurer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.7.18</version>
</dependency>
<!-- 引入配置属性处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.7.18</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>
5.2 定义配置属性类
package com.example.autoconfigure.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "my.hello")
public class MyHelloProperties {
private String prefix = "Hello";
private String suffix = "";
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
5.3 定义服务类
package com.example.autoconfigure.service;
public class MyHelloService {
private String prefix;
private String suffix;
public MyHelloService(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
public String sayHello(String name) {
return prefix + ", " + name + suffix;
}
}
5.4 定义自动配置类
package com.example.autoconfigure;
import com.example.autoconfigure.properties.MyHelloProperties;
import com.example.autoconfigure.service.MyHelloService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass(MyHelloService.class)
@EnableConfigurationProperties(MyHelloProperties.class)
@ConditionalOnProperty(prefix = "my.hello", havingValue = "enabled", matchIfMissing = false)
public class MyHelloAutoConfiguration {
private final MyHelloProperties properties;
public MyHelloAutoConfiguration(MyHelloProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(MyHelloService.class)
public MyHelloService myHelloService() {
return new MyHelloService(properties.getPrefix(), properties.getSuffix());
}
}
5.5 配置spring.factories文件
在src/main/resources/META-INF/目录下创建spring.factories文件:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfigure.MyHelloAutoConfiguration
5.6 使用自定义Starter
在其他项目中引入我们自定义的Starter,就可以直接使用了:
# application.properties
my.hello.enabled=true
my.hello.prefix=你好
my.hello.suffix=!
@RestController
public class HelloController {
@Autowired
private MyHelloService myHelloService;
@GetMapping("/hello")
public String hello(@RequestParam String name) {
return myHelloService.sayHello(name);
}
}
访问http://localhost:8080/hello?name=小明,返回"你好,小明!"。整个过程不需要任何额外的配置,这就是自动配置的魅力!
六、总结与面试题
6.1 自动配置原理总结
让我们用一张图来总结Spring Boot自动配置的全过程:
用户运行main方法
↓
SpringApplication.run()
↓
@SpringBootApplication注解生效
↓
@ComponentScan扫描组件(默认扫描启动类所在包及其子包)
↓
@EnableAutoConfiguration注解生效
↓
@Import导入AutoConfigurationImportSelector
↓
AutoConfigurationImportSelector调用SpringFactoriesLoader
↓
加载META-INF/spring.factories文件中的配置类
↓
根据@Conditional系列注解进行条件过滤
↓
加载符合条件的自动配置类
↓
注册相应的Bean到容器中
↓
应用启动完成
简单来说,Spring Boot自动配置的核心流程就是:启动类注解→导入选择器→加载工厂文件→条件过滤→注册Bean。
6.2 常见面试题
面试题一:请简单说一下Spring Boot自动配置原理?
标准答案:Spring Boot自动配置的核心是@EnableAutoConfiguration注解。这个注解通过@Import导入了AutoConfigurationImportSelector类,该类使用SpringFactoriesLoader加载classpath下META-INF/spring.factories文件中的配置类。然后根据@Conditional系列注解进行条件过滤,只加载符合当前环境条件的配置类。最后这些配置类会向容器中注入相应的Bean。
面试题二:@SpringBootApplication注解由哪些注解组成?
答案:由@SpringBootConfiguration(本质是@Configuration)、@ComponentScan(组件扫描)和@EnableAutoConfiguration(开启自动配置)三个注解组成。
面试题三:spring.factories文件的作用是什么?
答案:spring.factories文件是Spring Boot的SPI配置文件,它定义了所有需要自动配置的类。当Spring Boot启动时,会读取这个文件中的配置类列表,然后根据条件判断哪些配置类需要被加载。
面试题四:如何排除某个自动配置类?
答案:有三种方式。第一种是在@SpringBootApplication注解上使用exclude属性:@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。第二种是在application.properties中配置:spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration。第三种是自定义starter时,不引入相关依赖。
七、后记:从API调用工程师到真正的开发者
写完这篇文章,我不禁想起几个月前那个只会写@Controller、@Service注解的自己。那时候的我,看代码只看到注解,背后的实现一概不知。遇到问题只会百度粘贴,完全不知道为什么这样能work。
学习自动配置原理的过程中,我最大的收获是:知道了"为什么"比知道"怎么做"更重要。
以前看到@EnableAutoConfiguration注解,觉得它就是魔法,不知道它是怎么工作的。现在知道了它背后的SpringFactoriesLoader、spring.factories、@Conditional等机制,突然觉得Spring Boot变得"可理解"了。
更重要的是,这种"知其然也知其所以然"的学习方式,让我在遇到问题时有了更多的思路。比如当某个自动配置没有生效时,我知道应该检查spring.factories文件、应该检查@Conditional条件是否满足、应该查看日志中的自动配置报告。
这篇文章断断续续写了两个星期,期间查阅了大量的源码和资料。如果这篇文章对你有帮助,希望你能点赞、收藏、关注!你们的支持是我继续创作的最大动力!
我们下篇文章见!
作者:码到三十五
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载时请注明出处!
更多推荐

所有评论(0)