主题
Spring Boot 快速上手:约定优于配置与 Starter
本文是 Spring 系统学习系列的 L1 入门篇。前置:IoC、DI、AOP:Spring 到底解决什么问题。学完可以配合面试题食用:Spring Boot 自动配置原理:@EnableAutoConfiguration 与 spring.factories、Spring 条件注解
没有 Boot 的日子:配置地狱长什么样
2014 年以前搭一个能跑的 Spring MVC 项目,业务代码一行没写,先干这些活:装 Tomcat、写 web.xml 声明 DispatcherServlet、写 spring-mvc.xml 配组件扫描和视图解析器、pom 里逐个挑 spring-web/spring-webmvc/jackson 的版本并祈祷它们互相兼容。一个 Hello World 的配置量是业务代码的好几倍,而且每个新项目都要重来一遍。
Spring Boot 的解法是"约定优于配置":你引了 web 相关依赖,它默认你是个 web 应用,内嵌 Tomcat、DispatcherServlet、Jackson 全部按约定配好;约定不合口味,用自己的配置覆盖。看一个 Boot 版 Hello World:
java
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@RestController
class HelloController {
@GetMapping("/hello")
String hello() {
return "hello";
}
}一个类跑起来一个 web 服务,没有 XML,没有 Tomcat 安装步骤。省掉的活归了三处:依赖打包归 starter,默认配置归自动配置,服务器归内嵌容器。这篇讲前两处,自动配置的完整加载链路在后面的自动配置深潜篇展开。
starter 是什么:一包依赖加一份自动配置
拆开 spring-boot-starter-web 的 pom 看,这个模块里几乎没有代码,只有一堆依赖声明:
- spring-web / spring-webmvc:MVC 框架本体
- spring-boot-starter-tomcat:内嵌 Tomcat
- spring-boot-starter-json:JSON 序列化,用的是 Jackson
- spring-boot-starter:日志(Logback)、YAML 解析、自动配置引擎
所以 starter 不是什么新格式,就是一个普通 Maven 模块,价值在两件事。
第一,版本对齐。 spring-boot-starter-parent 里维护了几百个依赖的版本号,你引 starter 不用写版本,官方保证这套组合是测过的。依赖冲突在 Boot 项目里出现的概率大幅下降,前提是你别自己去指定一堆版本号。
第二,触发自动配置。 starter 把 jar 送进 classpath,autoconfigure 模块里的配置类用条件注解检查 classpath:"有 webmvc 的类?那我把 web 环境配起来。"整条链路是这样:
mermaid
flowchart LR
A[pom 引入 starter-web] --> B[依赖进入 classpath<br/>webmvc / tomcat / jackson]
B --> C[自动配置类条件命中<br/>ConditionalOnClass]
C --> D[容器装配 Web 组件<br/>DispatcherServlet / 内嵌 Tomcat]
D --> E[应用直接处理 HTTP 请求]条件注解是这套机制的开关:@ConditionalOnClass(classpath 有某类才生效)、@ConditionalOnMissingBean(容器里没有才注册)、@ConditionalOnProperty(配置了某属性才生效)。每个自动配置类都挂着一排这样的注解,所谓"约定"就是被它们一行行写死的。哪些条件注解、各自怎么判定,在面试题篇条件注解有展开。
常用注解速查:装配你的 Bean
Boot 应用里写业务,大部分时间只跟几个注解打交道。@SpringBootApplication 点开是三个注解的组合:
- @SpringBootConfiguration:本质是 @Configuration,声明这是个配置类
- @EnableAutoConfiguration:打开自动配置的总开关
- @ComponentScan:扫描主类所在包及子包下的 @Component
这里藏着一个容易踩的约定:主类要放在根包。组件扫描默认从主类的包开始往下扫,把主类放到 com.example.util 这种深包里,你的 Service 全都扫不进来。
自己注册 Bean 有两条路:
java
// 路线一:@Configuration + @Bean,常用于第三方类
// 第三方 jar 里的类你改不了源码,加不了 @Component,只能在这声明
@Configuration
public class WebConfig {
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}java
// 路线二:@ConfigurationProperties 批量读配置
@ConfigurationProperties(prefix = "app")
public record AppProps(String name, int timeout) {}
// application.yml:
// app:
// name: order-service
// timeout: 3000@Value 和 @ConfigurationProperties 怎么选:读一两个零散值用 @Value 够了;同一个前缀下有一组配置,或者配置项超过三个,用 @ConfigurationProperties。后者有三个实际好处:松散绑定(yml 里写 app-name 也能映射到 appName 字段)、把一组配置收成一个对象而不是散装字段、配合 configuration-processor 能生成 IDE 自动提示。团队里统一用 @ConfigurationProperties 更省心,配置的读法只剩一种,review 时不用猜某个魔法字符串是哪来的。
配置体系:application.yml 与多环境
Boot 默认读 classpath 下的 application.yml(或 .properties)。多环境靠 profile 文件切:
yaml
# application.yml(公共配置)
spring:
application:
name: order-service
profiles:
active: dev # 默认激活 dev 环境
# application-dev.yml -> 本地:连本机库、开 debug 日志
# application-prod.yml -> 生产:连线上库、只留必要日志启动时用 --spring.profiles.active=prod 覆盖默认值,同一份包跑不同环境。
比"怎么配"更重要的是谁覆盖谁。同一配置项出现在多个来源时,按优先级排:
- 命令行参数(
--app.timeout=5000) - 操作系统环境变量(
APP_TIMEOUT) - application-{profile}.yml
- application.yml
这个顺序在排查"配置为什么不生效"时直接决定排查方向:先查启动命令和环境变量,再查文件。生产上真实出过这种事:环境变量里残留一个旧值,yml 里改了几遍都不生效,最后发现根本没轮到 yml 说话。完整的排查案例见生产事故排查篇。
动手实操:写一个最小 starter
理解 starter 最快的路是自己写一个。目标:做一个 greeting-spring-boot-starter,使用方引依赖、配一行 yml,就能注入 GreetingService。
目录结构:
text
greeting-spring-boot-starter/
├── pom.xml # 打成普通 jar
└── src/main/
├── java/com/example/greeting/
│ ├── GreetingService.java
│ ├── GreetingProperties.java
│ └── GreetingAutoConfiguration.java
└── resources/META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports核心三个类:
java
// GreetingService.java —— 对外提供的能力,故意不加 @Component
// (加不加由自动配置类决定,不由服务类自己决定)
public class GreetingService {
private final String prefix;
public GreetingService(String prefix) {
this.prefix = prefix;
}
public String greet(String name) {
return prefix + ", " + name;
}
}java
// GreetingProperties.java —— 把 greeting.prefix 配置收进来
@ConfigurationProperties(prefix = "greeting")
public class GreetingProperties {
private String prefix = "hello"; // 默认值就是"约定"
public String getPrefix() { return prefix; }
public void setPrefix(String prefix) { this.prefix = prefix; }
}java
// GreetingAutoConfiguration.java —— starter 的灵魂
@AutoConfiguration
@ConditionalOnClass(GreetingService.class)
@EnableConfigurationProperties(GreetingProperties.class)
public class GreetingAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 使用者没自己定义时才注册
GreetingService greetingService(GreetingProperties props) {
return new GreetingService(props.getPrefix());
}
}最后一步,告诉 Boot 去哪找这个配置类。Boot 2.7+ 写在 imports 文件里(老版本是 spring.factories,3.x 只认 imports):
text
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.greeting.GreetingAutoConfiguration使用方 mvn install 这个 starter 之后:
yaml
# 使用方的 application.yml
greeting:
prefix: 你好java
@Autowired
GreetingService greetingService;
greetingService.greet("祥哥"); // -> "你好, 祥哥"回头看 @ConditionalOnMissingBean 那一行,它就是 Boot 的设计态度:约定给默认值,但永远留覆盖的门。使用者自己定义一个 GreetingService Bean,starter 的默认实现自动让位,不冲突、不报错。
常见误区与小结
- 把 starter 当成特殊 jar 格式。它就是普通 Maven jar,特殊之处只在 META-INF 里那份 imports 声明和配好的依赖组合。
- 主类放在深包里导致 Bean 扫不进来。@ComponentScan 默认从主类所在包往下扫,主类放根包。
- @Value 和 @ConfigurationProperties 混用同一组配置。@Value 没有松散绑定,
@Value("${app-name}")读不到 app.name,两套规则别混。 - 在 profile 专属文件里设 spring.profiles.active。active 只在主文件、命令行、环境变量里生效,profile 文件里写了也不会被理会。
这一篇把"为什么有 Boot、starter 怎么运作、配置怎么组织"过了一遍,是 Spring 模块的起跑线。下一篇进容器内部看启动过程:注解和配置类怎么变成容器里的 BeanDefinition 登记记录。
参考
- Spring Boot 官方文档 - Using Spring Boot / Developing Auto-configuration
- 本地 Maven 仓库翻 spring-boot-starter-web 的 pom,比任何转述文章都直观