《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 Boot | 3.2.x 或 3.3.x | 需要 JDK 17+ |
| Druid | com.alibaba:druid-spring-boot-3-starter:1.2.23 | 专为 Boot 3 适配的 starter |
| MyBatis-Plus | com.baomidou:mybatis-plus-spring-boot3-starter:3.5.7 | 官方提供 Boot 3 专用 starter |
对应 Maven 依赖示例:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-3-starter</artifactId> <version>1.2.23</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency>1.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.23,Maven 仲裁后旧包没有完全剔除,导致运行时部分类还是旧逻辑。
遇到这种情况,最快的方式是在druid-spring-boot-3-starter依赖上添加排除规则,把可能间接引入的旧druid核心包排掉,然后强制统一到 1.2.23:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-3-starter</artifactId> <version>1.2.23</version> <exclusions> <exclusion> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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-active=30时,min-idle设为 10 就够日常低峰期使用了。
2.3 探活机制与连接回收:理解 testWhileIdle 与 keep-alive
test-while-idle和test-on-borrow是两套完全不同的探活策略。test-on-borrow是每次从连接池拿连接时都执行一次validation-query,这种方式最安全但性能开销大,每次请求都会多一次查询。test-while-idle则是当连接空闲时间超过time-between-eviction-run-millis时,由后台回收线程检测一次,性能开销小得多。
我推荐组合是test-while-idle=true+test-on-borrow=false,配合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、WallFilter
3.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-millis=1000,超过 1 秒的 SQL 会自动打印到应用日志中。 - 连接池(Active 页签):看当前活跃连接数和等待次数。如果 ActiveCount 长期接近 max-active,说明连接池容量或 SQL 效率有问题。
- URI 监控:统计每个接口的请求次数与耗时,排查某个接口是否因为慢 SQL 拖垮了整体性能。
mergeSql参数值得单独说。默认情况下,相同结构但参数不同的 SQL 会被统计成多条记录,比如select * from user where id=1和select * from user where id=2会占两行,导致慢 SQL 分析很难看。开启filter.stat.merge-sql=true后,这类 SQL 会合并成一条,统计执行总次数和平均耗时,分析效率会高很多。
3.3 WallFilter:把 SQL 防火墙加入组合
Druid 的 WallFilter 相当于内置的 SQL 防火墙,能拦截常见的 SQL 注入攻击,比如or 1=1、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 "MyP@ssw0rd123"执行后会输出三行内容:privateKey、publicKey、password。其中password是数据库密码的密文,publicKey可以放到应用配置或环境变量中,privateKey只放在部署服务器的环境变量或管理系统中。
注意:如果数据库密码里包含特殊字符,比如#、@、$,使用命令行生成时建议把密码用双引号包起来,避免 bash 把特殊字符当作语法解析。生成后,把password字段的值复制到application.yml的spring.datasource.password中,再把publicKey放到环境变量DRUID_PUBLIC_KEY里,这里不直接明文写在 yml 中,是为了密钥管理和代码仓库隔离。
4.3 Spring Boot 3 接入 ConfigFilter
Spring Boot 3 中使用 ConfigFilter 的完整配置如下:
spring: datasource: druid: filter: config: enabled: true connection-properties: config.decrypt=true;config.decrypt.key=${DRUID_PUBLIC_KEY}config.decrypt=true表示对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 访问时被拒绝。- 项目里使用了网关代理,外部请求经过网关转发到应用时丢失了原始 IP,
allow判断就无法命中。
排查顺序建议先看应用日志有无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-enable=false,exclusions包含/druid/* - 数据库密码已用 ConfigTools 加密,私钥只存在于服务器环境变量
- 连接池参数根据压测结果设置,
max-wait不超过 10000 test-while-idle、keep-alive已开启,test-on-borrow保持关闭- WallFilter 已启用,上线前灰度观察 3 天,确认无业务 SQL 被误杀
每次 Redis、MySQL 这类基础组件的连接方式在框架升级后发生变化,最有效的应对方法就是把配置项一个个搞清楚,而不是从旧项目整段复制。连接池的核心逻辑无非是"连接从哪来、连接怎么保活、连接怎么释放"这三件事,围绕这三个问题去配置,很多问题其实可以提前预防,根本不需要等到线上报错再排查。