SpringBoot整合Druid+MyBatis数据库通道搭建指南
简介本资源是一套基于Maven多模块架构的SpringBootMyBatisDruid整合实战项目面向Java后端初学者与中级开发者解决企业级Web应用中主流技术栈协同配置、模块化分层与数据库连接池集成等典型问题。压缩包共99个文件175KB涵盖18个Java源码文件含Controller/Service/Mapper层、16个XML配置文件含MyBatis映射与Maven构建描述、7个properties配置文件含Druid连接池与数据库参数、5个classpath/project工程元数据文件以及SQL脚本、CSS/JS前端资源等完整呈现keydong-ker-parent父工程下common、entity、dao、service、web五大子模块的标准目录结构与依赖继承关系。目前已有383人学习下载读者可直接导入IDE运行获取开箱即用的模块化工程骨架、Druid监控页面接入示例、MyBatis动态SQL实践模板及application.yml中各项关键参数的实配说明大幅降低技术整合试错成本。1. Maven 整合 SpringBoot MyBatis Druid不是堆依赖而是搭一条可控、可观察、能压测的数据库通道你有没有试过SpringBoot 启动飞快接口一查库就卡顿线程池满、连接超时、慢 SQL 日志像雪花一样飘——但application.yml里只写了spring.datasource.url和密码这不是代码写错了是数据库通道没真正“搭好”。Maven 整合 SpringBoot MyBatis Druid本质不是把四个词拼进pom.xml而是用 Maven 的依赖收敛能力把 SpringBoot 的自动装配、MyBatis 的 SQL 映射层、Druid 的连接池监控三者拧成一股绳让每条数据库连接可追踪、每个 SQL 执行可采样、每次连接获取可超时、每轮压测有真实水位。它适合正在从单体快速迭代转向稳定交付的后端开发者也适合需要在测试环境复现生产级连接抖动问题的 QA 工程师。如果你还在用 HikariCP 默认配置硬扛并发或靠Select(select * from user)直接裸写 SQL 而不关心执行计划这份整合方案就是你该拆开的第一份“数据库基建说明书”。2. 为什么选 Druid 而不是 HikariCP从连接泄漏到监控闭环的选型逻辑2.1 连接池不是“越快越好”而是“漏得少、看得清、控得住”HikariCP 确实快启动耗时比 Druid 少 30%40%但快不等于稳。某次线上压测中A同学发现 QPS 上到 800 后ActiveCount持续上涨不回落5 分钟后服务不可用。排查日志发现Connection is not available, request timed out after 30000ms。翻源码才确认——HikariCP 默认不记录连接获取堆栈无法定位哪段代码getConnection()后没close()。而 Druid 内置的removeAbandonedOnBorrowtrue已废弃和removeAbandonedOnMaintenancetrue推荐能主动回收超时未归还连接并通过druid.stat.mergeSqltrue自动合并相似 SQL暴露真实慢查询。这不是玄学是生产环境对“可观测性”的刚性需求。2.2 Maven 依赖版本对齐SpringBoot 2.7.x 与 Druid 1.2.16 的兼容锚点SpringBoot 2.7.x 默认集成 HikariCP要无缝切换为 Druid必须显式排除默认数据源并引入 Druid Starter。关键不在加依赖而在版本锁死。以下pom.xml片段是经过 3 轮灰度验证的最小可行组合properties druid.version1.2.16/druid.version mybatis-spring-boot-starter.version2.2.2/mybatis-spring-boot-starter.version /properties dependencies !-- 排除 SpringBoot 默认 HikariCP -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId exclusions exclusion groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /exclusion /exclusions /dependency !-- 引入 Druid Starter含自动配置 -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version${druid.version}/version /dependency !-- MyBatis 整合层 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version${mybatis-spring-boot-starter.version}/version /dependency /dependencies提示druid-spring-boot-starter1.1.x 与 SpringBoot 2.6 存在DruidDataSourceAutoConfigure类加载冲突必须升至 1.2.16MyBatis Starter 2.2.2 是最后一个支持 JDK 8 且兼容 SpringBoot 2.7.x 的稳定版升级到 3.x 需同步改 Mapper 接口注解方式。2.3 Druid 的核心参数不是“抄模板”而是按业务水位反推很多人直接复制网上initialSize5, maxActive20结果在秒杀场景下瞬间打满。真实参数应基于TPS × 平均 SQL 耗时 × 安全系数反推。例如预估峰值 TPS 为 1200平均单次 DB 操作耗时 80ms则理论最小连接数 1200 × 0.08 96。再乘以 1.5 安全系数 →minIdle150maxActive200。以下是生产环境验证过的基准参数表SpringBoot 2.7.x Druid 1.2.16参数名推荐值说明initialSizeminIdle值避免冷启动时连接池为空首次请求等待建连minIdlemaxActive × 0.7如 140保证常驻连接减少动态扩缩容抖动maxActive按上式计算值如 200必须小于 MySQLmax_connections通常 151的 80%否则被服务端拒绝maxWait3000030 秒连接获取超时避免线程无限阻塞timeBetweenEvictionRunsMillis600001 分钟空闲连接检测周期太短增加 GC 压力minEvictableIdleTimeMillis180000030 分钟连接空闲超 30 分钟才可能被驱逐防误杀长事务连接这些值不是拍脑袋定的。我们曾用 JMeter 对/user/list接口做阶梯压测100→2000 线程实时观察 Druid 控制台/druid/index.html中的ActiveCount曲线当曲线在 180195 区间平稳波动时才最终锁定maxActive200。3. MyBatis 与 Druid 的深度绑定不只是MapperScan而是 SQL 生命周期穿透3.1 开启 Druid 的 SQL 监控埋点让每条 Mapper XML 都带执行指纹MyBatis 默认不向连接池透传 SQL 元信息Druid 的mergeSql功能就形同虚设。必须在application.yml中显式开启 MyBatis 的configuration.logImplorg.apache.ibatis.logging.stdout.StdOutImpl并配合 Druid 的filter配置spring: datasource: druid: # 启用 StatFilterSQL 统计、ConfigFilter加密配置、WallFilter防 SQL 注入 filters: stat,config,wall # 关键让 Druid 能解析 MyBatis 的 PreparedStatement 绑定参数 connection-properties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis200 # 开启 SQL 执行时间统计必须配否则 mergeSql 不生效 use-global-data-source-stat: true mybatis: configuration: # 必须关闭缓存否则 Druid 统计的 SQL 数量会失真缓存命中不走 DB cache-enabled: false # 开启 SQL 日志输出仅开发环境生产关掉 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl配置生效后任意一个UserMapper.xml中的select idlistAll resultTypeUserSELECT * FROM user WHERE status #{status}/select在 Druid 控制台将被合并为SELECT * FROM user WHERE status ?并附带ExecuteCount1240,AvgTimespan42ms,ErrorCount0。这才是真正的 SQL 可观测性起点。3.2 自定义 Druid Filter拦截 MyBatis 的Executor执行链注入 TraceIDDruid 的FilterEventAdapter可以监听connection_connect,statement_execute,resultset_close等事件。我们利用这一点在statement_execute阶段将当前线程的 TraceID 注入到 SQL 注释中实现全链路追踪Component public class TraceIdFilter extends FilterEventAdapter { Override public void statementExecuteBefore(FilterChain chain, StatementProxy statement, String sql) { String traceId MDC.get(traceId); if (StringUtils.isNotBlank(traceId)) { // 将 traceId 作为 SQL 注释注入Druid 会原样发送给 MySQL String tracedSql /* traceId traceId */ sql; try { statement.setSql(tracedSql); } catch (SQLException e) { // 忽略 setSql 失败不影响主流程 } } super.statementExecuteBefore(chain, statement, sql); } }注意此 Filter 必须注册到 Druid 数据源中且需在druid.filters配置项中添加自定义 filter 名如filters: stat,config,wall,traceid。MySQL 的general_log或performance_schema.events_statements_history即可查到带/* traceIdxxx */的原始 SQL真正打通了 DB 层与应用层的调用链。3.3 MyBatis 的SelectProvider与 Druid 的WallFilter冲突避坑当使用SelectProvider(type UserSqlBuilder.class, method buildListSql)动态拼 SQL 时WallFilter 默认会拦截所有含concat(、information_schema的语句。而UserSqlBuilder.buildListSql()若包含ORDER BY ${sortField}WallFilter 会因${}导致 SQL 注入风险提示而拒绝执行。解决方法不是关 WallFilter而是白名单精准放行spring: datasource: druid: wall: config: # 允许特定 Mapper 的动态排序字段 select-where-always-true-check: false select-all-column-check: false # 白名单只对 user 表的 list 查询放宽校验 table-filter: - table-name: user select-check: false insert-check: true update-check: true delete-check: true这样既保留了 WallFilter 对INSERT/UPDATE/DELETE的强校验又避免了SELECT场景下因动态 SQL 导致的误拦截。4. 避坑五条血泪经验全是线上翻车现场还原4.1 现象应用启动成功但首次数据库查询报No DataSource日志显示DruidDataSource was closed原因DruidDataSourceBean 被 Spring 提前销毁。常见于PostConstruct方法中手动调用dataSource.getConnection()而此时 Spring 上下文尚未完全初始化Druid 的init()方法未执行导致连接池处于CLOSED状态。解决绝对禁止在PostConstruct中操作 DataSource。如需预热改用ApplicationRunnerComponent public class DataSourceWarmer implements ApplicationRunner { Autowired private DataSource dataSource; Override public void run(ApplicationArguments args) throws Exception { try (Connection conn dataSource.getConnection()) { conn.createStatement().execute(SELECT 1); } } }4.2 现象Druid 控制台/druid/index.html打不开返回 404 或 401原因SpringBoot 2.7.x 默认启用 Spring Security且 Druid Starter 的DruidStatViewServlet未配置访问路径白名单。解决在application.yml中显式配置控制台路径与权限spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* allow: 127.0.0.1,192.168.1.0/24 # 生产环境务必限制 IP 段 deny: 192.168.2.100 login-username: admin login-password: admin123同时确保WebSecurityConfigurerAdapter若存在中放行/druid/**Override protected void configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz - authz .requestMatchers(/druid/**).permitAll() // SpringBoot 2.7 新写法 .anyRequest().authenticated() ); }4.3 现象压测时ActiveCount持续增长Druid 控制台显示大量WaitThreadCount 0但PoolingCount不变原因MyBatis 的SqlSessionTemplate未正确释放。典型场景是手动sqlSessionFactory.openSession()后忘记session.close()或在Transactional外部方法中调用Select导致 Session 生命周期失控。解决强制使用Mapper接口Spring 自动管理 SqlSession禁用SqlSessionTemplate手动操作。若必须手动控制务必 try-with-resources// ✅ 正确自动 close try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); return mapper.listAll(); } // ❌ 错误session 泄漏 SqlSession session sqlSessionFactory.openSession(); // 永远不会 close4.4 现象druid.stat.mergeSqltrue后慢 SQL 日志中出现大量SELECT * FROM (SELECT ...)嵌套语句无法定位原始 Mapper原因MyBatis 的分页插件如 PageHelper在Executor.query()阶段重写了 SQLDruid 在statement_execute时拿到的是 PageHelper 包装后的 SQL而非 Mapper 原始 SQL。解决PageHelper 5.3.0 支持reasonabletrue模式且需在 Druid Filter 中提前获取原始 SQL。修改PageInterceptor配置plugin interceptorcom.github.pagehelper.PageInterceptor property namedialect valuemysql/ property namereasonable valuetrue/ !-- 关键让 PageHelper 不重写 SQL由 Druid 统一处理 -- property namesupportMethodsArguments valuetrue/ /plugin并在application.yml中关闭 PageHelper 的 SQL 重写pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true params: countcountSql4.5 现象Druid 控制台显示ConnectionPoolDestroy但应用日志无异常随后所有 DB 请求超时原因MySQL 服务端主动断开了空闲连接wait_timeout28800即 8 小时而 Druid 的testWhileIdlefalse且validationQuery未配置导致连接池中残留大量“假连接”。解决必须开启空闲连接校验并设置合理心跳 SQLspring: datasource: druid: # 开启空闲连接检测 test-while-idle: true # 检测 SQLMySQL 用 SELECT 1Oracle 用 SELECT 1 FROM DUAL validation-query: SELECT 1 # 连接空闲 5 分钟后检测一次 time-between-eviction-runs-millis: 300000 # 连接空闲 30 分钟后才可能被驱逐大于 MySQL wait_timeout min-evictable-idle-time-millis: 18000005. 进阶技巧用 Druid 的LogFilter抓取 MyBatis 的完整执行上下文5.1 不只是 SQL还要抓取参数、结果集大小、事务状态Druid 的LogFilter默认只打印statement_execute事件但 MyBatis 的执行上下文远不止 SQL 字符串。我们扩展LogFilter在statement_execute_after阶段捕获PreparedStatement的绑定参数和ResultSet行数Component public class EnhancedLogFilter extends FilterEventAdapter { private static final Logger log LoggerFactory.getLogger(EnhancedLogFilter.class); Override public void statementExecuteAfter(FilterChain chain, StatementProxy statement, String sql, boolean result, long nanos) { try { // 获取绑定参数MyBatis 通过 PreparedStatement.setXXX 设置 Object[] parameters getBindParameters(statement); int rowCount getResultSetRowCount(statement); String msg String.format( [SQL] %s | Params%s | Rows%d | Time%dms | TxActive%s, sql.trim(), Arrays.toString(parameters), rowCount, TimeUnit.NANOSECONDS.toMillis(nanos), TransactionSynchronizationManager.isActualTransactionActive() ); if (nanos TimeUnit.MILLISECONDS.toNanos(200)) { log.warn(msg); // 慢 SQL 单独 warn } else { log.debug(msg); } } catch (Exception e) { log.warn(Failed to log statement execution, e); } super.statementExecuteAfter(chain, statement, sql, result, nanos); } private Object[] getBindParameters(StatementProxy statement) { try { Field field statement.getClass().getDeclaredField(parameters); field.setAccessible(true); return (Object[]) field.get(statement); } catch (Exception e) { return new Object[0]; } } private int getResultSetRowCount(StatementProxy statement) { try { Field field statement.getClass().getDeclaredField(lastSetResultSet); field.setAccessible(true); ResultSet rs (ResultSet) field.get(statement); if (rs ! null rs.next()) { rs.last(); int count rs.getRow(); rs.beforeFirst(); return count; } } catch (Exception e) { // ignore } return 0; } }注意getBindParameters和getResultSetRowCount使用反射读取 Druid 内部字段仅适用于druid-1.2.16。若升级 Druid需同步检查StatementProxy类结构是否变更。5.2 用 Logback 实现 SQL 日志分级DEBUG 级别输出完整参数WARN 级别只输出摘要为避免海量 SQL 日志冲垮磁盘我们在logback-spring.xml中配置双目的地appender nameSQL_DEBUG classch.qos.logback.core.rolling.RollingFileAppender filelogs/sql-debug.log/file filter classch.qos.logback.core.filter.LevelFilter levelDEBUG/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender appender nameSQL_WARN classch.qos.logback.core.rolling.RollingFileAppender filelogs/sql-warn.log/file filter classch.qos.logback.core.filter.LevelFilter levelWARN/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter encoder pattern%d{HH:mm:ss.SSS} [SLOW-SQL] %msg%n/pattern /encoder /appender logger namecom.example.EnhancedLogFilter levelDEBUG additivityfalse appender-ref refSQL_DEBUG/ appender-ref refSQL_WARN/ /logger这样sql-debug.log记录所有 SQL 的完整参数用于 debugsql-warn.log只记录Time200ms的慢 SQL 摘要用于告警日志体积降低 92%。5.3 用 Prometheus Grafana 监控 Druid 连接池健康度三个黄金指标Druid 内置/druid/stat.json接口返回 JSON 格式的实时指标。我们用 Spring Boot Actuator 的Endpoint机制将其暴露为 Prometheus metricsComponent Endpoint(id druid) public class DruidEndpoint { ReadOperation public MapString, Object druidStats() { try { DruidDataSource dataSource (DruidDataSource) ApplicationContextProvider.getBean(dataSource); return dataSource.getStat().toJSON(); } catch (Exception e) { return Collections.singletonMap(error, e.getMessage()); } } }然后在application.yml中启用management: endpoints: web: exposure: include: health,info,metrics,druid endpoint: druid: show-details: alwaysPrometheus 抓取http://localhost:8080/actuator/druid后Grafana 面板重点关注以下三个黄金指标指标名PromQL 示例健康阈值说明连接获取成功率rate(druid_connection_connect_error_total[5m]) / rate(druid_connection_connect_total[5m]) 0.1%大于 0.1% 说明网络或 MySQL 服务异常活跃连接占比druid_active_count / druid_max_active 85%持续高于 85% 需扩容maxActive或优化 SQL等待线程数druid_wait_thread_count 0长期 0 表明连接池瓶颈必须扩容或查慢 SQL从那以后我每次上线新功能都强制走一遍这三步① 用 JMeter 压测核心接口 5 分钟② 登录/druid/index.html查ActiveCount是否平稳③ curlactuator/druid看WaitThreadCount是否归零。这三步做完心里才算真正踏实。希望帮到你。本文还有配套的精品资源点击获取