
《Druid 正确使用姿势全解析Spring Boot 3 MyBatis-Plus 集成实战指南》做 Spring Boot 3 迁移的时候我把一个老项目的连接池从 Druid 换成了 HikariCP结果压测阶段又老老实实换了回来。原因很现实HikariCP 性能确实能打但 Druid 的监控面板在排障时实在太顺手了线上出现慢 SQL、连接泄漏打开/druid/sql.html一眼就能定位到问题这种看得见的能力在生产环境里比那点性能差异值钱得多。不过这次切换并没有想象中顺利Spring Boot 3 从javax迁移到jakarta命名空间直接把很多老版本的 Druid starter 打回原形启动就报错。这篇文章把 Spring Boot 3 MyBatis-Plus Druid 这套组合的版本选型、连接池参数设置、监控面板启用、数据库密码加密、常见踩坑排查完整梳理一遍给正在做升级或者新开项目的同学做个参考。1. Spring Boot 3 下的 Druid 版本选型先避开 javax/jakarta 迁移的坑1.1 为什么你从 Spring Boot 2 搬过来的配置会炸Spring Boot 3 的一个重大底层变化是 Servlet API 从javax.servlet迁移到了jakarta.servlet。这套命名空间迁移的影响范围远比想象中大所有基于 Servlet API 编译的三方库都必须重新适配否则在 Spring Boot 3 的 Tomcat 容器里根本加载不了。Druid 早期版本1.2.18 及之前默认按javax编译你直接引入druid-spring-boot-starter启动时大概率会看到类似ClassNotFoundException: javax.servlet.Filter的报错或者监控页面即使配好了也访问不到。我一开始没有意识到这个问题以为只是兼容性 warning直到本地启动直接失败看了堆栈才发现是整个 Servlet 命名空间对不上。这个坑在 Spring Boot 2.7 时代被完全掩盖了因为 2.7 还在用javax很多项目升级到 Boot 3 时第一个炸的就是连接池 starter。1.2 坐标选择与版本对应关系Druid 官方在 1.2.19 之后单独提供了druid-spring-boot-3-starter坐标注意这个坐标不是简单换个名字而是内部依赖的 Servlet API 相关代码全部切换到了jakarta。我建议直接使用 1.2.23 或更新版本因为 1.2.20 到 1.2.22 之间还修复了一些 StatFilter 与 Spring Boot 3 自动装配的兼容问题太老的版本即使能启动监控模块也可能存在隐性 bug。下面是我在项目中验证过的版本组合组件推荐坐标 / 版本备注Spring Boot3.2.x 或 3.3.x需要 JDK 17Druidcom.alibaba:druid-spring-boot-3-starter:1.2.23专为 Boot 3 适配的 starterMyBatis-Pluscom.baomidou:mybatis-plus-spring-boot3-starter:3.5.7官方提供 Boot 3 专用 starter对应 Maven 依赖示例dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-3-starter/artifactId version1.2.23/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependency1.3 依赖冲突的自查与清理如果你在项目中同时保留了旧版本的druid-spring-boot-starter或多个 Druid 核心包mvn dependency:tree会告诉你真相。我在一次排查中发现项目里同时存在com.alibaba:druid:1.2.8和druid-spring-boot-3-starter:1.2.23传递进来的com.alibaba:druid:1.2.23Maven 仲裁后旧包没有完全剔除导致运行时部分类还是旧逻辑。遇到这种情况最快的方式是在druid-spring-boot-3-starter依赖上添加排除规则把可能间接引入的旧druid核心包排掉然后强制统一到 1.2.23dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-3-starter/artifactId version1.2.23/version exclusions exclusion groupIdcom.alibaba/groupId artifactIddruid/artifactId /exclusion /exclusions /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.23/version /dependency这一套操作做完版本问题基本就稳了。记住一个原则Boot 3 项目里所有和 Web/Servlet 相关的三方库都要检查是否有jakarta适配版本不光是 Druid。2. 连接池核心参数逐项拆解为什么这些数字不能照搬默认值2.1 一份能落地的连接池配置模板很多人从网上复制一段 Druid 配置就能跑起来但流量一上来就出问题本质是不理解每个参数的含义。我先给一份我在多个生产项目中验证过的模板然后逐个解释关键参数spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 10000 time-between-eviction-run-millis: 60000 min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 600000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false keep-alive: true pool-prepared-statements: true max-open-prepared-statements: 20 filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000这里的initial-size是连接池启动后立即创建的物理连接数我习惯把它和min-idle保持一致避免刚启动时流量突然进来还要现建连接。max-active是最大活跃连接数max-wait是获取连接的最大等待时间单位毫秒。生产环境如果拿不到连接超过max-wait会直接抛异常而不是无限阻塞这比 HikariCP 默认的 30 秒更直观。2.2 连接池大小按业务算不按模板抄max-active设置多少不能拍脑袋。一个比较实用的估算方法是连接数 峰值并发请求数 × 单请求平均持有连接时间 / 1000。举个例子假设一个接口峰值 QPS 是 200单次请求查询数据库平均耗时 30ms那么需要的活跃连接数约等于 200 × 0.03 6 个。但如果接口开启了事务事务内又查了 3 张表单次请求持有连接的时间可能从 30ms 放大到 100ms这时需要的连接数就变成 200 × 0.1 20 个。这也是为什么建议在压测环境下用真实接口数据跑一轮再决定max-active。min-idle是连接池中保持的最小空闲连接数。注意不要把min-idle设置得比max-active还大这会导致连接池永远处于高占用状态。我的习惯是min-idle不超过max-active的 1/3比如max-active30时min-idle设为 10 就够日常低峰期使用了。2.3 探活机制与连接回收理解 testWhileIdle 与 keep-alivetest-while-idle和test-on-borrow是两套完全不同的探活策略。test-on-borrow是每次从连接池拿连接时都执行一次validation-query这种方式最安全但性能开销大每次请求都会多一次查询。test-while-idle则是当连接空闲时间超过time-between-eviction-run-millis时由后台回收线程检测一次性能开销小得多。我推荐组合是test-while-idletruetest-on-borrowfalse配合validation-query: SELECT 1。这套组合经历了典型的MySQL 8小时断开问题数据库端因为 wait_timeout 断开了空闲连接客户端不知道拿到的连接已经失效第一次执行 SQL 直接报CommunicationsException。开启keep-alive后Druid 的守护线程会定期向空闲连接发送探测语句把已经死掉的连接提前剔除这个坑就基本不会再遇到。min-evictable-idle-time-millis是连接最少空闲多久才允许被回收time-between-eviction-run-millis是回收线程多长时间跑一次。这两个参数要配合着看回收线程每 60 秒跑一次但只有空闲超过 300 秒的连接才被回收。如果项目中有定时任务在低峰期也会使用数据库就把min-evictable-idle-time-millis调大一些避免连接被频繁回收又重建。2.4 连接泄漏的检测思路Druid 提供了removeAbandoned参数可以强制回收被业务代码忘记释放的连接。但我建议生产环境不要直接开启remove-abandoned因为它可能把一支正在执行的慢 SQL 连接误杀导致数据写一半就断掉。更稳妥的做法是配合监控面板的连接池页签观察ActiveCount的变化趋势配合max-wait超时异常来定位可能是哪段业务代码持有连接时间异常。3. 监控面板启用与数据解读StatViewServlet、WebStatFilter、WallFilter3.1 监控功能全开需要哪三个组件Druid 的监控体系由三个核心组件组成StatViewServlet提供 Web 监控页面WebStatFilter拦截请求统计 URL 访问情况StatFilter统计 SQL 执行情况。三个缺一不可。下面是一份可以直接用的配置spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: monitor login-password: monitor123 reset-enable: false allow: 127.0.0.1 deny: 192.168.1.100 web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*allow和deny控制访问监控页面的 IP 白名单和黑名单注意这里的allow在部分版本里不支持网段只认单个 IP 或空放行所有。生产环境如果监控页面挂在公网强烈建议打开login-username和login-password并把reset-enable设为false否则任何人访问/druid/*都可以点击重置按钮清空监控统计数据。web-stat-filter的exclusions必须包含/druid/*否则监控请求会被统计过滤器拦截导致监控页面本身无法访问。这个配置我踩过坑当时只在stat-view-servlet配了 url-pattern没在web-stat-filter里排除结果/druid/index.html一直返回 404排查了半天才发现是请求在到达 Servlet 之前就被过滤掉了。3.2 监控页面怎么读懂慢 SQL 与连接池状态配置完成后浏览器访问http://localhost:8080/druid/index.html输入用户名密码进入监控面板。我日常最关注三个页签SQL 监控按执行次数和耗时排序观察是否有 SQL 出现执行次数少但耗时极高的情况。配合filter.stat.slow-sql-millis1000超过 1 秒的 SQL 会自动打印到应用日志中。连接池Active 页签看当前活跃连接数和等待次数。如果 ActiveCount 长期接近 max-active说明连接池容量或 SQL 效率有问题。URI 监控统计每个接口的请求次数与耗时排查某个接口是否因为慢 SQL 拖垮了整体性能。mergeSql参数值得单独说。默认情况下相同结构但参数不同的 SQL 会被统计成多条记录比如select * from user where id1和select * from user where id2会占两行导致慢 SQL 分析很难看。开启filter.stat.merge-sqltrue后这类 SQL 会合并成一条统计执行总次数和平均耗时分析效率会高很多。3.3 WallFilter把 SQL 防火墙加入组合Druid 的 WallFilter 相当于内置的 SQL 防火墙能拦截常见的 SQL 注入攻击比如or 11、union select这类语句。在配置中启用spring: datasource: druid: filter: wall: enabled: true config: multi-statement-allow: falsemulti-statement-allow默认是 false即禁止一次执行多条 SQL。如果你用了 MyBatis-Plus 的某些批量操作或者存储过程发现 SQL 被拦截再按需开启。我没有在生产环境开启多语句支持因为绝大多数滥用的 SQL 注入都是通过拼接多语句实现的保持关闭更安全。WallFilter 的拦截日志会通过 Druid 的日志体系输出上线前建议先在一个灰度实例观察几天确认没有误杀正常业务 SQL 后再全量放开。4. 数据库密码加密实战用 Druid ConfigFilter 把明文密码从配置里赶出去4.1 为什么要专门做一道加密绝大多数项目的数据库密码都明晃晃写在application.yml里一旦代码仓库泄露或者开发人员的本机被侵入数据库就等于裸奔。Druid 的 ConfigFilter 使用非对称加密算法配置中存放的是数据库密码的密文和一把公钥真正的私钥只在运行环境里存在。哪怕有人拿到整个配置文件没有私钥也无法还原出明文密码。这套方案尤其适合 Spring Boot 多环境部署场景开发环境、测试环境、生产环境各用一套密钥每个环境的 application.yml 里只放各自的密文而不是像明文密码那样所有环境共用一份。4.2 用 ConfigTools 生成密钥与密文Druid 自带一个名为ConfigTools的工具类不需要额外安装直接用 jar 包执行java -cp druid-1.2.23.jar com.alibaba.druid.filter.config.ConfigTools MyPssw0rd123执行后会输出三行内容privateKey、publicKey、password。其中password是数据库密码的密文publicKey可以放到应用配置或环境变量中privateKey只放在部署服务器的环境变量或管理系统中。注意如果数据库密码里包含特殊字符比如#、、$使用命令行生成时建议把密码用双引号包起来避免 bash 把特殊字符当作语法解析。生成后把password字段的值复制到application.yml的spring.datasource.password中再把publicKey放到环境变量DRUID_PUBLIC_KEY里这里不直接明文写在 yml 中是为了密钥管理和代码仓库隔离。4.3 Spring Boot 3 接入 ConfigFilterSpring Boot 3 中使用 ConfigFilter 的完整配置如下spring: datasource: druid: filter: config: enabled: true connection-properties: config.decrypttrue;config.decrypt.key${DRUID_PUBLIC_KEY}config.decrypttrue表示对password字段进行解密config.decrypt.key指定公钥来源。启动时 Druid 会先读取配置中的密文再用公钥解密得到真实密码去建立连接。如果你参考过若依RuoYi这类框架的做法会发现它们通常把 Druid 配置封装成一个独立的DruidConfig配置类在 Java 代码中手动组装 connection-properties。这样做的好处是可以通过配置中心动态下发公钥不修改代码就能切换密钥。我的建议是如果项目没有引入配置中心直接在 yml 里用环境变量引用即可如果引入了 Nacos 或 Apollo则用配置类方式更灵活。4.4 密钥保管与轮换的注意点privateKey绝对不允许进入 Git 仓库也不允许打印到日志中。我见过一个线上问题开发者在启动日志里打印了 config 属性结果把公钥和私钥都带了出来等于加密方案完全失效。数据库密码变更时需要用新密码重新执行 ConfigTools 生成新的密文、公钥、私钥然后把新的password更新到配置中新的私钥更新到服务器环境变量并重启应用。如果服务器上同时有多个实例记得所有实例的环境变量都要同步更新否则会出现部分实例解密失败。这个流程最好写成脚本或接入发布系统避免手工操作遗漏。5. MyBatis-Plus 集成细节分页插件、多数据源与 Druid 的协同5.1 选对 starter 坐标MyBatis-Plus 在 3.5.4 之前提供的mybatis-plus-boot-starter还是基于javax编译直接用在 Spring Boot 3 项目中启动时会遇到 Mapper 接口扫描异常或者运行时反射调用失败。官方在 3.5.4 以后正式提供了mybatis-plus-spring-boot3-starter坐标我建议使用 3.5.7 以上版本。这个问题最有迷惑性的地方在于报错信息通常不直接提 MyBatis-Plus而是抛一个关于 MapperFactoryBean 或 SqlSessionFactory 的NoClassDefFoundError很容易让人误判成数据源配置问题排查方向跑偏到 Druid 连接池上。所以项目一旦出现奇怪的 MyBatis 初始化异常先检查 starter 坐标是不是 boot3 版本。5.2 分页插件与 WallFilter 的兼容问题MyBatis-Plus 分页依赖MybatisPlusInterceptor定义如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }这个拦截器与 Druid 本身没有直接关系但当 Druid 的 WallFilter 开启时分页插件生成的 SQL 包含LIMIT关键字通常会被 WallFilter 认为是正常语句。如果你在分页查询时报了 SQL 被拦截检查一下 WallFilter 的multi-statement-allow或者delete-allow等配置是否过严把对应功能在 WallFilter 白名单中放行即可。5.3 多数据源与事务边界项目里用到多数据源时最流行的方案是 baomidou 的dynamic-datasource。在 Spring Boot 3 中使用时需要引入dynamic-datasource-spring-boot3-starter并在配置中把 Druid 作为底层数据源spring: datasource: dynamic: primary: master strict: true datasource: master: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/master druid: initial-size: 5 max-active: 20 slave: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/slave druid: initial-size: 3 max-active: 10事务边界要特别注意DS注解只影响数据源路由如果在一个Transactional事务内切换数据源Spring 事务管理器已经通过DataSourceTransactionManager绑定了第一个数据源后面的切换不会生效而且可能造成连接管理异常。我的经验是跨数据源操作最好不要放在同一个事务里拆成两个独立事务用补偿逻辑处理比强行在一个事务内切换数据源要稳得多。5.4 启动顺序与初始化排查Druid 数据源在 Spring 容器启动时初始化MyBatis-Plus 的 SqlSessionFactory 随后创建。如果你在配置中开启了 Druid 的异步初始化应用启动可能先返回成功随后 Mapper 首次执行查询时才暴露连接失败这种问题在本地开发时很难发现。建议在本地环境关闭异步初始化让启动失败快速暴露生产环境确认连接池稳定后再打开异步初始化。6. 常见问题完整排查链路启动失败、监控 404、连接池爆满6.1 启动失败先看依赖树现象Spring Boot 3 项目引入 Druid starter 后启动直接报错堆栈里出现javax.servlet相关的类找不到。第一步执行mvn dependency:tree确认实际引用的 Druid starter 版本。如果发现是druid-spring-boot-starter而非druid-spring-boot-3-starter直接在 pom 里替换坐标。第二步检查是否有多版本 Druid 核心包用 1.2.23 统一版本。第三步本地 clean 后重新编译有些 IDE 的缓存会让旧的类残留。这个问题的根因 80% 是坐标选错20% 是版本号太老。先不要动数据源连接参数先把依赖树捋清楚否则越改越乱。6.2 监控 404检查 exclusions 与 allow现象配置了stat-view-servlet和web-stat-filter但访问/druid/index.html返回 404。我从实战里总结出三个最可能的点web-stat-filter.exclusions没有包含/druid/*导致监控请求被过滤器拦掉在监控请求到达 Servlet 之前就直接返回了空响应。stat-view-servlet.allow配置为空数组或仅允许本机 IP本地访问没问题但通过服务器 IP 访问时被拒绝。项目里使用了网关代理外部请求经过网关转发到应用时丢失了原始 IPallow判断就无法命中。排查顺序建议先看应用日志有无blocked by stat view servlet之类的安全拦截提示再确认网络链路是否经过代理最后检查配置中的 URL 路径是否与请求一致。6.3 连接池被打满一个定位实例现象某个高峰期应用突然大量出现Connection is not available, request timed out数据库本身 CPU 和内存都不高但 Druid 连接池一直处于满状态。我的排查思路分三步第一步打开 Druid 监控面板的 Active 页签看当前活跃连接被哪类 SQL 占满。如果能看到某个 Mapper 方法长期持有连接不释放基本可以锁定业务代码问题。第二步检查事务边界。我实际遇到过一个问题定时任务方法上标注了Transactional方法内部又调用了远程 HTTP 接口HTTP 请求等响应超时 30 秒数据库事务始终不提交连接一直被占用。这个锅不在 Druid事务和远程调用混在一起才是根源。解决办法是调整事务边界远程调用放到事务外或者缩短超时时间。第三步看max-wait是否设置得过大。如果设置成 60000 毫秒长时间等待的请求会在调用方堆积进一步加剧连接池压力。建议max-wait控制在 5~10 秒之间宁愿快速失败让上游重试也不要全部阻塞在连接池上。6.4 把坑前置化上线前的自检清单经历过几次线上问题后我整理了一张自检清单每次新项目上线前都会过一遍确认 Druid 使用druid-spring-boot-3-starter版本不低于 1.2.23确认 MyBatis-Plus 使用mybatis-plus-spring-boot3-starter监控面板已开启reset-enablefalseexclusions包含/druid/*数据库密码已用 ConfigTools 加密私钥只存在于服务器环境变量连接池参数根据压测结果设置max-wait不超过 10000test-while-idle、keep-alive已开启test-on-borrow保持关闭WallFilter 已启用上线前灰度观察 3 天确认无业务 SQL 被误杀每次 Redis、MySQL 这类基础组件的连接方式在框架升级后发生变化最有效的应对方法就是把配置项一个个搞清楚而不是从旧项目整段复制。连接池的核心逻辑无非是连接从哪来、连接怎么保活、连接怎么释放这三件事围绕这三个问题去配置很多问题其实可以提前预防根本不需要等到线上报错再排查。