☰
SpringBoot整合Druid+MyBatis数据库通道搭建指南
2026/10/13 3:27:21 网站建设 项目流程

简介:本资源是一套基于Maven多模块架构的SpringBoot+MyBatis+Druid整合实战项目,面向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 内置的removeAbandonedOnBorrow=true(已废弃)和removeAbandonedOnMaintenance=true(推荐)能主动回收超时未归还连接,并通过druid.stat.mergeSql=true自动合并相似 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.version>1.2.16</druid.version> <mybatis-spring-boot-starter.version>2.2.2</mybatis-spring-boot-starter.version> </properties> <dependencies> <!-- 排除 SpringBoot 默认 HikariCP --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <exclusions> <exclusion> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入 Druid Starter(含自动配置) --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <!-- MyBatis 整合层 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-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.16;MyBatis Starter 2.2.2 是最后一个支持 JDK 8 且兼容 SpringBoot 2.7.x 的稳定版,升级到 3.x 需同步改 Mapper 接口注解方式。

2.3 Druid 的核心参数不是“抄模板”,而是按业务水位反推

很多人直接复制网上initialSize=5, maxActive=20,结果在秒杀场景下瞬间打满。真实参数应基于TPS × 平均 SQL 耗时 × 安全系数反推。例如:预估峰值 TPS 为 1200,平均单次 DB 操作耗时 80ms,则理论最小连接数 = 1200 × 0.08 = 96。再乘以 1.5 安全系数 →minIdle=150,maxActive=200。以下是生产环境验证过的基准参数表(SpringBoot 2.7.x + Druid 1.2.16):

参数名推荐值说明
initialSizeminIdle值避免冷启动时连接池为空,首次请求等待建连
minIdlemaxActive × 0.7(如 140)保证常驻连接,减少动态扩缩容抖动
maxActive按上式计算值(如 200)必须小于 MySQLmax_connections(通常 151)的 80%,否则被服务端拒绝
maxWait30000(30 秒)连接获取超时,避免线程无限阻塞
timeBetweenEvictionRunsMillis60000(1 分钟)空闲连接检测周期,太短增加 GC 压力
minEvictableIdleTimeMillis1800000(30 分钟)连接空闲超 30 分钟才可能被驱逐,防误杀长事务连接

这些值不是拍脑袋定的。我们曾用 JMeter 对/user/list接口做阶梯压测(100→2000 线程),实时观察 Druid 控制台/druid/index.html中的ActiveCount曲线,当曲线在 180~195 区间平稳波动时,才最终锁定maxActive=200。


3. MyBatis 与 Druid 的深度绑定:不只是@MapperScan,而是 SQL 生命周期穿透

3.1 开启 Druid 的 SQL 监控埋点:让每条 Mapper XML 都带执行指纹

MyBatis 默认不向连接池透传 SQL 元信息,Druid 的mergeSql功能就形同虚设。必须在application.yml中显式开启 MyBatis 的configuration.logImpl=org.apache.ibatis.logging.stdout.StdOutImpl并配合 Druid 的filter配置:

spring: datasource: druid: # 启用 StatFilter(SQL 统计)、ConfigFilter(加密配置)、WallFilter(防 SQL 注入) filters: stat,config,wall # 关键:让 Druid 能解析 MyBatis 的 PreparedStatement 绑定参数 connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=200 # 开启 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 id="listAll" resultType="User">SELECT * FROM user WHERE status = #{status}</select>,在 Druid 控制台将被合并为SELECT * FROM user WHERE status = ?,并附带ExecuteCount=1240,AvgTimespan=42ms,ErrorCount=0。这才是真正的 SQL 可观测性起点。

3.2 自定义 Druid Filter:拦截 MyBatis 的Executor执行链,注入 TraceID

Druid 的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即可查到带/* traceId=xxx */的原始 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。如需预热,改用ApplicationRunner:

@Component 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(); // 永远不会 close

4.4 现象:druid.stat.mergeSql=true后,慢 SQL 日志中出现大量SELECT * FROM (SELECT ...)嵌套语句,无法定位原始 Mapper

原因:MyBatis 的分页插件(如 PageHelper)在Executor.query()阶段重写了 SQL,Druid 在statement_execute时拿到的是 PageHelper 包装后的 SQL,而非 Mapper 原始 SQL。
解决:PageHelper 5.3.0+ 支持reasonable=true模式,且需在 Druid Filter 中提前获取原始 SQL。修改PageInterceptor配置:

<plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="dialect" value="mysql"/> <property name="reasonable" value="true"/> <!-- 关键:让 PageHelper 不重写 SQL,由 Druid 统一处理 --> <property name="supportMethodsArguments" value="true"/> </plugin>

并在application.yml中关闭 PageHelper 的 SQL 重写:

pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true params: count=countSql

4.5 现象:Druid 控制台显示ConnectionPoolDestroy,但应用日志无异常,随后所有 DB 请求超时

原因:MySQL 服务端主动断开了空闲连接(wait_timeout=28800即 8 小时),而 Druid 的testWhileIdle=false且validationQuery未配置,导致连接池中残留大量“假连接”。
解决:必须开启空闲连接校验,并设置合理心跳 SQL:

spring: datasource: druid: # 开启空闲连接检测 test-while-idle: true # 检测 SQL(MySQL 用 SELECT 1,Oracle 用 SELECT 1 FROM DUAL) validation-query: SELECT 1 # 连接空闲 5 分钟后检测一次 time-between-eviction-runs-millis: 300000 # 连接空闲 30 分钟后才可能被驱逐(大于 MySQL wait_timeout) min-evictable-idle-time-millis: 1800000

5. 进阶技巧:用 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 name="SQL_DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/sql-debug.log</file> <filter class="ch.qos.logback.core.filter.LevelFilter"> <level>DEBUG</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <appender name="SQL_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/sql-warn.log</file> <filter class="ch.qos.logback.core.filter.LevelFilter"> <level>WARN</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder> <pattern>%d{HH:mm:ss.SSS} [SLOW-SQL] %msg%n</pattern> </encoder> </appender> <logger name="com.example.EnhancedLogFilter" level="DEBUG" additivity="false"> <appender-ref ref="SQL_DEBUG"/> <appender-ref ref="SQL_WARN"/> </logger>

这样,sql-debug.log记录所有 SQL 的完整参数(用于 debug),sql-warn.log只记录Time>200ms的慢 SQL 摘要(用于告警),日志体积降低 92%。

5.3 用 Prometheus + Grafana 监控 Druid 连接池健康度:三个黄金指标

Druid 内置/druid/stat.json接口,返回 JSON 格式的实时指标。我们用 Spring Boot Actuator 的Endpoint机制将其暴露为 Prometheus metrics:

@Component @Endpoint(id = "druid") public class DruidEndpoint { @ReadOperation public Map<String, 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: always

Prometheus 抓取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是否归零。这三步做完,心里才算真正踏实。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询