简介:一套整合SpringBoot与MyBatis的Java Web开发模板,面向后端开发者与微服务初学者,解决快速搭建RESTful API、数据库持久层及接口文档一体化的问题。项目基于SpringBoot自动配置与MyBatis SQL映射,引入Druid连接池监控和Swagger接口文档,适合企业级应用开发、微服务实践及Spring生态学习场景。压缩包共159个文件,大小1.81MB,主要包含Java源码(15个java、16个class)、前端页面资源(66个js、10个css、6个html)、配置与映射文件(7个xml、3个properties)以及图片图标等,目录结构完整清晰。已有159人学习下载。资料内整合了用户管理、Redis配置、Swagger2、FastJson支持、文件上传等模块,涵盖Controller、Service、Mapper等分层代码,可直接学习SpringBoot与MyBatis的整合方式、Druid监控配置及Swagger API测试实践。通过本资源可快速理解基础框架的搭建思路,节省项目初始化时间,并借鉴其分层设计与配置细节。
1. Spring Boot + MyBatis:老组合为什么还在大量新项目里服役
Spring Boot 加 MyBatis 这对组合在 Java 后端项目里服役超过五年了,新项目还在选它,不是因为没得选,而是它把"启动省事"和"SQL 可控"这两件事同时做到了。JPA 把查询封装得太满,遇到复杂报表你会想骂人;MyBatis-Plus 是很省事,但当你需要手写一段多表关联时,反而要绕过它的封装。Spring Boot 负责自动配置、依赖收敛、内嵌容器,MyBatis 负责把 SQL 的最终解释权交回给你。这份源码工程按"订单查询模块"和几个模拟项目里的真实用法拆解,适合刚入门想少走弯路的人,也适合从 JPA 转过来想对照边界的人。
2. 第一次整合:依赖选型、数据源配置与第一条查询跑通
2.1 依赖怎么选:starter 与驱动的版本匹配
Maven 工程里最常用的组合是mybatis-spring-boot-starter加 MySQL 驱动。Spring Boot 的版本管理会自动关联 starter 的兼容版本,但如果你用的是 Spring Boot 3.x,starter 也要选 3.0.x 以上的配套版本,否则SqlSessionFactory的自动配置扫描不到,启动会直接报错。我一般这样写 pom:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.x</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>版本号这里我建议不要手写具体小版本,让 Boot 的依赖管理去收敛。很多人从旧项目升级时还在用mysql-connector-java这个老坐标,Spring Boot 2.7 之后官方驱动坐标已经换成mysql-connector-j,继续写老坐标虽然不报错,但会一直收到过时提示,依赖树里也容易出现两份驱动。把坐标换掉,同时去掉手写的<version>,能少很多冲突。
驱动要不要加<scope>runtime</scope>?我习惯加。编译期你根本不会直接引用 JDBC 驱动类,运行时才需要它。不加也不报错,但打包产物和依赖分析里会多出无意义的编译路径记录,排查问题时多一层噪音。另外一个常见的选型问题是"为什么不用 MyBatis-Plus"。这个源码工程刻意用原生 MyBatis,是因为 Plus 的 BaseMapper 虽然帮你省了单表 CRUD,但一旦遇到多表关联、动态排序、自增主键回填这些场景,反而要写更绕的 Wrapper 或者绕过它写 XML。与其绕,不如一开始就把 XML 和注解的能力摸清楚。
2.2 数据源配置:连接串参数与第一次启动验证
依赖装好之后,最基础的数据源配置长这样。假设你连的是一个本机 MySQL 实例:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver注意连接串里的三个参数不能乱删。serverTimezone=Asia/Shanghai不设置时,MySQL 8.x 驱动在某些环境会直接报The server time zone value的错误,那个乱码提示看起来像编码问题,实际上是时区问题。characterEncoding=utf8只是连接层字符集,如果你的库表实际用的是 utf8mb4,这里也改成 utf8mb4,否则 emoji 入库会变成问号。useSSL=false在本地开发环境关掉就好,生产环境如果数据库侧开了 SSL,这个参数要按实际证书情况配置,不是越省越好。
再往下是连接池配置。Spring Boot 2.x 默认连接池是 HikariCP,所以 Spring Boot 3.x 里如果没额外引入 Druid 依赖,那么 datasource 的底层就是 Hikari。此时把连接池参数写在spring.datasource.hikari下才有效:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000maximum-pool-size不是越大越好。你的数据库实例最大连接数如果是 100,应用部署了 10 个节点,每个节点配 20,高峰期所有节点同时打满就是 200,数据库会直接拒绝新连接。我一般按下限来:单实例 20 以内,多实例根据实例数反推。connection-timeout单位是毫秒,代表获取连接的最大等待时间,设成 30000 表示等 30 秒拿不到连接就抛异常,这个值在数据库抖动时能起到缓冲作用,不要设到 3000 以下。
2.3 接口、XML 与测试链路:第一条查询和插入
先写实体类。以模拟项目X的订单表t_order为例,字段有id、order_no、user_id、status:
public class Order { private Long id; private String orderNo; private Long userId; private Integer status; }对应 Mapper 接口放在com.example.mapper包下:
@Mapper public interface OrderMapper { Order selectByOrderNo(@Param("orderNo") String orderNo); }XML 放到resources/mapper目录,文件名OrderMapper.xml。这里有两个关键点:namespace 必须和接口全限定名完全一致,写错一个字母就是Invalid bound statement;#{orderNo}是预编译占位符,走数据库预编译缓存,也能挡注入。我不用${}去拼普通值,除非是动态表名这种实在躲不开的场景。
<?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.mapper.OrderMapper"> <select id="selectByOrderNo" resultType="com.example.entity.Order"> SELECT id, order_no, user_id, status FROM t_order WHERE order_no = #{orderNo} </select> </mapper>然后配置 MyBatis 的 XML 扫描路径:
mybatis: mapper-locations: classpath:mapper/*.xml最后用测试类验证整条链路是否通:
@SpringBootTest class OrderMapperTest { @Autowired private OrderMapper orderMapper; @Test void shouldQueryByOrderNo() { Order order = orderMapper.selectByOrderNo("20240001"); assertNotNull(order); assertEquals("20240001", order.getOrderNo()); } }这个测试类的作用不只是验证查询能跑通,还顺带验证了 Spring 容器能正确注入 Mapper。如果启动报Field orderMapper ... No qualifying bean,先查 Mapper 接口上有没有@Mapper注解,或者启动类上有没有@MapperScan("com.example.mapper")。两种方式任选其一,不要同时用,同时用也不会出问题,但会让新人误以为必须两个都加。
3. 配置调优与运行细节:驼峰映射、SQL 日志与连接池参数
3.1 map-underscore-to-camel-case:默认关闭的坑
很多项目的表字段是order_no,实体属性是orderNo。MyBatis 默认不会自动做下划线转驼峰,不打开开关时,select出来的 Order 对象orderNo一定是 null,而且你查日志、查 SQL 都看不出问题,因为字段名和列名都对得上,就是值没进去。
mybatis: configuration: map-underscore-to-camel-case: true这个配置对 resultType 自动映射生效,也对 XML 里没写 resultMap 的查询生效。但如果你的 XML 里写了显式 resultMap,resultMap 中 column 是order_no、property 是orderNo,那么这个开关就不起作用了。原因也好理解:显式 resultMap 已经把映射关系写死,开关只管自动映射那条路径。所以我处理复杂查询时统一用 resultMap,基础查询全部依赖自动映射,两条路线分开,互不干扰。
还有一个隐蔽的边界问题。如果实体属性名是单个单词userid,表字段是user_id,打开驼峰映射后,MyBatis 会把user_id拆成userId去匹配属性名,userid就匹配不上,结果就是属性为 null。这种问题我实际碰到过,排查了半天才发现是对不上号。解决办法是统一属性命名为userId,或者干脆在 SQL 里用别名user_id AS userid对齐。
3.2 SQL 日志:让每条真实执行的 SQL 可见
MyBatis 是否打印 SQL,取决于日志框架和日志级别。你工程里有 SLF4J 时,MyBatis 默认会走 SLF4J 输出,但 mapper 接口的日志级别默认是 DEBUG 以下,控制台不显示。推荐把 mapper 包级别单独调成 debug:
logging: level: com.example.mapper: debug这样只会打印 Mapper 相关的 SQL,不会把 Spring Boot 框架自身的调试日志全部带出来。很多人习惯把整个 root 调到 debug,然后用完一遍再改回来,这个操作很容易漏改,一上线日志就爆炸。
还有一个坑:不要混用mybatis.configuration.log-impl。如果你显式设置了log-impl: org.apache.ibatis.logging.stdout.StdOutImpl,SQL 会通过标准输出直接打在控制台,不经过 logback 的时间戳和格式,排查问题时时间线对不上。我一般不动 log-impl,统一交给 logger 级别控制。日志能显示的内容包括:预编译 SQL、参数列表、返回行数。看到==> Preparing:就说明走了预编译,看到==> Parameters:能确认参数实际值,返回行数能快速判断查询是否命中了预期。
3.3 连接池超时参数:别让 MySQL 先杀你的连接
前面 2.2 里写了连接池基本参数,这里单独说两个容易被忽略的值:idle-timeout和max-lifetime。HikariCP 默认 idle-timeout 是 10 分钟,max-lifetime 是 30 分钟,这两个时间要跟数据库侧的会话超时时间对齐。MySQL 的wait_timeout默认是 8 小时,但很多托管数据库实例默认只有 10 分钟甚至更短。如果数据库先把连接断了,连接池还不知道,下次查询拿到这条死连接,就会报Communications link failure之类的随机错误。
这种错误的特点是:服务刚启动时正常,跑一段时间后偶发,重启又好了。我遇到这类问题第一反应就是看连接池和数据库会话超时的关系。保守做法是把 max-lifetime 设成比数据库 wait_timeout 短,比如数据库侧 10 分钟,连接池的 max-lifetime 设成 8 分钟,idle-timeout 设成 5 分钟。这样连接池会在数据库断开之前主动回收连接,避免拿到死连接。参数不是越短越好,太短会导致频繁重建连接,对数据库也是一种额外压力。
4. 常见问题与避坑:五条高频踩坑记录与排查路径
4.1 Invalid bound statement (not found)
现象:应用启动完全不报错,一调用 Mapper 方法就抛BindingException: Invalid bound statement (not found): com.example.mapper.OrderMapper.selectByOrderNo。
原因:最常见的是 XML 的 namespace 和接口全限定名不一致,其次是mapper-locations没有覆盖到 XML 所在路径,第三种是 pom 的 resources 过滤器把 XML 排除到了打包之外。
解决:排查顺序固定走三步。第一步,打开编译后的target/classes目录,看 XML 到底有没有被打包进去,没有就先改 pom;第二步,核对 namespace 与接口全限定名是否完全一致,包括包名大小写;第三步,确认mybatis.mapper-locations的通配符路径和实际目录匹配。我见过一个项目把 XML 放在src/main/java下,导致默认打包不进去,报错报了一个下午。
4.2 动态 SQL 里的大于小于号
现象:在 XML 写<if test="startDate != null"> AND create_time > #{startDate}</if>,启动立刻报错,提示格式不正确。
原因:XML 里<是特殊字符,解析器会把它当成标签开始。MyBatis 在启动加载 XML 时就做解析,所以这个错等不到运行时,启动即炸。
解决:用 XML 实体转义,>写>,<写<,或者用 CDATA 包住整段 SQL:
<if test="startDate != null"> AND create_time >= #{startDate} </if>另一种写法:
<if test="startDate != null"> <![CDATA[ AND create_time >= #{startDate} ]]> </if>注意 CDATA 内部不能再用${}做字符串拼接,但#{}预编译参数可以正常使用。两种方式选一种统一用就行,别在同一套 XML 里换来换去,后续维护的人会懵。
4.3 循环调用 Mapper 引发 N+1 查询
现象:一个列表接口返回 100 条数据,数据库却执行了 100 多次查询,接口耗时从 20ms 涨到 800ms。
原因:开发时在 Java 的 for 循环里逐条调用 Mapper 查询子表数据,MyBatis 不会拦你,但每次循环都是一次数据库往返,100 条订单就是 100 次额外查询。
解决:把子表查询改成批量 IN 查询,一次查出全部数据,再在内存里组装。可用 foreach 拼 IN:
<select id="selectByOrderIds" resultType="com.example.entity.Order"> SELECT id, order_no, user_id, status FROM t_order WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>对应接口方法需要显式标注参数名:
List<Order> selectByOrderIds(@Param("ids") List<Long> ids);foreach 拼 IN 时有个边界:如果 ids 为空集合,生成的 SQL 是WHERE id IN (),MySQL 直接报语法错误。所以调用前要判空,或者在 XML 里用<if test="ids != null and ids.size() > 0">包住整个条件。这两个方法随便选一个,但一定得有。
4.4 主键回填失效
现象:插入一条订单后,数据库自增 id 正常,但 Java 实体的 id 字段还是 null,后续逻辑拿不到主键值。
原因:insert 语句没有配置useGeneratedKeys和keyProperty,MyBatis 不会自动把数据库生成的主键回填到实体。
解决:在 insert 标签上显式声明:
<insert id="insertOrder" keyProperty="id" useGeneratedKeys="true"> INSERT INTO t_order (order_no, user_id, status) VALUES (#{orderNo}, #{userId}, #{status}) </insert>这里 keyProperty 填的是实体属性名id,不是数据库列名,写错就拿不到。批量插入时 useGeneratedKeys 的兼容性也要注意:MySQL 8 驱动下多行 INSERT 能正确回填,老版本驱动可能只有第一行能拿到 id。如果业务要求批量插入后每行都有主键,别依赖回填,插入完成后按业务唯一键再查一次最稳。
4.5 @Transactional 没生效
现象:方法里连续执行两次插入,第二次故意抛异常,结果第一条数据还是留在数据库里了。
原因:最常见的是同类自调用。类里methodA调methodB,methodB上标了@Transactional,Spring 的声明式事务是通过代理对象实现的,自调用没有经过代理,注解直接失效。另一个可能原因:事务注解标在了private方法上,Spring 代理不会为私有方法加事务。
解决:把事务方法拆到另一个 Spring Bean 里,由外层 Bean 注入后调用;或者在类里注入自身代理再调用;最省事的是把事务边界整体上移到入口方法。这条规则只对 public 方法生效,所以从写第一行代码开始,@Transactional就只放在 public 方法上。还有一个附加条件:同一数据源下的传播行为默认 REQUIRED,如果你内部自己又开了一个数据源,事务不会覆盖到第二个数据源。
5. 进阶用法:分页插件、批量插入与链路验证
5.1 PageHelper 的参数边界
分页插件在 MyBatis 生态里基本是标配。使用时先引入依赖,然后在查询前调用PageHelper.startPage(pageNum, pageSize),紧接着的第一次查询会被拦截拼上 limit:
PageHelper.startPage(pageNum, pageSize); List<Order> list = orderMapper.selectPageList(query); PageInfo<Order> pageInfo = new PageInfo<>(list);startPage 只对紧随其后的第一条查询生效。如果你在 startPage 和 Mapper 调用之间插入了任何其他数据库操作,分页就会串到别的语句上,结果返回的条数完全不对。我在这个坑上翻车过一次,从那以后每次写分页,都强制检查 startPage 调用和 Mapper 调用之间有没有夹带其他查询。另外,分页查询返回的 List 不要做 Stream 过滤或二次内存分页,total 是 SQL 查出来的真实总数,你一旦在内存里过滤,列表和 total 就对不上了。要过滤就写进 SQL 条件,别偷懒。
5.2 批量插入的写法与验证
批量插入不要用 Java 循环一条一条 insert,也不要用 foreach 拼几千条 VALUES。前一种往返次数太多,后一种 SQL 报文太大容易触达max_allowed_packet限制。建议分批插入,每批 500 到 1000 条:
<insert id="batchInsert" keyProperty="id" useGeneratedKeys="true"> INSERT INTO t_order (order_no, user_id, status) VALUES <foreach collection="list" item="item" separator=","> (#{item.orderNo}, #{item.userId}, #{item.status}) </foreach> </insert>连接串里加上rewriteBatchedStatements=true,可以让 JDBC 驱动把多条连续 insert 重写成批量执行,性能提升明显。这个参数不在 MyBatis 里,在数据源 url 上:
jdbc:mysql://127.0.0.1:3306/demo?rewriteBatchedStatements=true验证批量插入有没有真正走批量,最直接的做法是打开 MySQL 的通用日志或看连接器发出去的 SQL 条数。如果不生效,代码层写的是executeBatch,实际数据库侧收到的还是一条一条 insert,常见原因就是驱动没加 rewrite 参数。这份源码工程我在压测环境实际压过:500 条一批,开启参数后耗时下降明显。从那以后我每次上线涉及批量写入的接口,都强制走一遍核对连接串参数的流程。希望帮到你。
本文还有配套的精品资源,点击获取