应届生必问: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 版权协议,转载时请注明出处!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐