
从 Spring Boot 2.3 一路用到 3.xMyBatis 和 PostgreSQL 这套组合我前前后后搭了不下十次。很多人把“Spring Boot 整合 MyBatis 与 PostgreSQL”当成一个固定流程引入依赖、写个数据源、复制一段 CRUD 模板跑通就关掉文档。但在实际项目里版本选型、事务边界、缓存策略、SQL 条件写法、甚至第三方接口放哪儿这些才是决定项目能跑多久的关键。这篇就把我踩过的坑和验证过的做法整理出来按“工程整合 → 最小功能闭环 → 细节排错 → 进阶工程化”的顺序讲适合刚接触这套技术栈的新人也适合准备从 Spring Boot 2.x 升 3.x、或者想把项目做得更规范的老手。1. 为什么选这个组合以及版本搭配的第一道选择题1.1 三者的分工与常见误区很多初学者会把 Spring Boot、MyBatis、PostgreSQL 当成一个“大而全”的框架包来理解其实它们各自管得非常清楚Spring Boot 管的是“应用怎么启动、对象怎么装配、HTTP 接口怎么暴露”它解决的是工程化问题MyBatis 管的是“数据库操作怎么映射成 Java 方法”它解决的是 SQL 与代码的边界问题PostgreSQL 管的是“数据本身怎么存储、怎么保证一致性”它解决的是底层数据信任问题。三者叠加之后最容易出的误判是以为整合的难点在“把三个东西拼起来”其实真正的难点在“约定不一致”。比如 MyBatis 默认下划线字段不会自动映射成驼峰属性PostgreSQL 的序列和自增主键行为又和 MySQL 的 AUTO_INCREMENT 不一样Spring Boot 2.x 和 3.x 的 starter 版本也会互相牵制。这些细节如果不提前搞清楚跑 Demo 没问题一上生产就崩。1.2 PostgreSQL 版本选择不是越新越稳搜索引擎里高频出现的“postgresql下载哪个版本”“postgresql 16便携版”“linux离线安装postgresql”这类词说明大家在版本选择上确实容易纠结。我的建议是分场景新项目且没有历史包袱选 PostgreSQL 16 或 15这两个版本在性能、分区表、逻辑复制方面都有明显改进官方支持周期还很长生产环境已经有 12/13/14 存量库先别急着升级老版本更成熟配套的工具链、监控脚本、备份方案都被验证过升级带来的收益不一定覆盖风险本地学习、做小型实验用 Docker 跑一个官方镜像最省事不需要在系统里残留一堆依赖。Spring Boot 官方对 PostgreSQL 没有强制版本绑定驱动postgresql的坐标会跟随 Spring Boot 父工程管理版本所以我见过不少人直接把驱动写在pom.xml里不管版本号也能正常跑。但有一点要特别提醒如果项目里用了 PostgreSQL 的 JSONB、数组、TIMESTAMPTZ这类类型驱动版本太旧会出现类型转换异常这时候就需要显式声明驱动版本。1.3 Spring Boot 2.x 与 3.x 的取舍逻辑在“spring boot 2.3.x 2.6.x”和“后端spring boot 3和python fastapi”这些搜索词背后大家真正想问的是我到底该基于哪个版本做新项目我的判断标准很简单如果你在意生态兼容性尤其是公司内部有大量基于 Spring Boot 2.x 开发的基础组件就继续用 2.7.x这是 2.x 系列的最终版本维护周期最长如果你做的是全新项目或者想用上 JDK 17 的虚拟线程、新的观测体系直接上 Spring Boot 3.x对应的 MyBatis starter 要用mybatis-spring-boot-starter的 3.0.x 版本尽量别停留在 2.3.x/2.6.x 这种中间版本它们没有持续的安全修复后面想升级的时候跨度越大改动成本越高。1.4 数据库装法的选择Docker、本地安装还是源码编译代码写之前得先有个能连的 PostgreSQL。“postgresql安装”“postgresql下载配置”“docker安装postgresql”“ubuntu 源码编译postgresql”这些热词的关注点挺一致怎么在最短时间内把库跑起来。我的实操顺序是这样的第一本地开发直接用 Dockerdocker run -d \ --name pg-dev \ -e POSTGRES_USERpostgres \ -e POSTGRES_PASSWORDpostgres \ -e POSTGRES_DBshop_db \ -p 5432:5432 \ -v pgdata:/var/lib/postgresql/data \ postgres:16这样启动的 PostgreSQL 数据卷独立挂在 Docker volume 里容器删了数据还在。第二如果你在 CentOS 或其他 Linux 上安装用官方 APT/Yum 源比源码编译靠谱得多。源码编译需要自己处理./configure、make、make install和 PATH 配置只适合需要定制编译参数或者没法用包管理器的环境。第三Windows 用户直接下载 EnterpriseDB 的图形化安装包安装步骤里记得勾选“安装 Stack Builder”时选 PostgreSQL JDBC Driver后面 Java 项目里就不用手动去别处找 jar 了。2. 工程整合依赖、数据源配置和连接池的细节2.1 依赖坐标和最容易写错的 starter如果你用 Maven典型的最小依赖是这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId scoperuntime/scope /dependency /dependencies如果你用的是 Spring Boot 3.x把mybatis-spring-boot-starter换成 3.0.xdependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.4/version /dependency容易写错的地方有两处。一是把坐标写成mybatis而不是mybatis-spring-boot-starter这样不会自动装配SqlSessionFactory你只能在代码里手动把它硬编码成自己的 Bean二是忘记把 PostgreSQL 驱动的 scope 设为runtime导致打了 jar 包之后驱动类被跳过或冲突。2.2 数据源配置区分“链接”的两种含义说到application.yml有一个基础但很重要的概念要先理清“pg链接”在项目里有两种完全不同的含义。第一种是 Java 应用和 PostgreSQL 服务之间的网络连接也就是 Spring 数据源配置里的jdbc:postgresql://...第二种是 SQL 查询里的表关联 JOIN。很多人排查问题时空有一句“PG 连不上”却说不清到底是指连接池连不上、还是指某个 JOIN 语句写错了导致运行慢这就很浪费排查时间。我建议的配置模板spring: datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/shop_db?prepareThreshold1useUnicodetruecharacterEncodingutf8 username: postgres password: postgres hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 pool-name: ShopDbHikariPool server: port: 8080 mybatis: mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true jdbc-type-for-null: null这里我特意加了prepareThreshold1。很多人在 MySQL 下没写 JDBC URL 参数也能跑但到了 PostgreSQL如果不显式处理预编译语句的阈值高并发场景下看到的现象就是“每个请求都重新分析 SQL 计划CPU 升高但查询本身很快”。另外hikari是 Spring Boot 默认连接池。maximum-pool-size不是越大越好PostgreSQL 每个连接都占内存和进程资源我见过一台 4G 内存的测试机配了 50 的池上限应用一启动数据库后面跟着出现一堆FATAL: sorry, too many clients already。基本经验常规 Web 应用池大小建议CPU核心数 * 2 磁盘数量20 对于多数中小项目已经是上限。2.3 验证连接的最快方式写一个不依赖业务的查询配置好数据源之后先别急着写业务。我会在启动类附近放一个临时的CommandLineRunner测试连接Component public class DbConnectionProbe implements CommandLineRunner { private final JdbcTemplate jdbcTemplate; public DbConnectionProbe(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Override public void run(String... args) { Integer result jdbcTemplate.queryForObject(SELECT 1, Integer.class); System.out.println([DBProbe] connect success, SELECT 1 result); } }Spring Boot 会因为数据源配置自动装配JdbcTemplate所以这段代码不需要额外注册 Bean。启动应用后控制台输出SELECT 1 1说明驱动、URL、账号密码整条链路都通。别一上来就 MyBatis否则你很难判断“报错”到底是配置问题还是映射问题。3. 跑通注册功能把最小业务闭环串起来3.1 表结构设计PostgreSQL 的主键生成策略很多人把“Spring Boot MyBatis PostgreSQL 实现注册功能”当成练手项目我理解这种思路注册涉及的 Controller → Service → Mapper → 数据库是一条完整链路任何一个环节断了都会立刻暴露。所以这一节我以注册为例把整套代码结构讲透。先建表CREATE TABLE user_account ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(100), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uk_user_username UNIQUE (username) ); COMMENT ON TABLE user_account IS 用户注册表; COMMENT ON COLUMN user_account.password IS 加密后的密码;这里我用的是 PostgreSQL 的GENERATED ALWAYS AS IDENTITY而不是 MySQL 习惯的AUTO_INCREMENT。二者效果类似但 Identity 是标准 SQL 写法而且ALWAYS意味着应用不能手动插入主键可以避免很多数据类型和主键冲突问题。事务里如果你需要知道新插入的主键配合 MyBatis 的useGeneratedKeystrue就能拿到。3.2 分层代码实体、Mapper、Service、Controller实体类public class UserAccount { private Long id; private String username; private String password; private String email; private LocalDateTime createdAt; private LocalDateTime updatedAt; // getter / setter 省略 }Mapper 接口Mapper public interface UserAccountMapper { int insert(UserAccount user); UserAccount findByUsername(Param(username) String username); }对应的 XML我放在src/main/resources/mapper/UserAccountMapper.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.UserAccountMapper insert idinsert parameterTypecom.example.demo.entity.UserAccount useGeneratedKeystrue keyPropertyid INSERT INTO user_account (username, password, email) VALUES (#{username}, #{password}, #{email}) /insert select idfindByUsername resultTypecom.example.demo.entity.UserAccount SELECT id, username, password, email, created_at, updated_at FROM user_account WHERE username #{username} /select /mapperService 层我建议加一个事务边界。注册这种操作往往不是只插一条记录还可能要记录日志、发验证码、初始化用户配置所以直接用Transactional兜住整体Service public class RegisterService { private final UserAccountMapper userAccountMapper; public RegisterService(UserAccountMapper userAccountMapper) { this.userAccountMapper userAccountMapper; } Transactional(rollbackFor Exception.class) public Long register(RegisterRequest request) { if (userAccountMapper.findByUsername(request.getUsername()) ! null) { throw new BusinessException(用户名已存在); } UserAccount user new UserAccount(); user.setUsername(request.getUsername()); user.setPassword(passwordEncoder(request.getPassword())); user.setEmail(request.getEmail()); userAccountMapper.insert(user); return user.getId(); } }Controller 就不重复贴了规范一点就是接收 DTO、校验参数、调用 Service、返回统一结果对象。3.3 查看 SQLMyBatis 打印日志的配置方法“mybatis配置打印”这个问题被搜得还挺多。我想说两种查 SQL 的方式分别对应开发和线上开发期间我通常直接在配置文件里开启 stdout 日志mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这会让每一个 SQL 和参数都直接打到控制台包括 Preparing: ...和 Parameters: ...。缺点是日志太碎生产环境不推荐。生产环境我推荐用更可控的方式在配置文件里指定 mapper 包日志级别logging: level: com.example.demo.mapper: debug这样只对 Mapper 接口输出 debug 日志既能看到 SQL也能通过日志文件留痕不至于被 stdout 刷屏。两种方式如果同时配置生产环境会有重复打印优化时注意看一下有没有log-impl的残留。4. 别被现象带偏MyBatis 条件不生效与缓存问题的排查链路4.1 条件不生效根因通常不在 PG而在 XML 写法和类型判断“mybatis条件不生效”是一个高频踩坑点。不少人第一反应是 SQL 写错了或者 PostgreSQL 不支持某个语法但大多数时候问题出在 MyBatis 动态 SQL 的if判断上。举一个常见的例子select idqueryUsers resultTypecom.example.demo.entity.UserAccount SELECT id, username, password, email, created_at FROM user_account where if testusername ! null and username ! AND username #{username} /if if testemail ! null and email ! AND email #{email} /if /where /select看着没问题但如果test里写的是if testusername ! null username ! 那么 XML 解析阶段就会报错因为在 XML 里必须转义成amp;amp;。另一个常见问题是传进来的username确实是空字符串但前端拼了空格导致! 判断通过最后查了个寂寞。再有就是 Java 类型和 PG 类型不一致。比如你传的是ListString但在 XML 里用IN查询时没有处理空集合if testids ! null and ids.size() 0 AND id IN foreach collectionids itemid open( separator, close) #{id} /foreach /if如果漏掉ids.size() 0判断就会生成AND id IN ()PostgreSQL 直接报语法错误。这些排查起来都不难但我强调一点当你看到“条件不生效”时先把最终生成的 SQL 打出来看再讨论逻辑不要凭空猜测。4.2 MyBatis 二级缓存的“甜”与“毒”“mybatis缓存”“mybatis二级缓存实现”这两个词背后的关注点其实是一致的缓存到底该怎么开、开了之后会不会有坑。MyBatis 的一级缓存是SqlSession级别的Spring 整合后通常每次 Mapper 调用会由 SqlSessionTemplate 管理。二级缓存是 Mapper namespace 级别的默认关闭开启方式是在 mapper XML 加一段cache evictionLRU flushInterval60000 size512 readOnlyfalse/好处很明显同一 namespace 下的查询结果可以被多个 SqlSession 共享减少数据库压力。但我要提醒几个和 PostgreSQL 相关的问题第一缓存保存的是 Java 对象如果查询出的结果里有TIMESTAMPTZ或JSONB类型序列化/反序列化过程可能不稳定不设置readOnlytrue时容易出现脏数据。第二如果在多个微服务实例里同时跑本地二级缓存不会自动同步。一个实例更新数据另一个实例的缓存还是旧的。所以多实例部署时要么直接用 Redis 这类外部缓存要么就把二级缓存关掉不要半吊子。第三更新时注意缓存失效策略。cache的默认失效是基于 namespace 的如果你在另一个 Mapper 里联表更新了同一张表这个 namespace 的缓存并不会失效查出来的还是旧数据。我的建议是单体低并发场景可以开微服务场景谨慎开。二级缓存是 MyBatis 里最容易被“好像能优化性能”带偏的功能真正的高并发系统很少靠 MyBatis 本地缓存扛。4.3 从“pg连接”到表连接两种概念的排查逻辑这一节我把 2.2 提到的两种“pg链接”展开因为很多人会在这两种概念之间反复横跳。第一种是数据库连接。真正排查时我会先看 HikariCP 的日志、连接池活跃数、等待线程数。如果连接池满了先判断是不是连接泄漏有没有在代码里开了连接不关闭有没有长时间事务有没有Transactional把大查询包进去后迟迟不提交。再判断是不是数据库端max_connections设置太低尤其是本地 Docker 起默认配置时。第二种是 SQL 里的表连接JOIN。PostgreSQL 的查询规划器在 JOIN 时比较依赖统计信息和索引。我遇到过“注册时查询用户列表特别慢”的情况最后发现是关联查询里对user_account.created_at做了to_char()函数转换导致索引失效。正确做法是把函数加工放到应用层来做或者使用date_trunc 查询条件范围。5. 进阶工程化监控接入与第三方接口的放置策略5.1 Spring Boot Admin 和 Actuator先监控再优化“spring boot实现监控”和“spring boot admin”经常被一起搜索。我的经验是不要等线上被卡死了才去看日志先把监控接好。最简单的接入方式是用 Spring Boot Admin。被监控的 Spring Boot 应用只需要引入dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency然后暴露所有常用端点management: endpoints: web: exposure: include: health,info,metrics,mappings再单独起一个 Spring Boot Admin Server 应用它通过 HTTP 去拉取客户端的/actuator/health、/actuator/metrics等端点信息。界面能看到内存、线程池、垃圾回收、HTTP 请求时长数据库连接池状态也能看到这是排查和优化之前的第一手数据。对于 MyBatis 来说监控的意义在于看慢 SQL、看缓存命中率、看连接池活跃数。如果没有这类指标后面所谓“你能写出更快的 SQL”都只是直觉判断不是数据判断。5.2 对外接口应该放在哪里独立服务还是业务服务内“spring boot对外提供的接口(给第三方)应该放在哪里?是单独的服务?还是放在对应的业务服务内”这是个很有意思的架构问题。我经历过几个项目最后形成的判断是这样的如果你们的体系是单一 Spring Boot 项目没有拆分微服务那第三方接口我建议独立放在一个controller包里比如com.example.demo.api.open和内部com.example.demo.controller区分开。原因是第三方接口面临的约束完全不同可能需要独立的鉴权方式如 API Key、签名、独立的限流策略、独立的版本号/v1/api/...。放同一个 Controller 里当然也能跑但后面会越来越乱。如果体系已经微服务化我会把对外开放接口做成独立的 BFF 层服务Backend For Frontend由它去调用内部的业务服务。这样做的好处是外部接口可以对多个内部服务做编排聚合又不让内部服务暴露过多能力还方便在 BFF 层统一做鉴权、限流和响应格式转换。独立服务不是万能的。如果你的团队很小或者接口数量也就两三个硬挤出一个小服务反而增加运维成本。这时候更务实的做法是在现有服务里划出一个独立模块从包名、配置、到返回结果都自成一套体系等哪天接口多了再拆。5.3 Spring Boot 版本升级时的注意事项最后补充一个和版本直接相关的实战提醒。从 Spring Boot 2.3 升到 2.6 的时候Spring Cloud 的 Bootstrap Context 默认关闭了如果你是靠bootstrap.yml拉配置的应用会发现配置中心突然不生效。从 2.6 升到 3.x 变化更大javax.*变成jakarta.*、spring.factories机制被 Autoconfiguration.imports 取代、MyBatis starter 也要换版本。升级前把这些试出来不然只改版本号跑一遍绿灯就以为完了后面上生产会打脸。我在一个项目里就吃过这个亏看着版本无害就从 2.3 跳到 2.6结果 Redis 配置和跨域拦截器全都变了行为排查了大半天才知道是某个自动配置的默认值变了。版本升级之后除了功能回归测试一定要把“配置项的生效行为”当作一等公民来测尤其是链路里同时存在 MyBatis、PostgreSQL 驱动、连接池、监控模块时每个组件都可能因为父工程依赖版本改变而换了解析路径。文章写到这最核心的整合要点已经全部展开。如果你正在搭一个新的 Spring Boot MyBatis PostgreSQL 项目我建议把版本搭配、数据源链接的两种含义、连接池大小、日志打印方式、二级缓存开关这五件事先定下来再写业务代码。最后再分享一个小技巧把mybatis.mapper-locations里的 XML 路径和你实际的 Mapper 接口包路径保持一致不要为了省事把 XML 全都扔到 classpath 根目录否则项目变大之后每找一个 SQL 都要翻半天文件那才是整合里最磨人的隐性成本。