1. 自动配置到底“自动”在哪:从 @SpringBootApplication 说起
上一节我们把 SpringBoot 项目从创建到启动跑通了,大多数人的第一反应是“原来启动一个 Web 服务这么简单”。但第二个问题马上就会冒出来:为什么我没写web.xml,没配 Tomcat,项目却能直接以 8080 端口跑起来?答案是 SpringBoot 的自动配置。
自动配置解决的核心问题,是把“Spring 容器里需要手动声明的一大堆 Bean”变成“根据依赖和配置自动注册”。你引入spring-boot-starter-web,SpringBoot 判断 classpath 里有 Spring MVC 相关类,就自动帮你配置DispatcherServlet、CharacterEncodingFilter、默认错误页面、Jackson 序列化器。你引入spring-boot-starter-data-redis,它又自动配好RedisTemplate、连接工厂和连接池参数。
1.1 真正入口并不是 main 方法
很多初学者以为main是 SpringBoot 的核心,其实main只是调用了SpringApplication.run()。真正控制自动配置的是类上的@SpringBootApplication注解,它本身是一个组合注解:
@SpringBootConfiguration:本质上就是@Configuration,表示当前类是配置类。@EnableAutoConfiguration:打开自动配置的总开关。@ComponentScan:默认扫描当前类所在包及其子包。
理解这个组合很重要。因为一旦你明白了@ComponentScan的存在,就会理解为什么“启动类一定要放在所有业务类的根包下”。放在根包下,所有@Service、@Controller、@Repository、@Component都会被扫到;放错位置,最常见的错误就是“明明写了@Service,但注入时报NoSuchBeanDefinitionException”。
1.2 自动配置的加载链路
@EnableAutoConfiguration会通过AutoConfigurationImportSelector去加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中列出的自动配置类。这是 SpringBoot 2.7 之后的机制,早期版本用的是spring.factories。
每个自动配置类上都有条件注解,比如:
@ConditionalOnClass:classpath 里存在指定类才生效。@ConditionalOnMissingBean:容器里没有指定 Bean 才生效。@ConditionalOnProperty:配置文件中存在指定属性才生效。@ConditionalOnWebApplication:当前必须是 Web 应用才生效。
RedisAutoConfiguration上写着@ConditionalOnClass(RedisOperations.class),只有你引入了 Redis 客户端依赖,这个配置类才会参与。DataSourceAutoConfiguration上写着@ConditionalOnClass(DataSource.class),没有数据库驱动,它不会创建数据源。这就是为什么“引入依赖 + 配置属性”就能用,而不需要写 Bean。
实际排查自动配置是否生效,有两个办法。
第一,启动时加--debug,控制台会输出正反两个列表:“Positive matches”是生效的自动配置,“Negative matches”是没生效的配置及原因。第二,使用 Spring Boot Actuator 的conditions端点,但生产环境不建议直接暴露这个端点。
我一般排查“自动配置没生效”时,先看依赖有没有真的引入,再看类路径里有没有需要的类和配置项,最后才怀疑自动配置类本身的问题。大多数情况不是框架坏了,而是条件不满足。
2. 常用注解的实战用法:看起来简单,用错却很难排查
SpringBoot 的核心知识点里,注解占了很大比重。很多人会背注解定义,但真正写项目时仍然踩坑,尤其是@Autowired、@Resource、@Value、@ConfigurationProperties这些注解的细节。这里按“声明 Bean、注入 Bean、读取配置、控制事务”四条线拆开讲。
2.1 声明类注解和注入注解的搭配
| 场景 | 推荐注解 | 说明 |
|---|---|---|
| 业务逻辑类 | @Service | 语义明确,表示服务层 |
| 数据访问类 | @Repository | 表示仓储层,也转换持久层异常 |
| 配置类 | @Configuration | 用于声明@Bean |
| 普通组件 | @Component | 无明确分层语义时使用 |
| 控制器 | @RestController | 等同于@Controller+@ResponseBody |
| 接口注入 | @Autowired | 按类型注入,Spring 提供 |
| 接口注入 | @Resource | 先按名称,再按类型,JDK 提供 |
| 构造函数注入 | 构造器参数 | 推荐用于必选依赖 |
@Autowired默认按类型注入。如果同一接口有多个实现类,会出现NoUniqueBeanDefinitionException。这时可以搭配@Qualifier("beanName")指定名称,或者直接用@Resource(name = "beanName")。
我建议在 SpringBoot 项目里优先使用构造器注入。原因有两点:一是依赖关系更清晰,单元测试时直接把 mock 对象传入构造函数就行;二是避免@Autowired在字段上导致“看似能注入,实际上 Bean 创建顺序有问题”。虽然 SpringBoot 2.x 以后已经解决了大部分循环依赖问题,但过度依赖字段注入会让代码可读性变差。
2.2 @Value 和 @ConfigurationProperties 怎么选
读取application.yml里的配置有两种常见方式:
@Service public class OssService { @Value("${oss.endpoint}") private String endpoint; @Value("${oss.access-key}") private String accessKey; }这种写法适合配置项很少的情况,比如只有两三个属性。但如果配置项超过五个,或者有嵌套结构,推荐用@ConfigurationProperties:
@Component @ConfigurationProperties(prefix = "oss") public class OssProperties { private String endpoint; private String accessKey; private String secretKey; private String bucketName; // getter / setter 必须写 }使用@ConfigurationProperties时,配置前缀为oss,子属性名要和字段名对应。它支持复杂嵌套、List、Map,还支持数据校验。比如:
oss: endpoint: https://oss.example.com access-key: xxx secret-key: xxx bucket-name: my-bucket allow-suffix: - jpg - png - zip自动类型转换是它最大的优点。@Value拿到的都是字符串,需要手动转类型;@ConfigurationProperties内部完成类型转换,出错时启动阶段就能发现。
2.3 事务、异步和定时任务注解
@Transactional:加在 Service 方法上,事务生效的前提是该方法被 Spring 代理对象调用。同类内部直接调用不会走代理,事务会失效。@Async:开启异步之前,必须先在某处配置类上使用@EnableAsync。@Scheduled:开启定时任务之前,需要先在启动类或配置类上添加@EnableScheduling。@EnableConfigurationProperties:让@ConfigurationProperties类注册为 Bean,很多时候可以替代@Component。
事务失效是面试和排错的高频点。常见原因有:访问修饰符不是public、方法内部this调用、异常被 catch 掉了、抛出的是检查异常而没加rollbackFor。解决事务问题,先看方法是不是公开方法,再看有没有被异常吞掉,最后再看事务管理器有没有真的配好。
3. 整合 MyBatis:从数据源配置到自动建表
搜索热词里“springboot + mybatis 当表不存在自动建表”“springboot 整合mybatis”出现次数很多,说明这是很多开发者的实际需求。SpringBoot 整合 MyBatis 并不复杂,关键是理解三个配置点:数据源、MyBatis 映射、建表策略。
3.1 基础整合步骤
第一步,引入依赖:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>如果你的 SpringBoot 版本是 3.x,推荐使用mybatis-spring-boot-starter的 3.0 版本以上,因为 Spring Boot 3 基于 Jakarta EE,依赖坐标可能不兼容。
第二步,配置数据源和 mybatis:
spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case建议打开。否则数据库字段user_name映射到实体字段userName时会全部为 null,这是新手最常见的坑。log-impl在开发环境打开,可以看到每次执行的 SQL 和参数,排错非常方便。
第三步,在启动类上加@MapperScan:
@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }也可以不在启动类上扫描,而是每个 Mapper 接口上加@Mapper,但接口多了之后,@MapperScan更省心。
3.2 表不存在时自动建表怎么处理
MyBatis 本身不做建表操作,SpringBoot 也不会自动建表。网上常见的“自动建表”方案有三种。
方案一:使用 spring.sql.init 初始化脚本。
spring: sql: init: mode: always schema-locations: classpath:db/schema.sql在src/main/resources/db/schema.sql中写:
CREATE TABLE IF NOT EXISTS user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_name VARCHAR(64) NOT NULL, age INT DEFAULT 0 );mode: always表示每次启动都执行脚本。IF NOT EXISTS可以避免重复执行报错。这个方案适合表结构简单、变化不频繁的小项目。
方案二:使用 JDBC 的 DatabaseMetaData 判断表是否存在,不存在时动态建表。
这种方案更灵活,但需要自己写代码,适合“系统首次启动时初始化业务表”的场景。思路是:通过DataSource获取数据库连接,再调用DatabaseMetaData.getTables()判断目标表是否存在,存在则跳过,不存在则执行CREATE TABLESQL。
方案三:引入 Flyway 或 Liquibase。
这是最规范的做法。Flyway 将建表脚本放到V1__init.sql、V2__update.sql这类带版本号的文件中,启动时自动检测并执行未执行的版本。
三种方案里,我推荐以 Flyway 为主。它不仅解决建表问题,还解决了“多环境表结构不一致”的问题。只要脚本命名规范,开发、测试、生产环境跑的是同一套表结构。
需要注意一点:如果你想用 JPA 的ddl-auto: update自动建表,但项目用的是 MyBatis,那是行不通的。JPA 的自动建表是 Hibernate 的功能,MyBatis 不管建表。所以不要在mybatis配置里找ddl-auto,找错了方向。
3.3 Mapper XML 的路径和命名
很多报错指向“Invalid bound statement”,本质是 Mapper 接口找到了,但对应的 XML 没被加载。排查顺序:
application.yml里mybatis.mapper-locations是否写了classpath:mapper/*.xml。src/main/resources/mapper目录下是否真的有 XML 文件。- XML 文件的 namespace 是否和 Mapper 接口全限定名一致。
- XML 中方法的 id 是否和接口方法名一致。
- 接口没有
@MapperScan或没有@Mapper时,Spring 没有为它生成代理对象。
这个报错大约有八成原因是“namespace 写错了”或“mapper-locations 没配对”。不要一上来就怀疑依赖版本,先按上面顺序过一遍。
4. 定时任务、日志和 Banner:提升项目体验的三个细节
这几个点单独看都不难,但属于开发中每天都会接触的能力。搜索热词里出现了“springboot 定时任务”“springboot banner生成器”“springboot 系统维护”“启动图形自定义”,说明大家在实际开发中很关注这类细节。
4.1 @Scheduled 的参数与线程池陷阱
在启动类上增加@EnableScheduling,然后写一个普通方法:
@Component public class ReportTask { private static final Logger log = LoggerFactory.getLogger(ReportTask.class); @Scheduled(cron = "0 0 1 * * ?") public void generateDailyReport() { log.info("开始生成日报表"); // 业务逻辑 } }cron 表达式有六个字段,顺序是“秒 分 时 日 月 周”。比如0 0 1 * * ?表示每天凌晨 1 点执行。0 */5 * * * ?表示每 5 分钟执行一次。
但有一个非常重要的细节:SpringBoot 默认的定时任务线程池只有一个线程。如果项目里有多个@Scheduled任务,其中一个长时间阻塞,其他任务全部排队等待。遇到“我的定时任务没执行”的问题时,不一定是 cron 写错,也可能是线程池被之前的任务占满了。
解决办法是自定义调度器:
@Configuration public class SchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(10); scheduler.setThreadNamePrefix("scheduled-task-"); scheduler.initialize(); taskRegistrar.setTaskScheduler(scheduler); } }把线程池大小设为 5 到 10,基本能满足大多数项目的定时任务需求。如果任务量巨大,建议拆分到独立的服务或用分布式任务调度平台,而不是在一个 SpringBoot 应用里堆大量定时任务。
4.2 日志:不要只在控制台“看日志”
SpringBoot 默认支持 Logback,你只要在application.yml里简单配置即可:
logging: level: root: info com.example.demo.mapper: debug file: name: logs/app.logcom.example.demo.mapper的日志级别设为debug后,配合 MyBatis 的StdOutImpl,开启 SQL 日志非常方便。但生产环境不要开 debug,否则磁盘写满只是时间问题。
更完整的生产级日志配置,建议使用logback-spring.xml,按天滚动、保留 30 天、按文件大小拆分、错误日志单独输出。这是一个很值得提前准备的配置,否则系统上线后查日志只能翻一天的原始文件,排查问题效率极低。
4.3 Banner 自定义和关闭
SpringBoot 启动时控制台会打印一个 ASCII Art 的 Banner。官方支持在src/main/resources/banner.txt中放入自定义图案。网上有 Banner 生成器,把文字转换成字符画,粘到banner.txt即可。
如果你觉得每次启动刷一大段图案很烦,直接禁用:
spring: main: banner-mode: off定制 Banner 对实际业务没有影响,但团队内部工具类项目、框架项目使用自定义 Banner 能标注版本号和环境信息,方便一眼确认启动的是哪个分支、哪个环境。这个属于“小细节提升运维体验”的范畴。
5. 生产环境最该关注的核心配置:多环境、Actuator 和优雅停机
一个 SpringBoot 项目从开发到上线,绝不是“本地能跑”就行。很多项目上线后出问题,都出在没有提前做好环境隔离和运行状态检查。
5.1 多环境配置
实际项目至少有三个环境:开发(dev)、测试(test)、生产(prod)。SpringBoot 原生支持多配置文件:
src/main/resources/ ├── application.yml ├── application-dev.yml ├── application-test.yml └── application-prod.yml主配置文件里写公共配置:
spring: profiles: active: dev打包或启动时指定环境:
java -jar demo.jar --spring.profiles.active=prod多环境配置最重要的作用是“隔离敏感信息”。生产环境的数据库密码、密钥不应该出现在代码仓库里,可以通过环境变量或配置中心动态注入。
常见的坑:把生产数据库连接配在application.yml,开发环境也读同一份配置,导致本地调试时连接的是生产库,误操作数据。多人协作项目建议提交application.yml时只保留公共项,环境相关配置全部拆到各自配置文件中。
5.2 Actuator:项目到底健不健康
引入 Actuator 很简单:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>然后配置:
management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always启动后访问/actuator/health,返回:
{ "status": "UP", "components": { "db": { "status": "UP" }, "diskSpace": { "status": "UP" } } }这是 Docker、K8s、云平台做健康检查最常用的端点。需要注意,生产环境不要把所有端点都暴露出去,shutdown、env、beans等端点在公网开放会有安全隐患。按需指定暴露范围最稳妥。
5.3 优雅停机与线程池处理
SpringBoot 2.3 之后支持优雅停机:
spring: application: name: demo-service server: shutdown: graceful开启后,应用收到关闭信号时,不会直接掐断正在处理的请求,而是等待现有请求处理完成或超过超时时间。对“部署时要保证不丢请求”的服务特别重要。
配合@PreDestroy可以在 Bean 销毁前释放资源,比如关闭线程池、保存临时状态、提醒队列消费者停止拉取。
6. 常见启动异常和环境问题排查参考
最后把这几类常见报错整理成清单,方便对照。
| 现象 | 优先排查方向 |
|---|---|
| 端口被占用 | server.port是否冲突;使用netstat -ano或lsof -i:8080查看占用进程 |
| 数据库连不上 | spring.datasource.url、账号密码、数据库版本、驱动类是否存在 |
| Mapper 注入失败 | @MapperScan包路径、接口是否被扫描、XML namespace |
| JSON 序列化中文乱码 | HTTP 请求、响应的编码配置;server.servlet.encoding.force |
| 自动配置不生效 | 启动时--debug看 Negative matches |
| 本地能跑,服务器不能跑 | JDK 版本、Maven 打包方式、配置文件环境是否激活 |
| 内存占用过高 | 查看maximumPoolSize、线程池配置、是否有未关闭的 IO 流 |
我遇到过一个问题:项目在 Windows 本地启动正常,上传到 Linux 服务器后却报“无法连接数据库”。查了半天,发现服务器application-prod.yml里的数据库地址写的是127.0.0.1。这个错误很基础,但排查时很容易忽略“环境隔离”这个层面。
如果你是初学者,建议准备一个「环境清单」:
- 本机 JDK 版本和项目编译版本是否一致。
- Maven 是否配置了本机仓库,依赖能否正常下载。
- 数据库版本和驱动版本是否匹配。
- 配置文件激活的环境是否正确。
- 端口是否被防火墙或安全组限制。
这套清单能帮你解决 80% 的启动问题。不要一遇到报错就怀疑 SpringBoot 框架本身,先按“配置、环境、依赖、代码”四个层次排查,效率会高很多。
7. 后续学习路径建议
如果你正在准备 SpringBoot 面试,建议围绕以下问题自测:
- 自动配置原理是什么?
@EnableAutoConfiguration如何加载配置类? @Autowired和@Resource有什么区别?- SpringBoot 如何读取配置?
@ConfigurationProperties和@Value的优缺点是什么? - 事务失效的常见场景有哪些?
- MyBatis 的 Mapper 扫描和 XML 映射流程是什么?
- 定时任务线程池为什么默认只有一线程?
- Actuator 的核心端点有哪些?生产环境如何安全暴露?
回答问题不是背口诀,而是能结合项目实例讲清楚“为什么”。能说出“默认定时任务线程池只有一个线程,所以我要配 10 个线程”的人,和只记得“定时任务要用 @Scheduled”的人,面试官判断完全不一样。
下一篇可以继续围绕 SpringBoot 的异常处理、文件上传下载、接口文档和 JWT 登录校验展开,这些是 Web 项目落地时绕不开的环节。先把这七块核心知识点吃透,再往下推进会顺畅很多。