先把结论放在前面:如果你手头没有一套验证过的版本组合,直接用IDEA新建SpringBoot工程再引入MyBatis和MySQL,大概率会在“下载依赖—连数据库—启动报错”这三步里卡掉半天。我最近帮同事在一台新电脑上从头搭环境,正好把这些坑全部重新踩了一遍,所以这篇文章干脆把从零搭建一套SpringBoot + MyBatis + MySQL工程项目的完整过程写清楚,包括版本怎么选、环境怎么配、配置怎么写、报错怎么查、接口怎么测。
这套东西本身不难,难的是版本搭配和配置细节。SpringBoot官方已经把大部分事情替你做好了,但正因为“替你做好了”,一旦底层版本换了,报错信息往往让人摸不着头脑。文章的目标就一个:让你照着操作,一遍跑通。
1. 版本选型别拍脑袋:JDK、SpringBoot、MySQL怎么搭才不打架
1.1 先定JDK再定SpringBoot:1.8还是17
很多新手直接用IDEA默认的SpringBoot版本创建项目,然后发现编译、启动全是问题,根源多半是JDK和SpringBoot版本不匹配。
SpringBoot 2.x系列要求JDK 8起步,最高也支持到JDK 17左右;SpringBoot 3.x系列则明确要求JDK 17及以上。如果你电脑上装的是JDK 8,却用了SpringBoot 3.2.x,启动会直接报UnsupportedClassVersionError或者编译阶段就过不去。
我的建议是,学习阶段优先选SpringBoot 2.7.x。理由很实在:网上能找到的资料、教程、demo绝大多数基于2.x,MyBatis、连接池等周边组件的兼容性问题也基本都暴露过、解决过了。而且2.7.x是2.x系列的最终版本,补丁相对完善。
如果你就是想尝鲜SpringBoot 3.x,那同时也要配上JDK 17,并且MyBatis依赖要用mybatis-spring-boot-starter的3.0以上版本,这一点特别容易漏。
1.2 MySQL 5.7还是8.0:驱动类名和URL都不同
MySQL的选择同样会影响代码配置。现在新装的MySQL基本都是8.0,但很多老教程写的是5.7甚至5.6的写法,混着用就会踩坑。
关键区别有两个:
- 驱动类名不同。5.7及以前写
com.mysql.jdbc.Driver,8.0以后必须写com.mysql.cj.jdbc.Driver。如果你用的连接包是mysql-connector-j(8.x),配置文件里还写旧驱动名,启动时就报ClassNotFoundException。 - URL参数不同。8.0版本对时区更敏感,不指定
serverTimezone可能直接报时区无法识别的错误。另外8.0默认开启了SSL,本地开发时建议在URL后面加useSSL=false,否则日志里会有SSL警告,某些场景下还会影响连接速度。
1.3 一套经过验证的稳定组合参考
我这次搭建用的组合,大家可以照抄:
| 组件 | 版本 |
|---|---|
| IDEA | 2023.2社区版 |
| JDK | 1.8(8u202) |
| Maven | 3.8.8 |
| SpringBoot | 2.7.18 |
| MyBatis Starter | 2.3.2 |
| MySQL | 8.0.36 |
这套组合的好处是兼容性验证得比较多,社区讨论也多,遇到问题能搜到现成答案。如果你是按这个版本搭的,后面所有的配置都可以直接复用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四件套环境从零配置:IDEA、JDK、Maven、MySQL
2.1 IDEA安装:社区版完全够用
IDEA分Ultimate和Community两个版本。很多新手纠结要不要装专业版,其实做SpringBoot + MyBatis + MySQL这种纯后端项目,社区版足够了。专业版主要是多了前端、数据库工具、Spring官方支持等付费功能,SpringBoot项目本身通过Spring Initializr一样能创建,不影响学习。
安装时有几个小细节要注意:
- 官网下载时认准从IntelliJ IDEA官方入口进,不要在下第三方打包站乱下,社区版安装包一两百MB,哪里都能下,但官网最干净。
- 安装过程中记得勾选“Create Desktop Shortcut”和“Add to PATH”相关选项,方便命令行里直接敲
idea启动。 - 首次启动会询问是否导入配置,一般选“Do not import settings”即可。
IDEA本身不需要破解,社区版就一直免费,不存在需要激活码的情况。凡是涉及“破解版”“激活码”的下载渠道建议都别碰,一个是没必要,另一个是安全风险太高。
2.2 本地JDK与Maven配置
JDK安装没什么花头,关键是配置环境变量。JAVA_HOME指向JDK安装目录,PATH里加上%JAVA_HOME%\bin。配置完后在命令行执行java -version验证。
Maven这边,解压后同样需要配MAVEN_HOME和PATH。但真正影响开发效率的是settings.xml里的镜像配置。国内网络环境直接访问Maven中央仓库经常非常慢或者直接失败,所以我会在conf/settings.xml的<mirrors>节点里加上阿里云镜像:
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
配好之后,Maven下载SpringBoot依赖的速度会有质的提升。
在IDEA里还要设置一下Maven的关联:Settings -> Build, Tools, Code Execution -> Build Tools -> Maven,把Maven home path指向你本地解压的Maven目录,User settings file指向对应settings.xml。这一步不设置的话,IDEA会用它自带的Maven,虽然也能用,但有些公司内网代理、镜像配置就无法生效,下载依赖可能反复失败。
2.3 MySQL安装与初始化注意点
MySQL安装本身不复杂,复杂的是安装过程中的选项。
我建议用MySQL Installer的Developer Default模式,它会连MySQL Shell、Workbench、驱动一起装好,省事。安装到设置root密码那一步,记住别设太复杂的密码,毕竟是本地开发环境,设成123456这类简单密码完全没毛病,后面连接配置也省心。
字符集设置也很关键。安装时或者初始化时把默认字符集设为utf8mb4,避免后面插入中文数据出现乱码。如果你已经装好了MySQL,也可以在my.ini里手动加上:
ini复制[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
修改完重启MySQL服务,然后用SHOW VARIABLES LIKE 'character_set_server';确认。
安装完成后,建议先用Workbench或命令行登录MySQL,手动创建一个数据库,比如:
sql复制CREATE DATABASE demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
这一步很重要,因为很多SpringBoot启动报错并不是连接信息错了,而是数据库根本不存在。
3. 建工程骨架与POM依赖:这一步决定了后面能少改多少配置
3.1 用Spring Initializr初始化项目
IDEA社区版不直接提供Spring Initializr可视化向导,但有两个办法可以绕开:
- 用IDEA自带选项里的“New Project -> Spring Boot”配合联网创建。
- 直接访问Spring Initializr官网,在网页上填好项目基本信息,生成一个zip包,再通过IDEA的“Open”导入并作为Maven项目打开。
我更推荐第二种,可控性更强。在Initializr页面上的几个关键配置项要注意:
- Project选择Maven。
- Language选择Java。
- Spring Boot版本选2.7.18。
- Group填
com.example,Artifact填demo,这两项决定了包路径。 - Java版本选8。
- Dependencies部分手动勾选
Spring Web、MyBatis Framework、MySQL Driver。
勾选完成后点击Generate下载zip,解压后用IDEA打开。初次打开时右下角会提示加载Maven项目,等它把依赖下载完,项目骨架就立起来了。
3.2 POM核心依赖解读
生成好的pom.xml里,主要依赖就是那三个按需勾选的组件,但有两个地方需要根据实际情况调整。
第一个是MyBatis Starter版本。Spring Initializr生成的项目里,MyBatis依赖只会给一个不带版本号的声明,因为版本由SpringBoot的BOM统一管理。但如果你用SpringBoot 2.7.x,默认管理的MyBatis Starter可能是2.1.x或2.2.x,这时候建议显式指定一个常用版本,防止版本过老带来兼容问题:
xml复制<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
第二个是MySQL驱动。SpringBoot 2.7.x的BOM默认引入的是mysql-connector-j(8.x版本),这个没问题,但如果你的项目是用SpringBoot 2.3.x或更老版本初始化的,可能会引入mysql-connector-java,两者虽然本质是同一个东西,但包名不同,POM报红的时候需要留意。
依赖结构这块,我的经验是启动报错先回头看POM,排除法永远是最快的。如果出现奇怪的ClassNotFoundException,第一反应应该是某个依赖没被引入或者版本冲突了。
3.3 application.yml:数据源与MyBatis配置项
SpringBoot的配置文件有两种形态:application.properties和application.yml。我偏向用yaml,层级关系清晰,写起来也省事。核心配置如下:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
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
解释几个重要的点:
serverTimezone=Asia/Shanghai是8.0时代必加的,否则可能报时区错误。allowPublicKeyRetrieval=true是8.0连接时因为公钥检索策略导致的Public Key Retrieval is not allowed错误,加上一劳永逸。useSSL=false是避免本机SSL握手警告。
mapper-locations指向的是Mapper XML文件存放的位置。很多人用classpath:mapper/*.xml,那么XML文件就必须放在src/main/resources/mapper/目录下,不然Mapper接口扫描到了XML却找不到。
map-underscore-to-camel-case用来把数据库的user_name自动映射成Java的userName,不配的话,实体属性如果和表字段命名风格不一致,查出来的数据全是null。这一项强烈建议开启。
log-impl配成StdOutImpl后,执行SQL时会在控制台打印完整的SQL语句和参数,对调试特别有用。
4. MyBatis接入MySQL:Mapper扫描、XML映射、SQL控制台打印
4.1 @MapperScan和@Mapper怎么选
MyBatis接入SpringBoot以后,第一个问题就是Mapper接口如何被容器识别。
有两种常见方式:
- 在主启动类上加
@MapperScan("com.example.demo.mapper"),一次性扫描整个包下的所有Mapper接口。 - 在每个Mapper接口上单独加
@Mapper注解。
推荐使用@MapperScan,在新加Mapper接口时不用每个都去记着加注解,减少遗漏。习惯上我也会把@MapperScan放在SpringBoot启动类上,这样Mapper接口包结构一眼就能看出来。
还有一种情况是,如果你的Mapper接口和XML文件分散在不同模块,扫描路径就要更精确地配置。单模块项目用@MapperScan完全够了。
4.2 实体类、Mapper接口和XML文件的对应关系
这是初次接触MyBatis最容易晕的地方。实体类、Mapper接口、XML文件三者之间的关系可以这样理解:接口负责定义方法,XML负责写SQL,实体类负责接收结果。
三个文件的代码示例后面第5节会完整给出,这里先说目录结构。标准做法是这样:
code复制src/main/java/com/example/demo
├── controller
├── service
├── mapper # Mapper接口
├── entity # 实体类
└── DemoApplication.java
src/main/resources
├── mapper # XML文件,和Java包中的mapper对应
└── application.yml
XML文件的namespace必须写成Mapper接口的全限定名:
xml复制<mapper namespace="com.example.demo.mapper.UserMapper">
不写或者写错,启动时不一定会报错,但一调用方法就报Invalid bound statement (not found)。
4.3 控制台打印SQL的两种方式
排查问题的时候,能在控制台看到MyBatis实际执行的SQL和参数,能省大量时间。打印SQL有两种常用做法。
第一种就是在application.yml里配置log-impl:
yaml复制mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
配置后运行任意查询,控制台会输出Preparing、Parameters、Total等关键信息。这种方式最简单,本地开发非常推荐。
第二种是通过日志框架级别控制,比如在配置里加上:
yaml复制logging:
level:
com.example.demo.mapper: debug
这种方式把指定包下的日志级别调成debug,依赖SLF4J去输出MyBatis的日志。两种方式选一种就行,不要同时配,否则日志刷屏,反而影响阅读。
5. 用一个用户表的增删改查把整条链路跑通
5.1 建表SQL与实体类
配置做完了,还得有业务代码来验证。我习惯用一个最简单的用户表来验证,结构如下:
sql复制CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_name` varchar(50) NOT NULL,
`password` varchar(100) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
实体类放在entity包下,注意字段名和表字段的映射,我这里配合了map-underscore-to-camel-case,所以Java里用驼峰命名userName,数据库里用下划线user_name,MyBatis可以自动映射:
java复制package com.example.demo.entity;
public class User {
private Long id;
private String userName;
private String password;
private Integer age;
// 省略getter和setter
}
5.2 Mapper接口与XML映射
Mapper接口定义增删改查方法:
java复制package com.example.demo.mapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
List<User> findAll();
User findById(@Param("id") Long id);
int insert(User user);
int update(User user);
int delete(@Param("id") Long id);
}
XML文件放在resources/mapper目录下,命名为UserMapper.xml:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<select id="findAll" resultType="com.example.demo.entity.User">
select id, user_name, password, age from user
</select>
<select id="findById" resultType="com.example.demo.entity.User">
select id, user_name, password, age from user where id = #{id}
</select>
<insert id="insert" parameterType="com.example.demo.entity.User"
useGeneratedKeys="true" keyProperty="id">
insert into user(user_name, password, age)
values(#{userName}, #{password}, #{age})
</insert>
<update id="update" parameterType="com.example.demo.entity.User">
update user
set user_name = #{userName}, password = #{password}, age = #{age}
where id = #{id}
</update>
<delete id="delete">
delete from user where id = #{id}
</delete>
</mapper>
这里有几个细节要特别提醒:
useGeneratedKeys="true" keyProperty="id"可以让插入后自动把自增主键回填到实体对象里,否则调用insert后user.getId()依然是null。
#{}和${}的区别要理解清楚。#{userName}会生成预编译占位符?,能防止SQL注入;${}是直接拼接字符串,只在需要动态拼接表名、列名等特殊场景下使用,日常查询一律用#{}。
5.3 Service、Controller与请求测试
Service层做一层简单的封装:
java复制package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public List<User> list() {
return userMapper.findAll();
}
public User get(Long id) {
return userMapper.findById(id);
}
public int add(User user) {
return userMapper.insert(user);
}
public int update(User user) {
return userMapper.update(user);
}
public int delete(Long id) {
return userMapper.delete(id);
}
}
Controller暴露HTTP接口:
java复制package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@GetMapping("/list")
public List<User> list() {
return userService.list();
}
@GetMapping("/{id}")
public User get(@PathVariable Long id) {
return userService.get(id);
}
@PostMapping("/add")
public String add(@RequestBody User user) {
return userService.add(user) > 0 ? "success" : "fail";
}
@PutMapping("/update")
public String update(@RequestBody User user) {
return userService.update(user) > 0 ? "success" : "fail";
}
@DeleteMapping("/{id}")
public String delete(@PathVariable Long id) {
return userService.delete(id) > 0 ? "success" : "fail";
}
}
写完这些代码,直接启动DemoApplication,控制台看到Started DemoApplication in x.xx seconds就说明全部通了。
接口测试可以直接用IDEA自带的HTTP Client。在IDEA里新建一个.http文件,写几个请求:
http复制### 查询列表
GET http://localhost:8080/user/list
### 新增用户
POST http://localhost:8080/user/add
Content-Type: application/json
{
"userName": "zhangsan",
"password": "123456",
"age": 20
}
### 修改用户
PUT http://localhost:8080/user/update
Content-Type: application/json
{
"id": 1,
"userName": "lisi",
"password": "123456",
"age": 25
}
### 删除用户
DELETE http://localhost:8080/user/1
5.4 一个容易忽视的问题:参数绑定失败
在实际测试过程中,POST和PUT接口如果返回{"timestamp":"...","status":400,"error":"Bad Request"},多半是请求的Content-Type没设置成application/json,或者请求体里的字段名和实体类字段对不上。
SpringBoot用Jackson做JSON反序列化,默认情况下它会要求JSON字段名和实体类属性名一致。如果前端习惯用user_name传参,而后端实体类是userName,就会绑定失败。这个问题在前后端协作时尤其常见,可以在application.yml里开启Jackson下划线转驼峰:
yaml复制spring:
jackson:
property-naming-strategy: SNAKE_CASE
不过这个配置开了之后,输出的JSON字段也会变成下划线风格,所以一般还是建议前后端约定好字段命名。自己测试的话,直接传驼峰字段就行。
6. 启动时最容易踩的四个坑与完整排查思路
6.1 驱动类加载失败:先确认依赖再确认配置
报错信息类似于:
code复制java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
这个报错在MyBatis + MySQL项目里非常经典。原因通常有两个:一是确实没有引入MySQL驱动,二是驱动版本和驱动类名不匹配。
排查思路按顺序来:
第一步,打开pom.xml确认有没有mysql-connector-j或mysql-connector-java依赖。没有就补上:
xml复制<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
第二步,确认application.yml里的driver-class-name。如果是8.x驱动,必须写com.mysql.cj.jdbc.Driver;如果是5.x驱动,写com.mysql.jdbc.Driver。
第三步,检查Maven依赖树里是不是同时存在两个版本的MySQL驱动。在IDEA右侧Maven面板找到项目下的依赖树,如果mysql-connector-java和mysql-connector-j同时存在,排除掉旧的那个。
6.2 时区错误导致数据库连接失败
报错信息类似于:
code复制java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone.
这是MySQL 8.0的时区参数没配好。解决方法就是在JDBC URL后面追加:
text复制serverTimezone=Asia/Shanghai
如果你用的是SpringBoot 2.7.x,也可以写成serverTimezone=GMT%2B8,注意+号在URL里需要转义成%2B。我更推荐直接写Asia/Shanghai,语义清晰。
6.3 Invalid bound statement:接口方法找不到XML里的SQL
报错信息:
code复制org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.demo.mapper.UserMapper.findById
这个报错的原因比较多,按概率从上到下排查:
- XML文件的
namespace写错。打开UserMapper.xml,检查namespace是不是Mapper接口的全限定名。 - XML文件没有放到
mapper-locations指定的目录下。比如配置的是classpath:mapper/*.xml,XML文件却放在了resources根目录,就会找不到。 - XML文件后缀名或文件名写错。SpringBoot扫描时是按文件名匹配的,
UserMapper.xml里定义的方法要对应UserMapper接口,两者文件名必须一致。 - Maven构建时没有把XML文件打包进去。检查
target/classes目录下有没有对应XML文件,没有的话在pom.xml的<build>节点里加资源过滤配置:
xml复制<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
这个情况常见于把XML和Mapper接口放在同一个目录下的场景,SpringBoot默认只打包resources里的XML,不处理src/main/java下的。
6.4 Access denied、Unknown database等其他常见问题
除了上面三个,以下是新手容易碰到的其他问题:
| 问题现象 | 原因 | 解决方式 |
|---|---|---|
Access denied for user 'root'@'localhost' |
用户名或密码错误 | 检查application.yml里的username和password |
Unknown database 'demo' |
数据库没创建 | 先连MySQL执行CREATE DATABASE demo |
Connection refused |
MySQL服务没启动或端口不对 | 确认MySQL服务已启动,端口默认3306,改了要同步配置 |
Port 8080 was already in use |
端口被占用 | 换端口,或者用netstat -ano找到占用进程并结束 |
| 控制台不打印SQL | log-impl没配置或日志级别不对 | 配置StdOutImpl,或调整mapper包日志为debug |
有时候问题不是单个出现的,而是连环的。比如MySQL的密码策略问题会连带着Access denied、连不上、然后SpringBoot启动失败。所以排查的时候一定要先看最底层的异常,从下往上解读堆栈信息,不要被第一行红色日志带偏。我第一次搭的时候看到一长串错误心里发毛,结果真正的原因就是密码多打了一个空格。
写在最后
这套环境搭完,后面再建项目就快多了。我个人的体会是,SpringBoot + MyBatis + MySQL这类组合项目,最大的瓶颈从来不是某个API不会用,而是版本之间互相不配合。所以我建议你装好环境之后,把这份协议固定下来:JDK 8对应SpringBoot 2.7.x,MySQL 8.0对应新驱动类名,MyBatis Starter用2.3.2。只要这个底子不乱,项目本身基本不会出幺蛾子。
另外一个小建议是,把初始项目保存一份模板。下次要开新项目时直接复制这份工程,改掉包名、数据库名,比每次都从Spring Initializr重新生成再配一遍快得多。我自己就是这么干的,省掉了大量重复劳动。
