
这两年问“怎么在IDEA里把一个 SpringBoot 加 MyBatis 加 MySQL 的工程跑起来”的人特别多。很多初学者卡在第一步代码会写了但工程不会搭或者照着教程把配置抄下来一启动就是一堆报错。我自己带过不少新人也反复新建过这类工程今天干脆把完整的搭建思路和实操过程整理出来。这篇文章会从技术选型开始讲落到底层的版本匹配、配置细节、代码分层和常见报错排查适合刚学完 Java 基础准备做第一个完整后端项目的人也适合已经能写点代码但始终没搞懂工程是怎么组织起来的朋友。看完你会有自己的一个“套娃模板”以后新建项目不用再查半天资料。1. 项目整体设计与方案选型1.1 这套技术组合到底解决什么问题先明确一件事我们搭建的是后端工程核心职责是接收前端或接口调用方的请求去数据库里查数据、写数据再把结果返回。这个过程拆开看有三块需求对外提供接口处理业务逻辑操作数据库。SpringBoot 解决的是“快速起一个 Web 服务”的问题。它把传统 Spring 那套繁琐的 XML 配置简化成了自动配置内嵌了 Tomcat一个启动类就能把服务跑起来。你不需要再去部署一个外部容器也不用记一堆 web.xml 配置。MyBatis 解决的是“数据库访问”的问题。它是一个半自动的持久层框架SQL 由你自己写执行映射和结果封装由框架完成。相对于纯 JDBC它省去了手动获取连接、预编译、遍历结果集的模板代码相对于 JPA它又把 SQL 控制权完全交还给你。后者对复杂查询尤其重要。我们项目中多表关联、条件查询、分页这种东西一旦多起来SQL 写在自己手里优化起来心里才有底。MySQL 负责最终的数据落地。它免费、轻量、部署简单个人开发和中小型项目都足够用。既然是入门阶段选 MySQL 最不容易被环境问题卡住。整套工程的价值在于通过 SpringBoot 的自动装配把 MyBatis 和数据库连接都接入统一生命周期。你会看到 Bean 的创建和注入、事务管理、请求分发、SQL 执行日志这些环节被一条链路串起来。把这个链路走通一遍你对后端工程的整体认知就算建立了。1.2 版本匹配与依赖清单这一环节最容易踩坑很多人项目跑不起来根本不是代码问题而是版本不兼容。我从实践里总结出一套推荐组合JDK 17、SpringBoot 3.2.x、MyBatis Starter 3.0.x、MySQL Connector/J 8.x。SpringBoot 3 起了一个很大的变化Java EE 迁移到了 Jakarta EE所以传统的javax.*包都变成了jakarta.*。这意味着如果你之前习惯看 SpringBoot 2 的教程代码里的javax.persistence、javax.servlet等包名都需要换。MyBatis Starter 3.0.x 专门适配了 SpringBoot 3这个版本号不要乱配。如果你用的是 SpringBoot 2.7.x那对应的 MyBatis Starter 应该是 2.3.x。这两组千万别搞混。MySQL 驱动包也有变化。新版的坐标是com.mysql:mysql-connector-j老版本很多教程用的是mysql:mysql-connector-java。两者虽然指向同一个驱动但新坐标在 Maven 中央仓库里更规范推荐直接使用新坐标而且在 SpringBoot 3 的依赖管理下可以不写版本号由父工程统一管理。下面这份 pom.xml 是我验证过的完整依赖清单直接放在项目里就能用parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency 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 dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies这里有个小细节SpringBoot 的父工程已经帮你管理了大量依赖版本所以你才不需要写mysql-connector-j的版本号。但 MyBatis Starter 不在 SpringBoot 的管辖范围内必须手动指定版本。我在实际项目里见过有人把 MyBatis Starter 版本号省略结果 Maven 抛“找不到依赖”的错误其实就是这个原因。1.3 为什么推荐手动创建 Maven 工程而不是全靠脚手架初学者第一眼会看到 IDEA 里通过 Spring Initializr 可以快速生成工程一个页面勾选依赖项目结构直接给你建好确实方便。我建议新手至少手动搭一次 Maven 工程原因很朴素只有手动搭过一次你才知道那些自动生成出来的目录和配置是怎么来的。手动创建的好处是每一步都在控制范围内。从选择 Maven 还是 Gradle到写 groupId、artifactId再到自己动手加依赖、写配置出了问题你能快速定位。自动生成虽然快但屏蔽了太多底层东西。等你以后遇到“为什么这个依赖没有生效”这类问题对目录结构和依赖加载有直觉排查起来会快很多。而且 Spring Initializr 有一个实际问题它按默认模版生成的包名和类名都比较长很多人不改就用或者改的时候漏掉了某些引用。手动建项目虽然多花五分钟但每个文件的位置、每个包的命名都是自己定的后面写代码会顺手得多。2. 在 IDEA 中从零搭建工程骨架2.1 新建工程的基本流程与目录规划打开 IDEA选择 New Project左侧选择 Maven不勾选任何 Archetype。这里注意不要选 maven-archetype-webapp因为 SpringBoot 工程自带内嵌容器不需要传统 Web 工程的webapp目录。选空白 Maven 工程即可。groupId 一般写成你自己的域名倒序比如com.example。artifactId 是工程名比如demo。工程名建议全小写字母不要带中文也不要带空格。包名建议跟随 groupId 加工程名形成com.example.demo这个主包。以后所有代码都放在这个主包底下按分层细分 package。工程创建完成后目录结构我会手动调整为这样demo ├── src │ ├── main │ │ ├── java │ │ │ └── com/example/demo │ │ │ ├── controller │ │ │ ├── service │ │ │ ├── mapper │ │ │ └── entity │ │ └── resources │ │ ├── mapper │ │ └── application.yml │ └── test │ └── java └── pom.xmlcontroller放接收请求的接口类service放业务逻辑mapper放 MyBatis 的接口entity放实体对象resources/mapper放 MyBatis 的 XML 文件。很多人会把 XML 和接口放在同一个包下那也不是不行但要用到pom.xml额外的资源配置。我习惯放在 resources 里因为这样打出来的 jar 包会自动包含它们不会出现“运行测试没问题打 jar 后 XML 丢失”的怪问题。在编写第一个 Java 类之前先确认项目里的 Maven 版本可用。你可以在 IDEA 的 Settings 里指定一个本地 Maven也可以直接用 IDEA 内置的 Maven。关键是确认它用的是配置文件里的镜像源不然首次下载依赖速度会很难受。镜像配置属于地域性优化我这边不展开讲但建议你确认一下settings.xml的配置是否正常。2.2 配置文件的写法与参数说明SpringBoot 的配置文件放在src/main/resources/application.yml。YAML 格式比 properties 更清晰嵌套结构一眼能看懂。唯一要注意的是缩进YAML 对缩进非常敏感必须用空格而不是 Tab否则启动时大概率报解析错误。下面是我项目里的标准配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 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.StdOutImpl逐项拆解。driver-class-name在 MySQL 8 以后固定是com.mysql.cj.jdbc.Driver老驱动com.mysql.jdbc.Driver已经不建议再用。URL 里的参数有三个值得解释characterEncodingutf8是让数据库读写中文不乱码serverTimezoneAsia/Shanghai是解决某些环境下 MySQL 驱动因系统时区不一致报错的问题useSSLfalse是关闭本地开发时的 SSL 握手提升连接速度。mybatis.mapper-locations需要注意类路径下的classpath:mapper/*.xml表示读取src/main/resources/mapper目录下所有 XML 文件。你要是把 XML 文件名写错或者路径配错启动后调用接口会报 “Invalid bound statement” 之类的问题。map-underscore-to-camel-case很实用数据库字段create_time可以自动映射到实体属性createTime省掉一堆人工映射配置。log-impl配成StdOutImpl后MyBatis 执行 SQL 时会把 SQL 语句和参数直接打印到控制台这对调试简直不要太舒服。2.3 启动类与包扫描的正确姿势主启动类是最先要写的一个类它有一个关键效果让整个工程在启动时扫描到应该被管理的 Bean。package com.example.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; MapperScan(com.example.demo.mapper) SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }有人会问MapperScan和SpringBootApplication里的扫描范围是不是重复了两者分工不同。SpringBootApplication扫描的是它所在包及子包下的注解组件比如Service、Controller、Component。但 MyBatis 的 Mapper 是接口它不是普通的 BeanSpring 默认扫描接口不会代理。MapperScan的作用就是告诉 MyBatis把指定包下的所有接口都注册成 MyBatis 的 Mapper Bean。这也是一个容易被忽略的坑主启动类如果不在包的顶层很容易扫描不到 service、controller 等组件所以启动类一定要放在com.example.demo这个主包根目录下。启动类写好之后可以先把空的 controller 写出来测试基本链路。但更完整的验证需要等数据库表、Mapper、Service 全都齐了再走一遍。3. MyBatis 核心代码实现与数据链路打通3.1 建库建表与数据库连接验证在写代码之前先保证数据库端没问题。我用 MySQL 8 做示范。先在命令行或者数据库客户端里执行下面的建库建表语句CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE demo; CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL, email VARCHAR(128) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4; INSERT INTO user (name, email) VALUES (测试用户A, test_aexample.com); INSERT INTO user (name, email) VALUES (测试用户B, test_bexample.com);我把库表名、字段名都挪到反引号里是因为像user这种词在一些 MySQL 版本或后续业务扩展中可能跟系统表产生歧义。从第一天就用反引号包起来能减少很多不必要的困扰。字符集选utf8mb4不是utf8。utf8mb4是真正的完整 UTF-8能存四字节字符比如 emoji。utf8在 MySQL 里实际上是三字节的utf8mb3存不了部分生僻字和 emoji。现在新项目基本都统一用utf8mb4。建完表后可以在 IDEA 的 Database 面板里连接一次确认账号密码和 URL 没问题。注意 IDEA 的 Database 面板是开发工具跟后端运行时是两套独立连接但你在这里如果能连通说明数据库服务和账号密码一定没问题后面排查起来思路就清晰了。3.2 实体类、Mapper 接口和 XML 三层对应关系实体类负责把数据库表结构映射成 Java 对象。字段名采用驼峰命名框架会自动做驼峰和下划线的转换package com.example.demo.entity; import java.time.LocalDateTime; public class User { private Long id; private String name; private String email; private LocalDateTime createTime; public Long getId() { return id; } public void setId(Long id) { this.id id; } public String getName() { return name; } public void setName(String name) { this.name name; } public String getEmail() { return email; } public void setEmail(String email) { this.email email; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime createTime; } }我在这里没有引入 Lombok因为新手阶段先手写 getter/setter 更容易理解对象结构。等工程熟练了再引入 Lombok用Data注解一行搞定那是后话。Mapper 接口定义方法签名不写实现类package com.example.demo.mapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Param; import java.util.List; public interface UserMapper { ListUser findAll(); User findById(Param(id) Long id); int insert(User user); int update(User user); int deleteById(Param(id) Long id); }XML 文件实现 SQL。先提醒最重要的一个规则XML 的 namespace 必须等于 Mapper 接口的全限定名每个语句的 id 必须等于接口方法名。只要有一处对不上运行时就会报 BindingException。这是个靠细心才能避开的坑报错信息虽然写得很直白但新手往往盯着代码看了半天也找不到问题在哪。?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN https://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper select idfindAll resultTypecom.example.demo.entity.User SELECT id, name, email, create_time FROM user ORDER BY id DESC /select select idfindById resultTypecom.example.demo.entity.User SELECT id, name, email, create_time FROM user WHERE id #{id} /select insert idinsert parameterTypecom.example.demo.entity.User useGeneratedKeystrue keyPropertyid INSERT INTO user (name, email) VALUES (#{name}, #{email}) /insert update idupdate parameterTypecom.example.demo.entity.User UPDATE user SET name #{name}, email #{email} WHERE id #{id} /update delete iddeleteById DELETE FROM user WHERE id #{id} /delete /mapper这里有两个语法细节值得展开。第一#{id}是预编译占位符MyBatis 会把它转成 JDBC 的?再由数据库做参数绑定这个过程能有效防止 SQL 注入。实际开发中能用#{}就用#{}不要图方便去用${}。后者是字符串拼接虽然某些场景下动态排序字段只能用${}但新手阶段绝大多数需求用不到。第二useGeneratedKeystrue keyPropertyid的作用是执行 INSERT 后把数据库生成的自增主键回填到传入的 User 对象里。如果你插入后想立刻拿到新记录的 id比如做关联操作这段配置必不可少。不写的话插入成功但你拿不到 id。resultType我写的是全限定类名com.example.demo.entity.User。因为我在application.yml里配了type-aliases-package所以这里也可以简写成User。为了明确可读性全限定名更稳妥至少不会让人产生歧义。3.3 Service 层与 Controller 层接口串联和事务管理Mapper 只是数据访问层Controller 也不应该直接调用 Mapper。中间加一个 Service 层是为了把业务规则、数据组装、事务控制放到一个独立的位置。这是三层架构的基本划分。package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; Service public class UserService { private final UserMapper userMapper; public UserService(UserMapper userMapper) { this.userMapper userMapper; } public ListUser list() { return userMapper.findAll(); } public User getById(Long id) { return userMapper.findById(id); } Transactional(rollbackFor Exception.class) public void create(User user) { userMapper.insert(user); } }构造器注入是我一直推荐的方式。字段直接用Autowired是省事但构造器注入的好处是依赖关系显式化测试时方便替换依赖。Spring 官方也更推荐这一种。Transactional(rollbackFor Exception.class)表示当方法内抛出任何异常时事务回滚。为什么需要显式写rollbackFor因为 Spring 默认只回滚 RuntimeException如果方法里抛的是受检异常默认不会回滚。写明确切异常类型以后数据库操作的一致性才有保障。一个典型场景是批量插入数据第一条成功第二条因为唯一约束失败如果你没有事务第一条就单独提交了数据就脏了。Controller 层负责接 HTTP 请求package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/user) public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } GetMapping(/list) public ListUser list() { return userService.list(); } GetMapping(/{id}) public User getById(PathVariable Long id) { return userService.getById(id); } PostMapping public User create(RequestBody User user) { userService.create(user); return user; } }RestController意味着所有接口方法的返回值会自动序列化成 JSON 返回给前端不需要再手动处理 response。RequestBody会把 POST 请求里的 JSON 内容反序列化成 User 对象。自己写接口测试时请求体里的字段名必须和实体类字段名一致比如name、email。写到这里整条链路就已经通了HTTP 请求进 ControllerController 调用 ServiceService 调 MapperMapper 通过 MyBatis 执行 SQL 操作 MySQL结果原路返回。我建议新手在每层各打印一条日志或者利用 MyBatis 的StdOutImpl看 SQL这样能直观看到调用顺序。4. 调试、验证与常见问题速查4.1 首次启动验证从控制台到接口的全链路走查代码写完后启动 DemoApplication。正常启动时控制台会看到 Spring Boot 的 ASCII 图形、内嵌 Tomcat 启动的日志、端口监听信息以及 MyBatis 初始化 Mapper 的日志。启动之后不要着急写前端先用简单工具验证接口。我习惯先在浏览器访问http://localhost:8080/api/user/list。如果返回 JSON 数组说明整条链路通了。如果返回错误页就需要关注控制台日志。排查顺序有讲究。第一步先看服务能不能正常启动启动失败多半是配置、依赖版本或端口占用问题。第二步再看接口访问这一层出问题多在 Mapper 或 SQL。第三步看数据结果如果是空数组但没报错可能是表里没数据或者 SQL 的表名、字段名写错。不要一上来就乱试按顺序查能省下很多时间。为了验证插入接口可以打开命令行用 curl 发一个 POST 请求curl -X POST http://localhost:8080/api/user \ -H Content-Type: application/json \ -d {name:测试用户C,email:test_cexample.com}返回体里如果带上了新生成的 id说明useGeneratedKeys生效了。再去 MySQL 里查询一下数据确认数据确实落库。整个过程下来你对“接口调用链路”和“数据库持久化”的认知会比看十遍文档都清晰。想做到更好的调试效果可以直接在 Controller 方法里打断点用 IDEA 的 Debug 模式启动。断点打在 Service 层的create方法上会看到完整的调用栈。这一步能帮你理解控制反转和依赖注入在运行期是如何协作的。4.2 高频踩坑记录与解决方案我按实战中遇见的频率整理一个排错表。这套排错表同样适用于你以后搭建其他类似工程。现象原因解决方案启动报Access denied for user rootlocalhost数据库密码写错或者账号不对检查 application.yml 中的 username 和 password先去数据库客户端验证一次启动报Failed to configure a DataSource没有正确读取到数据源配置检查spring.datasource配置是否在 yml 中的正确层级缩进是否错误ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动依赖缺失或驱动类名写错确认 pom.xml 中 mysql 驱动依赖存在确认 driver-class-name 写法调用接口报Invalid bound statement (not found)XML namespace 或 id 与 Mapper 接口不匹配逐字比对 XML 的 namespace 和接口的全限定名确认mapper-locations路径正确查询结果里createTime为 null数据库字段下划线未映射到驼峰确认配置map-underscore-to-camel-case: true中文显示乱码数据库连接字符集没有指定URL 上增加characterEncodingutf8确认建库字符集为 utf8mb4接口返回 404启动类包路径不对把启动类放到主包根目录确保能扫描到 controller端口被占用被其他进程占用了 8080换个端口或者查找占用进程并结束这里重点讲讲Invalid bound statement这个坑。它算是集齐了“配置、命名、路径”三个因素的综合题。我第一次遇到时也很懵明明接口方法写了XML 文件也写了为什么 MyBatis 说找不到后来才发现是mapper-locations配置成了classpath:mapper/*.xml但 XML 文件放在了别的目录。建议先确认文件系统里 XML 的实际位置再回去看配置别急着改代码。再说createTime为 null 的情况。MyBatis 默认是不做下划线到驼峰映射的如果不开启map-underscore-to-camel-case那create_time查询出来后MyBatis 尝试在实体里找create_time属性是找不到的结果就是 null。很多人第一反应是加JsonProperty或者在 XML 里做AS createTime这些方案不是不行但全局配置解决才是最优雅的。数据库连接丢失或闲置断开也是一个常见问题。本地开发还好一旦部署到服务器长时间没有请求导致连接池里的连接被数据库回收下一个请求就会报Communications link failure。解决方式是配置连接池的validationQuery和空闲检测参数。不过这是进阶话题新手阶段先把基础链路跑通更重要。4.3 从工程骨架到一个能持续迭代的项目这个工程虽然看起来简单但它已经具备了一个后端项目的完整骨架。在此基础上继续扩展方向非常明确加统一返回体结构加全局异常处理加分页插件加 Redis 缓存。每一个方向都建立在这条已经跑通的数据链路上。我要特别提醒一点不要在一开始就堆太多中间件。有人看到网上说“项目里要有 Redis、要有 MQ、要有分库分表”于是从第一天就把这些全加上。结果不是 Redis 连不上就是各种依赖冲突镜花水月。我把这套骨架先跑通再逐步加东西每一步都验证过了再往前走这才是工程化思维。回到 IDEA 本身你还可以把工程模板做成自定义模板下次新建项目时直接复用。IDEA 的工程设置里可以保存模板也可以把自己常用的配置、依赖清单整理成一份笔记需要时按图索骥。我的经验是不要相信记忆力要把配置细节写下来因为半年后你再新建一个项目时大概率已经忘光了。最后补充一个很实用但容易被忽略的建议把 MySQL 的库表设计也纳入版本管理。很多人只把代码放入仓库数据库结构全靠手动导 SQL时间一长就没人记得表结构是哪些脚本建出来的。我自己会建一个sql/目录里面放建库建表脚本和初始数据脚本这样新同事拉下代码执行一遍脚本就能跑起完整项目。这套做法成本极低收益却很高建议你从第一次搭建工程就开始养这个习惯。