ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot基础全解:从自动装配原理到项目实战避坑指南

SpringBoot基础全解:从自动装配原理到项目实战避坑指南 SpringBoot 这话题我从 1.x 时代就开始写了到现在 3.x 都出来这么久后台私信里问得最多的依然是SpringBoot 到底怎么学、 项目结构怎么搭合理这类入门问题。正好借这个机会把 SpringBoot 基础篇梳理成一套完整的内容从核心原理到实操细节一次讲透。这篇文章不整虚的产线怎么用我就怎么写。1. SpringBoot 到底是什么——从 SSM 时代的痛点说起很多新手学 SpringBoot 的第一反应是又一个新框架要学其实恰恰相反SpringBoot 的诞生是为了消灭框架。回到 2013 年前后Java 后端的主流配置是 SSHSpring Struts Hibernate或者 SSMSpring SpringMVC MyBatis每次新建项目都要写一堆 XML 配置数据源配一个、事务管理器配一个、SpringMVC 视图解析器配一个、还有一堆组件扫描、AOP 切面配置。配置文件的篇幅经常超过业务代码而且每个项目之间复制粘贴改来改去极容易出错。SpringBoot 的核心思想用一句话概括约定大于配置。它把过去需要手动写的大量配置变成默认行为你只需要在需要偏离默认规则的时候才写配置。比如过去配置一个内嵌 Tomcat 需要引入依赖、配置端口、配置连接池现在你引入spring-boot-starter-web默认端口 8080 就起来了想改端口就写一行server.port9090。这里有个很多人没想明白的关键点SpringBoot 不是一个新的编程框架它只是 Spring 框架的一种自动化装配方案。底层用的依然是 Spring 的 IOC 容器、AOP、SpringMVC 这些老伙计只是把这些东西的组装过程自动化了。所以你在 SpringBoot 里写Autowired、Service、Transactional这些注解时本质还是在写 Spring 代码。能干什么、解决什么问题也很明确快速搭建独立运行的 Spring 应用、零 XML 配置或极简配置、内嵌 Web 容器实现一键启动、通过 starter 机制简化依赖管理。适合谁来看打算入门 Java 后端的初学者、从传统 SSM 工程迁移的老开发、以及想搞清楚 SpringBoot 自动装配原理的进阶选手。这一篇把底层的逻辑捋清楚后面你学 SpringCloud、搭微服务才会不慌。2. 环境准备与第一个 SpringBoot 应用2.1 开发环境和版本选型搞 SpringBoot 第一步是选对版本。现在 SpringBoot 官方维护的是 3.x 和 2.7.x 两个大版本线3.x 要求 JDK 172.7.x 兼容 JDK 8 和 JDK 11。我的建议很直接新项目优先 3.x老项目维护看 2.7 的合规基础。网上经常看到springboot版本太高的报错本质原因是版本与 JDK 或依赖组件不匹配。比如装了 JDK 8 硬上 SpringBoot 3.2.x启动直接报UnsupportedClassVersionError这不是框架的问题是版本组合没选对。版本的配对关系记住这个表SpringBoot 版本最低 JDK内嵌 Tomcat适用场景2.7.xJDK 89.0.x老项目维护、公司强制 JDK 83.0.xJDK 1710.1.x新项目首选3.2.xJDK 1710.1.x新项目推荐3.4.xJDK 1710.1.x最新特性功能激进选版本另一个坑是 SpringCloud 和 SpringBoot 版本对应关系如果你打算后续引入微服务治理先查 SpringCloud 发版公告里支持的 Boot 版本范围两个大版本错开会导致组件加载失败。基础篇阶段先不用管 SpringCloud但心里要有这根弦。2.2 项目结构再拆解——目录不是摆设新手用 IDEA 的 Spring Initializr 生成项目后看到一堆目录容易懵。实际产线项目的基础结构一般长这样src/main/java ├── com.example.demo │ ├── DemoApplication.java # 启动类必须在根包 │ ├── controller/ # 接口层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # 数据访问层MyBatis │ ├── entity/ # 数据库实体 │ ├── dto/ # 传输对象 │ ├── config/ # 配置类 │ └── common/ # 通用工具、常量、异常 src/main/resources ├── application.yml # 主配置文件 ├── application-dev.yml # 开发环境 ├── mapper/ # MyBatis XML 文件 ├── static/ # 静态资源 └── templates/ # 模板引擎页面这里必须强调一个最常见的错误把启动类放在子包里。SpringBoot 启动时默认扫描启动类所在包及子包下的组件如果你把启动类放在com.example.admin而 Controller 放在com.example.web那 Web 层的RestController根本不会被扫描注册接口直接 404。解决方式一保证启动类位于所有类的根包解决方式二手动指定SpringBootApplication(scanBasePackages com.example)我个人更推荐后者因为包结构更灵活。2.3 启动原理——从 main 方法到内嵌 TomcatDemoApplication.java里面就一个main方法加上SpringBootApplication注解点运行之后发生了什么第一步调SpringApplication.run(DemoApplication.class, args)这里会创建 Spring 应用上下文AnnotationConfigServletWebServerApplicationContext。第二步根据你的依赖推断应用类型classpath 里有spring-boot-starter-web就启动 Web 容器没有就启动普通上下文。第三步执行自动装配逻辑加载所有META-INF/spring.factories文件里的配置类创建内嵌 Tomcat、初始化 DispatcherServlet。内嵌 Tomcat 是 SpringBoot 一个极具代表性的设计。传统 SSM 项目要把打好 war 包丢到外部 Tomcat 的 webapps 目录SpringBoot 直接把 Tomcat 作为依赖嵌进来代码里启动 Tomcat、绑定端口、部署应用一条龙完成。这也是为什么java -jar就能跑应用的原因——Tomcat 的 jar 包都在 fat jar 里内置的JarLauncher负责把它们加载起来。这里补充一个我实测过的坑如果你自定义了 Tomcat 的TomcatServletWebServerFactory去改端口、改虚拟线程等参数注意配置生效时机是在SpringApplication.run执行过程中不是 run 完之后。如果你想在 run 之前动态改端口比如从配置中心拉取得在 main 方法里先设置System.setProperty(server.port, 9090)或者在 run 之前构造SpringApplication实例后手动设置属性。3. 核心配置体系——从 application.yml 到多环境管理3.1 配置文件的优先级和加载顺序SpringBoot 的配置文件支持.properties和.yml两种格式application.yml是我最推荐的层级清晰很多。配置文件的加载顺序有一定优先级设计你要知道这个顺序因为排查问题时会发现明明配置了怎么不生效的怪事多半是优先级理解反了。默认情况下SpringBoot 按下面的优先级从高到低加载配置命令行参数java -jar app.jar --server.port8081JVM 系统属性-Dserver.port8081操作系统环境变量application-{profile}.yml指定环境application.ymlclasspath 内部的application.yml所以你遇到改配置文件没反应的问题先检查是不是有环境变量或者启动脚本里硬编码的参数把它覆盖了。我记得有一次排查了一个多小时发现是 Dockerfile 里ENV SERVER_PORT8080把配置文件里的 9090 盖掉了这种问题在云原生环境下很容易踩。3.2 YAML 配置的常用项和绑定方式一个典型的application.yml配置长这样server: port: 9090 servlet: context-path: /api spring: application: name: demo-service datasource: url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8 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 logging: level: com.example.demo.mapper: debugcontext-path这个配置很多人容易忽略它决定了所有接口的前缀。如果你配置了/api那么RequestMapping(/user)实际对外暴露的地址是/api/user。前后端联调时最常遇到的 404 问题一半以上跟这个前缀有关。配置绑定我推荐用ConfigurationProperties它能把配置文件里的结构化数据直接映射成 Java 对象。举个例子你配置了app: upload: path: /data/upload maxSize: 100MB对应的配置类Component ConfigurationProperties(prefix app.upload) Data public class UploadProperties { private String path; private DataSize maxSize; }然后在任何地方Autowired这个类就能拿到配置值。用ConfigurationProperties的好处是类型安全DataSize类型会自动把 100MB 解析成字节数避免了用Value拿字符串还要手动转换的麻烦。配合 IDEA 的 spring-boot-configuration-processor 依赖还能在写配置时有代码提示。3.3 多环境配置——dev/prod 切换的正确姿势产线上的多环境配置绝对不能靠手动改配置文件正确姿势是 profile 机制。在resources目录下放多个配置文件# application.yml 只放公共配置 spring: profiles: active: dev # application-dev.yml 放开发环境 server: port: 8080 # application-prod.yml 放生产环境 server: port: 8080启动时用--spring.profiles.activeprod覆盖默认环境或者用环境变量SPRING_PROFILES_ACTIVEprod指定。要提醒的是数据库密码、密钥这类敏感配置不要直接写进 application-prod.yml 再推到 Git 仓库。产线实践一般用环境变量注入或者配置中心Nacos、Apollo配置文件里只留占位符${DB_PASSWORD}在部署平台填实际值。4. 自动装配原理与 starter 机制4.1 自动装配到底自动了什么SpringBoot 最核心的机制就是自动装配。很多教程讲到原理就止步于SpringBoot 启动时会自动读取 spring.factories但实际原理远比这个复杂值得好好拆解。SpringBootApplication是一个组合注解它由三个注解组成SpringBootConfiguration本质上是一个Configuration标记这是一个配置类EnableAutoConfiguration自动装配的总开关ComponentScan组件扫描关键在EnableAutoConfiguration它通过Import引入了AutoConfigurationImportSelector类。这个类做的事情是读取所有 jar 包META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件SpringBoot 2.7 之前是META-INF/spring.factories拿到所有自动配置类的全限定名列表根据条件注解ConditionalOnClass、ConditionalOnMissingBean等进行过滤把符合条件的配置类导入 IOC 容器拿DataSourceAutoConfiguration举例它上面加了ConditionalOnClass({ DataSource.class })意思是只有当 classpath 里存在DataSource类时才执行这个自动配置。你引入了mybatis-spring-boot-starterclasspath 里就有数据源相关的类自动配置生效帮你创建DataSource、SqlSessionFactory。你没引入相关依赖条件不满足不执行。这套有依赖才自动配置的机制是 SpringBoot 能实现零配置启动的根本原因。它把选择权交给了依赖本身——你引入什么 starter框架就自动为你装配什么能力。4.2 条件注解——自动装配的基石条件是自动装配的决策机制常用的条件注解有注解作用ConditionalOnClassclasspath 存在指定类才生效ConditionalOnMissingClassclasspath 不存在指定类才生效ConditionalOnBean容器中存在指定 Bean 才生效ConditionalOnMissingBean容器中不存在指定 Bean 才生效ConditionalOnProperty配置文件中存在指定配置才生效ConditionalOnWebApplication是 Web 应用才生效ConditionalOnMissingBean是最值得关注的它给了开发者覆盖默认配置的机会。比如RedisAutoConfiguration里配置了一个默认的RedisTemplateString, String同时加了ConditionalOnMissingBean(name redisTemplate)。这意味着如果你在项目里自己定义了一个RedisTemplateSpringBoot 的默认配置就自动退位用你的。这种设计既保证了开箱即用又留了自定义的窗口。4.3 如何写一个自定义 starter理解自动装配之后进阶操作就是自定义 starter。产线场景很常见多个微服务都要用同一个工具包比如统一的日志切面、统一的接口签名校验直接拷贝代码是最差方案封装成 starter 才是正解。创建一个自定义 starter 的基础结构my-common-starter/ ├── pom.xml # 只做依赖管理不放业务逻辑 └── src/main/java └── com.example.common ├── CommonAutoConfiguration.java └── config/ └── CommonProperties.java src/main/resources └── META-INF └── spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.importsCommonAutoConfiguration.java内容Configuration EnableConfigurationProperties(CommonProperties.class) public class CommonAutoConfiguration { Bean ConditionalOnMissingBean public SignFilter signFilter() { return new SignFilter(); } }AutoConfiguration.imports文件内容一行一个配置类全限定名com.example.common.CommonAutoConfiguration核心点是自动配置类不加Component注解否则组件扫描会重复加载失去条件控制的意义。加载的工作完全交给AutoConfigurationImportSelector去处理。写 starter 还有一个规范问题SpringBoot 官方要求把自动配置代码放在独立的XXXAutoConfiguration模块中业务工具类放在XXX-starter模块中starter 模块只依赖自动配置模块。这样能避免自动配置类被业务代码意外扫描。小型团队可以简化但大项目尽量遵守。5. Web 开发与数据访问实践5.1 Controller 层与参数接收的细节Web 层是 SpringBoot 里用得最多但也最容易写错的层。先说参数接收的几个典型写法RestController RequestMapping(/user) public class UserController { // GET 请求路径参数 :8080/user/1 GetMapping(/{id}) public ResultUser getUser(PathVariable Long id) { return success(userService.getById(id)); } // GET 请求查询参数 :8080/user/list?page1size10 GetMapping(/list) public ResultPageResultUser list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { return success(userService.page(page, size)); } // POST 请求 JSON 参数 PostMapping public ResultUser save(RequestBody Validated UserDTO userDTO) { return success(userService.save(userDTO)); } }PathVariable和RequestParam的区别要拎清楚前者是 URL 路径的一部分后者是?后面的键值对。如果你用RequestParam去接路径参数或者反过来接口肯定报错。RequestBody是接 JSON 请求体的POST、PUT 这类有 body 的请求才用。一个容易踩的坑RequestParam接不到参数时默认直接报 400所以必须给非必传参数加defaultValue或者设置required false。前端的调用习惯是变量多个空格、空字符串、不传三种情况后端要做好兜底判断。JSON 序列化也有学问。默认使用 Jackson日期格式默认输出的是时间戳长整型跟前端对不上时要在配置里指定格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8或者更推荐的方式是在实体字段上用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)单独控制因为不同的字段格式需求不一定一样。5.2 整合 MyBatis——从依赖到满血运行SpringBoot 整合 MyBatis 的流程比 SSM 时代省了太多事。依赖只需要两个dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意mybatis-spring-boot-starter的版本要和 SpringBoot 大版本对应。SpringBoot 3.x 用 3.x 的 starterSpringBoot 2.7 用 2.3.x 的 starter混用会出现各种诡异的加载错误。Mapper 接口的写法Mapper public interface UserMapper { User selectById(Param(id) Long id); ListUser selectList(Param(keyword) String keyword); }对应的 XML 放在resources/mapper/UserMapper.xml?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper select idselectById resultTypecom.example.demo.entity.User SELECT * FROM tb_user WHERE id #{id} /select /mapper两个容易踩的坑必须说明第一Mapper注解与MapperScan的取舍。在启动类上加了MapperScan(com.example.demo.mapper)就不用在每个 Mapper 接口上写Mapper。我推荐用MapperScan少写注解、统一管理。但是注意扫描包路径别写宽了一旦扫到不该扫的包Spring 容器初始化会报找不到 Bean 的错误。第二resultType全限定名太啰嗦。配置了mybatis.type-aliases-package: com.example.demo.entity之后XML 里可以直接写resultTypeUser。或者进一步用Results注解手动映射列名到属性名。再补充一个 MyBatis 在 SpringBoot 下的缓存问题默认情况下一级缓存是开启的二级缓存是关闭的。如果你加了CacheNamespace启用了二级缓存注意实体类必须实现Serializable否则抛NotSerializableException。产线上二级缓存用的不算多因为多实例部署时缓存同步是个大问题更推荐引入独立的 Redis 做缓存层。5.3 定时任务——SpringBoot 里最容易被忽略的坑SpringBoot 内置定时任务功能用法非常简单。启动类或配置类加EnableScheduling然后业务方法上写Scheduled(cron 0 0/5 * * * ?)即可。实际使用中需要注意定时任务默认是单线程执行的这是新手最容易踩的坑。你写了三个Scheduled方法如果第一个任务执行时间长后面两个会排队等待。解决办法是自定义定时任务线程池Configuration public class ScheduleConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }cron 表达式有个秒级陷阱。0 0 12 * * ?表示每天中午 12 点执行这个是对的。但如果你写了0/1 * * * * ?表示每秒执行一次数据库压力会瞬间拉满。每次写完 cron 表达式建议用在线解析工具验证一下或者先写成fixedDelay、fixedRate这类相对时间先跑起来测试逻辑。fixedDelay和fixedRate的区别也容易被忽略。fixedRate是按照固定速率执行比如每 5 秒一次不管上一次任务有没有执行完到点就开启新任务会出现任务叠加fixedDelay是固定延迟执行上一次任务结束之后再等 5 秒执行下一次不会叠加。有状态的定时任务用fixedDelay更安全。产线上更完善的方案是引入xxl-job之类的分布式任务调度平台单机定时任务无法解决多实例重复执行的问题。基础篇先掌握内置方案心里清楚它适合单机场景即可。6. 版本、错误排查与面试高频考点6.1 版本兼容矩阵——少走三年弯路springboot版本太高这个热搜词出现频率很高原因就是乱升版本引发连锁反应。给一个稳妥的选型建议JDK 8 老项目老老实实用 SpringBoot 2.7.x。不要试图升到 3.x因为 Spring 6 和 Jakarta EE 9 的包名变更会把项目里所有javax.*的 import 都改掉工作量巨大。JDK 17 或 21 新项目直接用 SpringBoot 3.2.x 或 3.4.x。配套框架版本也尽量选兼容版本MyBatis starter 3.x、Spring Cloud 2023.x、Hutool 5.8。遇到启动报错先别慌按这个顺序排查看完整的异常栈从最底部的Caused by开始看检查 JDK 版本和 SpringBoot 版本是否匹配检查依赖树是否引入了多个版本的同一个库mvn dependency:tree检查是不是配置了多余的旧版配置类6.2 端口占用与启动失败实战启动时最常见的报错是Web server failed to start. Port 8080 was already in use.这是端口被占用。解决方式# 查看端口占用进程 lsof -i:8080 # 强制杀掉进程 kill -9 PID或者更省事的方案在application.yml把端口改掉开发环境习惯性用 8081、8082 错开能减少很多冲突。产线上端口分配应该有规范文档别随意占用。还有一类启动失败是程序自己挂的比如数据库连不上。报错信息通常是Failed to configure a DataSource这时要看你的项目里有没有引入数据库相关依赖但没配数据源。排查思路是不需要数据库的功能模块别引入 mybatis 依赖引了就要把spring.datasource配好或者用exclude DataSourceAutoConfiguration.class排除自动装配。6.3 SpringBoot 默认使用 CGLIB 代理——面试高频题springboot默认使用cglib代理是搜索热词也是面试几乎必问的考点。背后逻辑值得讲清楚。Spring 的 AOP 代理有两种实现方式JDK 动态代理基于接口代理对象是接口的实现类CGLIB 代理基于继承代理对象是目标类的子类SpringBoot 2.x 开始默认使用 CGLIB 代理spring.aop.proxy-target-classtrue是默认值。原因很简单现在很多 Service 类压根不写接口JDK 动态代理没法代理没有接口的类。这个默认行为带来的实际影响如果你的类被 CGLIB 代理类不能是 final 的目标方法不能是 private 或 final 的。CGLIB 通过生成子类来代理final 类没法被继承final 方法没法被重写。在公司代码里见过有人给 Service 类加了 final 修饰符结果Transactional和Async全部失效排查了很久才发现是这个原因。另一个经典面试问法Transactional自调用为什么失效假设同一个类里方法 A 调用方法 BB 上有Transactional你调 A 时 B 的事务不生效。原因是 Spring 的代理对象在外部调用时才生效自调用走的是 this 引用绕过了代理。解决方式是拆类或者用AopContext.currentProxy()获取代理对象调用。6.4 常用工具类与调试技巧开发 SpringBoot 时有一些实用小技巧能提升效率第一开启开发热更新。引入spring-boot-devtools依赖改完代码后 IDEA 里按CtrlF9编译应用自动重启。别把它带到产线它默认只在开发环境生效。第二Actuator 监控端点。引入spring-boot-starter-actuator后访问/actuator/health就能看到应用健康状态。产线部署时这个端点对运维特别有用配好权限暴露/health、/info就够用了。第三自定义 Banner。启动时的 Spring 图标可以用banner.txt文件替换网上有在线 banner 生成器能拼出自己的文字图案。这个纯粹是团队文化装饰有些公司会把版本号和团队名放在 banner 里每次启动有归属感。第四日志框架直接用 Logback。SpringBoot 默认集成了不用额外导入。建议配置logging: file: name: logs/app.log logback: rollingpolicy: max-file-size: 100MB max-history: 30定期滚动日志是做运维的基本功不配置的后果是日志文件无限增长最后磁盘被打满应用跟着挂掉。6.5 常见问题速查表把这些年见过的典型问题和解决方式汇总成一张表现象原因解决方式接口全部 404启动类不在根包或扫描路径不对检查SpringBootApplication包位置或配置scanBasePackages端口被占用端口冲突lsof -i:端口杀掉进程或改端口Autowired报 null类没有交给 Spring 管理加Service/Component注解或在配置类声明 Bean配置文件修改不生效配置优先级问题查环境变量、命令行参数、profile 覆盖内置 Tomcat 无法启动依赖冲突或 JDK 版本太低检查 SpringBoot 版本和 JDK 匹配度中文乱码请求或响应编码不对确认server.servlet.encoding配置数据库连接加characterEncodingutf8MyBatis XML 找不到mapper-locations 配置错了检查mybatis.mapper-locations路径是否匹配CGLIB 代理失效类或方法为 final去掉 final 修饰符这张表基本覆盖了入门阶段遇到的高频问题。遇到没见过的报错养成先看完整异常栈的习惯——最底层Caused by才是真正的原因表层异常信息经常只是症状。7. 从基础到实际项目——一个完整的整合案例讲了半天理论最后用一个实际场景把这些基础串起来。假设现在要搭一个极简的商品管理后端包含查询列表、新增商品两个接口数据存 MySQL。项目结构和核心代码src/main/java/com/example/shop ├── ShopApplication.java ├── controller/ProductController.java ├── service/ProductService.java ├── mapper/ProductMapper.java ├── entity/Product.java └── dto/ProductDTO.java启动类SpringBootApplication MapperScan(com.example.shop.mapper) public class ShopApplication { public static void main(String[] args) { SpringApplication.run(ShopApplication.class, args); } }ControllerRestController RequestMapping(/api/product) public class ProductController { Autowired private ProductService productService; GetMapping(/list) public ResultListProduct list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 20) Integer size) { return Result.success(productService.pageList(page, size)); } PostMapping public ResultLong add(RequestBody Validated ProductDTO dto) { return Result.success(productService.add(dto)); } }ServiceService public class ProductService { Autowired private ProductMapper productMapper; Transactional(rollbackFor Exception.class) public Long add(ProductDTO dto) { Product product new Product(); BeanUtils.copyProperties(dto, product); productMapper.insert(product); return product.getId(); } }Transactional(rollbackFor Exception.class)这个写法要注意默认情况下Transactional只对RuntimeException回滚如果你方法里抛的是受检异常比如IOException事务不会回滚。产线上统一加上rollbackFor Exception.class是规范做法。这个案例麻雀虽小五脏俱全。跑通它你对 SpringBoot 的 Controller 层、Service 层、Mapper 层的完整调用链以及配置、事务、参数校验这些基础能力就有了体感。我个人的建议是学 SpringBoot 不要沉迷于背面试题最好的学习路径是搭一个又一个小项目从能跑到跑得对再到跑得稳前两步靠模仿最后一步靠踩坑。SpringBoot 的自动装配、starter 机制、配置体系这些概念等你实际被坑过一次、排查过一次比看十篇原理文章都记得牢。
返回列表