
我一直觉得Spring Boot 是 Java 后端入门最值得投入的技术栈没有之一。很多编程小白一开始就扎进 SSM、微服务这些概念里结果被配置文件和各种 XML 劝退。而 Spring Boot 的设计哲学就是“约定大于配置”它把大量的工程化细节都帮你处理好了让你能用最少的代码把业务跑起来。这篇内容我按照自己带新人时的思路把从零到能独立开发一个预约服务系统的全链路拆开讲清楚。不管你是刚毕业的学生、想转行的职场人还是已经在写代码但想系统补一补 Spring Boot 基础的同行这份指南都适合你。它不涉及花哨的架构只讲最实用、最能落地的核心内容。1. 学习路径设计为什么Spring Boot是小白入门的正确姿势1.1 Spring Boot到底解决了什么问题先说说最基础的问题Spring Boot 到底是什么。简单讲它是 Spring 框架的“一键启动器”。以前用 Spring 写一个 Web 项目你得手动导入十几个 jar 包写一堆 XML 配置来管理 Bean还要配置 Tomcat、配置数据源、配置事务管理器……一套流程走下来项目还没开始写业务人已经快被配置搞疯了。Spring Boot 把这些繁琐的步骤全部自动化了。它通过自动配置机制AutoConfiguration在项目启动时根据你引入的依赖自动帮你装配好各种组件。你引入spring-boot-starter-web它就自动帮你配好 Tomcat 和 Spring MVC你引入spring-boot-starter-data-jpa它就自动帮你配好 Hibernate 和数据源。你要做的就是写自己的业务代码。我经常跟新人打一个比方Spring 就像是买了一套精装房的毛坯版所有材料都给你了但水电、墙面、地板全要你自己动手装而 Spring Boot 是拎包入住的版本你进去就能开始摆放家具业务代码。这个差距决定了你入门的速度和信心。1.2 全链路学习路线图从Hello World到能上线的小系统很多小白学习遇挫不是不够努力而是学习路径太乱。这里我按自己带人的经验把 Spring Boot 的学习拆成五个阶段环境与基础阶段搞定 JDK、Maven、IDE 安装掌握 Java 基础语法面向对象、集合、异常处理。这一阶段目标只有一个能独立运行起来一个 Hello World 的 Spring Boot 项目。Web 开发阶段掌握 Controller、Service、Mapper 三层架构熟悉 RESTful API 设计学会使用 Postman 调试接口。目标是能写一个最简单的 CRUD 接口。数据持久化阶段学会操作数据库掌握 MyBatis-Plus 或 Spring Data JPA 的常见用法理解事务管理。目标是能做一个带数据库的完整增删改查功能。进阶功能阶段学习文件上传、异步编程、定时任务、WebSocket、拦截器与过滤器。目标是让你开发的系统能处理真实业务场景中的复杂需求。实战项目阶段独立开发一个完整的业务系统比如预约服务、商城、博客等掌握项目从设计到部署的全流程。这条路线看似内容很多但每一步都是踩在前一步的基础上。只要按部就班基本两到三个月就能达到独立开发小型系统的水平。2. 环境准备与第一个Spring Boot应用2.1 开发环境搭建JDK、Maven、IDEA三件套这一部分我不讲太多理论直接给一套经过验证的配置方案。JDK 推荐使用 1.8 或 11具体取决于你所在公司的技术栈版本。如果完全自己学习建议直接用 JDK 17配合 Spring Boot 2.7.x 或 3.x 均可但要注意 3.x 要求 JDK 17 起步。Maven 建议安装 3.8 以上版本安装后一定要修改settings.xml里的镜像地址换成阿里云镜像。这一步至关重要因为默认的中央仓库在国内下载依赖非常慢一个spring-boot-starter-web可能要卡十几分钟换成阿里云镜像后速度快好几倍。IDEA 社区版就能开发 Spring Boot 项目不需要破解旗舰版。安装完成后建议顺手装几个插件Lombok简化实体类代码、MyBatisXMyBatis 的 IDA 增强插件、RestfulTool接口调试辅助。这些插件能让开发效率提升一个档次。2.2 5分钟跑通第一个接口环境配好后第一次创建项目建议直接去 Spring Initializrstart.spring.io生成没必要在 IDEA 里一步步点。在网页上选择 Maven 项目、Java 版本、Spring Boot 版本依赖搜索时勾选Spring Web点击生成下载压缩包然后用 IDEA 打开即可。打开项目后目录结构是这样的src/main/java/com/example/demo ├── DemoApplication.java启动类 src/main/resources ├── application.properties配置文件在DemoApplication.java同级目录下创建一个controller包新建一个HelloController.javapackage com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; RestController public class HelloController { GetMapping(/hello) public String hello() { return Hello Spring Boot!; } }然后直接运行DemoApplication的main方法控制台出现Tomcat started on port(s): 8080就说明启动成功。浏览器访问http://localhost:8080/hello就能看到返回的字符串。这一步看似简单但背后发生了很多事情内嵌 Tomcat 自动启动、DispatcherServlet 自动注册、RestController和GetMapping注解被自动扫描识别。你现在不需要完全理解每个机制的细节先建立“我写的代码能运行”的正向反馈这才是最重要的。3. 核心细节解析Spring Boot的骨架与灵魂3.1 启动类与核心注解的工作机制Spring Boot 项目的核心就是启动类它上面有三个关键注解SpringBootApplication、EnableAutoConfiguration、ComponentScan实际上SpringBootApplication是一个组合注解里面包含了后两者。SpringBootConfiguration继承自Configuration表示这个类是一个配置类。EnableAutoConfiguration开启自动配置这是 Spring Boot 最核心的注解。它会去读取 META-INF/spring.factories 文件中的配置类根据你引入的依赖条件化地加载配置。ComponentScan默认扫描启动类所在包及其子包下的所有Component、Service、Repository、Controller注解的类把它们注册为 Spring 容器中的 Bean。我刚开始学的时候曾经犯过一个错误把启动类放在com.example.root包下然后 Controller 放在com.example.web.controller包下导致启动后访问接口一直 404。后来才明白启动类默认只扫描自己所在包及子包。如果你非要放在外面就得手动指定scanBasePackages。这个坑新手一定要提前知道。3.2 配置文件与多环境切换Spring Boot 的配置文件支持两种格式application.properties和application.yml。我强烈建议使用application.yml因为它的层级结构更加清晰不用写一堆spring.datasource.url这种重复前缀。举个例子配置数据源时properties 格式是spring.datasource.urljdbc:mysql://localhost:3306/demo spring.datasource.usernameroot spring.datasource.password123456 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver同样的配置YAML 格式是spring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver有没有觉得 YAML 的层次感强很多另外注意一个小细节纯数字字符串的密码在 YAML 中最好用引号包起来否则可能被解析成数字导致连接失败。多环境配置是工程化开发绕不开的需求。常见做法是创建三个文件application-dev.yml开发环境、application-test.yml测试环境、application-prod.yml生产环境然后在主配置application.yml里通过spring.profiles.activedev指定当前生效的环境。这样切换环境只需改一行配置不用改代码里的任何逻辑。3.3 分层架构与代码规范分层架构是 Java Web 开发的基础这一节内容虽然不涉及具体代码但它决定了你项目后期好不好维护。推荐的分层方式Controller 层接口层接收 HTTP 请求参数校验调用 Service 层返回统一响应体。Service 层业务层处理业务逻辑事务控制调用 Mapper 层。Mapper 层数据访问层操作数据库只负责 SQL 和数据映射不写业务逻辑。Entity 层实体层与数据库表结构对应的 Java 对象。DTO 层数据传输对象用于接口入参和出参的数据封装避免直接把 Entity 暴露给前端。很多新人刚开始写代码喜欢把业务逻辑全堆在 Controller 里一个方法几百行。当时确实是“能跑”但后续要加需求、要排查问题就非常痛苦。所以从一开始就养成分层的习惯后面项目做大了才能体会到好处。4. 实操过程从零开发一个上门预约服务系统4.1 需求梳理与数据库表设计纸上谈兵没有意义这节直接带你做一个完整的业务项目上门烹饪预约服务系统。需求是这样用户可以在系统里查看厨师信息、预约上门烹饪服务管理员可以管理厨师、查看所有预约订单。我先把核心表结构设计出来。这里用 MySQL 数据库一共三张表-- 厨师表 CREATE TABLE chef ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, name VARCHAR(50) NOT NULL COMMENT 姓名, specialty VARCHAR(100) COMMENT 擅长菜系, price DECIMAL(10,2) DEFAULT 0 COMMENT 每小时服务费用, status TINYINT DEFAULT 1 COMMENT 状态 1-available 0-unavailable, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 用户表 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 预约订单表 CREATE TABLE booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, chef_id BIGINT NOT NULL COMMENT 厨师ID, booking_date DATE NOT NULL COMMENT 预约日期, start_time TIME NOT NULL COMMENT 开始时间, hours INT DEFAULT 1 COMMENT 服务时长小时, total_price DECIMAL(10,2) COMMENT 总费用, status TINYINT DEFAULT 0 COMMENT 0-待确认 1-已确认 2-已完成 3-已取消, remark VARCHAR(255) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里有个细节要注意status字段用TINYINT类型用数字表示状态而不是直接存中文。这样设计的好处是后续在代码里可以用枚举定义状态避免前后端因为字符串不一致导致的问题而且查询效率也更高。4.2 项目初始化与核心依赖配置数据库表建好后回到 Spring Initializr 生成项目这次需要勾选这几个依赖Spring Web提供 MVC 支持MySQL Driver数据库驱动MyBatis Framework持久层框架Validation参数校验Lombok简化代码项目生成后在application.yml里配置数据源和 MyBatisserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/booking_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.booking.entity configuration: map-underscore-to-camel-case: true这里注意map-underscore-to-camel-case: true有了这个配置数据库里的create_time才能自动映射到 Java 对象里的createTime属性否则查出来全是 null排查起来很头疼。4.3 实体类与 Mapper 的编写先创建实体类Chef、User、Booking。这里用 Lombok 简化 getter/setter代码看起来清爽很多package com.example.booking.entity; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data public class Chef { private Long id; private String name; private String specialty; private BigDecimal price; private Integer status; private LocalDateTime createTime; }Mapper 接口用 MyBatis 的注解方式还是 XML 方式我的建议是简单 SQL 用注解复杂 SQL 用 XML。原因很简单注解方式的 SQL 直接写在 Java 文件里开发时不用来回切换文件但对调试不友好XML 方式可以把 SQL 集中管理适合多表关联、动态 SQL 比较多的模块。我这里以ChefMapper为例演示两种方式package com.example.booking.mapper; import com.example.booking.entity.Chef; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; Mapper public interface ChefMapper { // 注解方式 Select(SELECT * FROM chef WHERE status 1) ListChef listAvailableChefs(); // XML 方式需要 resources/mapper/ChefMapper.xml 文件配合 Chef findById(Long id); }XML 文件中对应的 SQL?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.booking.mapper.ChefMapper select idfindById resultTypecom.example.booking.entity.Chef SELECT * FROM chef WHERE id #{id} /select /mapper之所以findById用 XML 而listAvailableChefs用注解是因为我故意让你看看两种写法的差异。实际开发中保持同一个项目风格统一更好我个人偏向 XML 方式因为后期 SQL 优化、explain 调试时更方便。4.4 Service 与 Controller 实现ChefService接口和实现类package com.example.booking.service; import com.example.booking.entity.Chef; import java.util.List; public interface ChefService { ListChef listAvailableChefs(); Chef getChefById(Long id); }package com.example.booking.service.impl; import com.example.booking.entity.Chef; import com.example.booking.mapper.ChefMapper; import com.example.booking.service.ChefService; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; Service public class ChefServiceImpl implements ChefService { Resource private ChefMapper chefMapper; Override public ListChef listAvailableChefs() { return chefMapper.listAvailableChefs(); } Override public Chef getChefById(Long id) { return chefMapper.findById(id); } }Service注解表示这是业务层组件Spring 容器会自动扫描并管理它的生命周期。Resource用来注入依赖这里也可以用Autowired。两者的区别是Resource按名称装配Autowired按类型装配实际使用差别不大但Resource更安全一些能找到就用名字匹配。Controller 层的代码如下package com.example.booking.controller; import com.example.booking.entity.Chef; import com.example.booking.service.ChefService; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; RestController RequestMapping(/api/chef) public class ChefController { Resource private ChefService chefService; GetMapping(/list) public ListChef list() { return chefService.listAvailableChefs(); } GetMapping(/{id}) public Chef detail(PathVariable Long id) { return chefService.getChefById(id); } }RequestMapping(/api/chef)统一了模块前缀GetMapping(/list)和GetMapping(/{id})分别对应查询列表和查询详情。启动项目后在浏览器或 Postman 里访问http://localhost:8080/api/chef/list只要数据库 chef 表里有数据就能看到 JSON 返回结果。到这里你已经完成了一个最基础的“查询菜品/服务列表”的业务闭环浏览器发请求到 ControllerController 调 ServiceService 调 MapperMapper 查数据库结果再一层层返回来最终变成 JSON 展示在前端。这个流程是 Spring Boot 后端开发最核心的主链路后面的复杂功能都是在这个链路上叠加。4.5 预约下单功能的完整实现光有查询还不够加上预约下单这个核心流程你才能真正理解业务代码怎么写。下单逻辑比查询复杂它涉及两个表的数据校验、金额计算和状态变更还要处理并发情况下的数据一致性问题。先创建BookingMapperpackage com.example.booking.mapper; import com.example.booking.entity.Booking; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Select; import java.util.List; Mapper public interface BookingMapper { Insert(INSERT INTO booking(user_id, chef_id, booking_date, start_time, hours, total_price, status, remark) VALUES(#{userId}, #{chefId}, #{bookingDate}, #{startTime}, #{hours}, #{totalPrice}, #{status}, #{remark})) Options(useGeneratedKeys true, keyProperty id) int insert(Booking booking); Select(SELECT * FROM booking WHERE user_id #{userId}) ListBooking listByUserId(Long userId); Select(SELECT * FROM booking WHERE id #{id}) Booking findById(Long id); }Options(useGeneratedKeys true, keyProperty id)是必须要加的它保证插入成功后booking.getId()能拿到数据库自增的主键后面如果要做订单编号、推送通知都需要用到这个主键值。Service 层的下单逻辑Override Transactional public Booking createBooking(BookingVO vo) { // 1. 校验厨师是否存在且可用 Chef chef chefMapper.findById(vo.getChefId()); if (chef null || chef.getStatus() ! 1) { throw new BusinessException(厨师不存在或已下线); } // 2. 计算总价 BigDecimal totalPrice chef.getPrice().multiply(new BigDecimal(vo.getHours())); // 3. 保存订单 Booking booking new Booking(); booking.setUserId(vo.getUserId()); booking.setChefId(vo.getChefId()); booking.setBookingDate(vo.getBookingDate()); booking.setStartTime(vo.getStartTime()); booking.setHours(vo.getHours()); booking.setTotalPrice(totalPrice); booking.setStatus(0); // 待确认 booking.setRemark(vo.getRemark()); bookingMapper.insert(booking); // 4. 这里有一步关键的防重复预约检查代码见正文说明 return booking; }这里Transactional注解非常重要它保证整个方法中的所有数据库操作要么全部成功要么全部回滚。比如“检查厨师可用”和“插入订单”之间如果发生异常前面的操作也不会留下脏数据。防重复预约的检查我在代码里没写全这里单独说。数据库层面可以给(chef_id, booking_date, start_time)加唯一索引然后在插入前捕获DuplicateKeyException。这样即使两个人同时请求预约同一个时段数据库也能拦下重复数据这是最可靠的方式。代码层面的检查只能作为辅助因为在高并发下你查的时候没有记录插入的时候记录可能存在了这就是经典的竞态条件。4.6 使用Springfox生成API文档接口写好后如果一个个给前端同学讲参数效率太低。Spring Boot 集成 Swagger 生成接口文档是标配操作。但由于 Springfox 3.0.0 和新版 Spring Boot 的兼容性问题非常常见很多人在这里卡住。这里给一套在 Spring Boot 2.6 下能正常工作的方案。引入依赖dependency groupIdio.springfox/groupId artifactIdspringfox-boot-starter/artifactId version3.0.0/version /dependency然后在启动类或配置类上添加EnableOpenApi注解。如果你用的是 Spring Boot 2.6还需要在application.yml里加上这段配置否则会报Failed to start bean documentationPluginsBootstrapperspring: mvc: pathmatch: matching-strategy: ant_path_matcher原因是 Spring Boot 2.6 之后默认的路径匹配策略从 AntPathMatcher 改成了 PathPatternParser而 springfox 3.0.0 还不兼容导致启动失败。知道这个原理以后遇到其他框架的兼容问题也有排查方向。配置成功后访问http://localhost:8080/swagger-ui/就能看到你的所有接口和参数说明。在 Controller 方法的ApiOperation注解里添加接口描述前端就能直接在线调试。## 5. 进阶能力异步编程与WebSocket集成 ### 5.1 CompletableFuture实现异步非阻塞处理 预约系统做起来之后你可能会遇到一个业务场景用户下单成功后系统要发送短信通知、推送微信消息、生成电子合同这些操作都比较耗时如果同步执行下单接口的响应时间会拖到两三秒严重影响体验。这时候就需要异步编程。 Java 8 的 CompletableFuture 是处理异步编程的利器比传统 new Thread() 和 ExecutorService 写起来更优雅。先看一个最基础的用法 java Service public class NotificationService { Async public CompletableFutureVoid sendSms(String phone, String message) { // 模拟发送短信实际会调用短信服务商的API try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println(短信发送到: phone , 内容: message); return CompletableFuture.completedFuture(null); } }Async注解需要搭配 Spring Boot 启动类上的EnableAsync一起使用启动类上加了EnableAsyncSpring 容器才会创建异步代理。然后在下单逻辑里调用Resource private NotificationService notificationService; // 下单成功后 notificationService.sendSms(13800000000, 您的预约已提交请耐心等待确认);执行这个方法时它的调用方下单接口不会阻塞等待 sleep 的 500ms直接返回响应。这就是异步带来的性能提升。但这里有一个新手容易踩的坑Async注解在同类的内部方法间调用是不生效的。比如你在NotificationService里写了一个方法sendAll()调用同类里的sendSms()异步会失效因为 Spring 的代理机制只在通过代理对象调用跨类方法时才生效。解决办法是拆分为两个不同的 Bean或者自己注入自己。如果要编排多个异步任务的执行顺序CompletableFuture还提供了thenApply、thenCombine、allOf等方法。比如先发短信、再推微信最后等两个都完成后再生成合同CompletableFutureVoid smsFuture notificationService.sendSms(13800000000, 预约消息); CompletableFutureVoid wechatFuture notificationService.sendWechat(userId123, 预约消息); // 两个都完成后再执行下一步 CompletableFuture.allOf(smsFuture, wechatFuture).thenRun(() - { // 生成电子合同等逻辑 System.out.println(所有通知发送完成开始生成合同); });这种写法比之前用 CountDownLatch 手动等待简洁多了代码可读性也好很多。异步编程的核心思想就是把耗时的非关键操作拆出去不阻塞主业务链路你的系统吞吐量才能提上来。5.2 Spring Boot 2.1集成WebSocket实现实时推送预约系统还有一个高频需求用户下单后管理员在后台确认订单系统需要实时把“订单确认成功”的消息推送到用户页面。如果轮询接口既浪费资源又有延迟。这时候用 WebSocket 是最合适的。WebSocket 和 HTTP 的区别是HTTP 是“你问一句、我答一句”的问答模式WebSocket 是“建立一条长连接通道双方随时可以发数据”的通信模式。用生活化的比喻HTTP 就像打电话让对方打过来而 WebSocket 是加了个微信消息随时能发不用每次都重新建立连接。先引入依赖这里以 Spring Boot 2.1 的写法为例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency配置 WebSocket 要写一个配置类Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new OrderWebSocketHandler(), /ws/order) .addInterceptors(new HttpSessionHandshakeInterceptor()); } }核心的业务处理器public class OrderWebSocketHandler extends TextWebSocketHandler { // 使用 ConcurrentHashMap 保存在线用户的会话 private static final MapString, WebSocketSession SESSIONS new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 连接建立后把 session 保存起来 // 这里的 userId 可以从握手拦截器里获取 String userId (String) session.getAttributes().get(userId); SESSIONS.put(userId, session); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 收到客户端消息时的处理 } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { SESSIONS.values().remove(session); } // 服务端主动推送消息 public static void sendToUser(String userId, String message) { WebSocketSession session SESSIONS.get(userId); if (session ! null session.isOpen()) { session.sendMessage(new TextMessage(message)); } } }注意SESSIONS我用的是ConcurrentHashMap因为 WebSocket 的 session 会被多个线程并发访问普通 HashMap 在多线程环境下扩容时会死循环。这是并发编程中常见的教训所有会被多线程访问的集合都要考虑线程安全。下单确认后管理员调用的确认接口里调用OrderWebSocketHandler.sendToUser(userId123, 您的订单已确认)用户页面就能实时收到消息。这比前端每秒轮询一次接口要优雅得多。6. 常见问题与排查技巧实录6.1 启动失败端口被占用怎么办刚接触 Spring Boot 的新人有 80% 的概率会遇到这个报错Port 8080 was already in use。这时候很多人第一反应是改端口。改端口确实能解决问题但我们要弄清楚谁占了端口。Windows 和 Mac/Linux 的命令不一样Windows 下netstat -ano | findstr 8080MySQL 下lsof -i :8080看到进程 PID 后Windows 用taskkill /F /PID 进程号杀掉进程Mac 用kill -9 进程号。如果你不想杀进程也可以直接在application.yml里改server: port: 8081还有一种可能是端口被其他 Spring Boot 项目占用那就不是杀进程的问题了可能是你自己起了多个实例。6.2 数据库连接失败与时区报错Spring Boot 连接 MySQL 时最经典的报错是The server time zone value Öйú±ê׼ʱ¼ä is unrecognized or represents more than one time zone.这就是数据库连接 URL 里没指定时区导致的。解决方案是在连接 URL 末尾加serverTimezoneAsia/Shanghaiurl: jdbc:mysql://localhost:3306/booking_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai另外一个常见的错误是驱动类找不到或版本不兼容。Spring Boot 2.1 及以下版本默认使用com.mysql.jdbc.DriverSpring Boot 2.1 以上版本推荐使用com.mysql.cj.jdbc.Driver。如果你不知道当前驱动用哪个可以直接去掉driver-class-name配置让 Spring Boot 根据连接 URL 自动识别。6.3 springfox与Spring Boot 2.6的兼容问题如果你在 Spring Boot 2.6 以上版本集成 springfox 3.0.0启动时会报Failed to start bean documentationPluginsBootstrapper; nested exception is java.lang.NullPointerException这个坑我在 4.6 节提过一次这里再把主要原因讲透。Spring Boot 2.6 引入了新的PathPatternParser作为默认路径匹配策略springfox 内部还是用旧的AntPathMatcher导致它把项目里的所有接口路径重新解析时拿到的PathMatcher是 null自然就空指针了。解决方案是你选用其中一种把 Spring Boot 降级到 2.5.x 以下在配置文件里把匹配策略改回ant_path_matcher彻底放弃 springfox改用springdoc-openapi推荐它是基于 OpenAPI 3.0 的新方案兼容性更好如果只是学习阶段方案二最省事如果是新项目建议直接选 springdoc。6.4 修改代码后不生效热部署配置开发过程中最影响效率的问题就是改一行代码就要重启应用。花几十秒等启动还算好如果项目大了启动要两三分钟调试效率就非常低了。解决方案是引入 devtools 热部署依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId scoperuntime/scope optionaltrue/optional /dependency但注意devtools 默认只做“自动重启”不是真正意义上的“热替换”。也就是说源码改动后它会自动帮你重启应用改变了原来手动重启的方式。对于方法内部代码的修改甚至不用重启它配合 IDEA 的Build ProjectCtrlF9就能实现实时生效。有个小坑要提醒devtools 的自动重启在某些 IDE 版本上可能不触发需要确认Build project automatically已经勾选compiler.automake.allow.when.app.running为 enable。6.5 常见问题速查表问题现象可能原因解决方案访问接口 404Controller 不在启动类子包下调整包路径或配置scanBasePackages数据库中文乱码连接 URL 未指定编码加characterEncodingutf8接口返回 JSON 含 null 字段实体字段未初始化使用JsonInclude(Include.NON_NULL)配置事务不生效同类内部方法调用拆分为不同 Bean 或使用自注入Bean 注入失败包扫描不到 Service 实现类确认Service注解和包路径启动慢依赖下载慢或未配置镜像修改 Maven 镜像为阿里云页面中文乱码编码配置不完整在 yml 中配置server.servlet.encoding排查问题的思路其实有章可循先看报错信息不要急着复制粘贴搜答案。Spring Boot 的错误信息已经覆盖了大量场景耐心看完报错前几行就能定位到问题。如果实在看不懂再带着完整报错去查效率翻倍。我在实际带项目的过程中发现新人的问题很多不是出在技术上而是出在“不敢看日志”。遇到控制台一大片红字就慌了急着去搜“Spring Boot xxx error”。其实大多数启动报错从Caused by开始看一两行就能找到根因。学会看异常堆栈是编程路上值得尽早投入时间的一项技能。这个预约系统只是 Spring Boot 能力的一个缩影。等你掌握了 CRUD、配置、事务、异步、WebSocket 这些核心能力再做商城系统、内容管理系统、后台管理系统都会觉得似曾相识。编程学习就是这样一个过程学的不是框架的每一个细节而是解决问题的思维方式和套路。Spring Boot 给了你一个很好的落地平台把它吃透Java 后端的门就算真正打开了。