最近帮一个朋友排查线上连接超时问题,发现他们的Spring Boot项目虽然已经引入了Druid,但监控页面是空的,慢SQL一条都看不到,连接池里的活跃连接却在疯狂增长。后来定位下来,是用了druid-spring-boot-starter之后,配置写错了层级,参数全部落在默认值上。这个经历让我想写一篇完整的druid-spring-boot-starter使用笔记,把从依赖引入、参数落地到监控开启、密码加密和排错的完整链路梳理清楚,给准备在Spring Boot里用好Druid的读者一个能直接照着操作的参考。
Druid在国内Java项目里的知名度没什么可怀疑的,但很多人对它的用法还停留在手动new DruidDataSource、手动塞StatViewServlet的老路上。druid-spring-boot-starter出现之后,集成方式简化了一大截,但同时也带来了新的问题:自动配置帮你做的事情太多,有时候你根本不知道它到底做了什么、配置为什么没生效。本文就按这个思路展开,涉及Spring Boot 2.x和3.x两个大版本的差异也会讲清楚。
1. 换掉默认连接池的理由:Druid不止是连接池
1.1 连接池本身:HikariCP和Druid的真实差距
先大方承认一个事实:论单次取连接的性能,HikariCP在大多数场景下和Druid的差距微乎其微,Spring Boot把它设为默认连接池是有道理的。HikariCP的优点就是快、稳、零配置,但你拿它做生产环境时很快会遇到几个痛点:
- 数据库连接串、账号密码想加密,HikariCP本身不提供现成方案。
- 慢SQL没有统一的统计入口,要在代码里手动埋点。
- SQL注入拦截、连接泄漏检测这些能力,HikariCP一概没有。
Druid在这些方面是补齐了一整圈外围能力的:SQL监控、慢SQL统计、WallFilter防火墙、ConfigTools密码加密、连接泄漏检测、Web请求和SQL的关联统计。所以问题的关键不是"哪个连接池快",而是"你的项目在连接池之外还需要什么"。数据库连接池在JavaEE里发展了这么多年,最后大家拼的都是外围治理能力,Druid在这条路上走得最完整。
1.2 为什么直接用druid-spring-boot-starter而不是手动配置
很多老项目里看到过这种写法:写一个@Configuration类,手动new DruidDataSource(),再写一个ServletRegistrationBean把StatViewServlet注册进去,还要再声明一个FilterRegistrationBean去挂WebStatFilter。这套代码本身不难,但每个项目复制来复制去,配置分散、版本容易漂移,换个人维护就是灾难。
用druid-spring-boot-starter之后,这些组件全部交给Spring Boot的自动配置去装载:DataSource、监控Servlet、WebStatFilter、StatFilter、WallFilter,都是在classpath扫描时自动创建的。你要做的只是往application.yml里写配置。这就是starter的价值——不是连接池本身变了,而是集成方式从"自己组装零件"变成了"写参数让框架组装"。
提示:这套自动配置是Spring Boot的
AutoConfiguration机制,不是你引入依赖后随便写写就行的,理解后面第2节的自动配置类列表对你排查问题帮助很大。
2. 依赖引入与starter的自动装配机制
2.1 不同Spring Boot版本对应的依赖坐标
写依赖之前先确认你的Spring Boot大版本。这里有个非常关键的区分:Spring Boot 3.x使用的是jakarta.servlet命名空间,和Spring Boot 2.x的javax.servlet不兼容,所以Druid官方从1.2.18开始拆出了单独的druid-spring-boot-3-starter。
Spring Boot 2.x项目用这个坐标:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.23</version> </dependency>Spring Boot 3.x项目必须用:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-3-starter</artifactId> <version>1.2.23</version> </dependency>如果你在Spring Boot 3项目里误用了druid-spring-boot-starter,最常见的报错就是ClassNotFoundException: javax.servlet.Servlet,因为Spring Boot 3的容器里已经没有javax.servlet这条包路径了。
与此同时,建议在引入JDBC或JPA依赖时排除掉HikariCP。虽然druid-spring-boot-starter的自动配置在大多数情况下会先创建DruidDataSource,但classpath里同时存在两套连接池实现时,某些版本的Boot会自动回退创建HikariDataSource,日志里会出现两套连接池的初始化信息。排除方式:
<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一套连接池,自动配置的路就走得很干净了。
2.2 Starter启动时替你做了什么:主要自动配置类一览
druid-spring-boot-starter在Spring Boot 2.x中通过META-INF/spring.factories注册自动配置,在Spring Boot 3.x中则通过META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports注册。这里面有几个核心类,建议每个用Druid的开发者都眼熟它们:
| 自动配置类 | 作用 |
|---|---|
| DruidDataSourceAutoConfigure | 创建DruidDataSource,读取spring.datasource.druid前缀的配置 |
| DruidSpringAopConfiguration | 开启aop-patterns后对指定方法做调用监控 |
| DruidStatViewServletConfiguration | 注册Druid监控页面StatViewServlet |
| DruidWebStatFilterConfiguration | 注册Web请求统计过滤器WebStatFilter |
| DruidFilterConfiguration | 组装stat、wall、slf4j等JDBC过滤器链 |
这些类共同的工作方式就是Spring Boot经典的"条件装配":classpath里有对应类,配置项允许,才创建对应Bean。所以你的配置必须写到正确前缀下,自动装配才会把你写的参数绑定进DruidDataSource。
2.3 DataSource类型由谁决定:type字段与自动配置优先级
有一个常见的疑问:既然引入了Druid starter,为什么还要写spring.datasource.type?其实在DruidDataSourceAutoConfigure里,它会通过@ConditionalOnMissingBean(DataSource.class)之类的条件判断接管DataSource创建,同时spring.datasource.type可以显式指定最终创建的连接池类型。如果你的项目里已经手动声明了DataSourceBean,自动配置则不会重复创建。
所以我的建议是:要么完全信任starter的自动配置,要么完全自己声明DataSource,不要两套混着来。混着来的时候,你经常会在监控页面看到一个Druid实例、日志里却在打印另一个连接池的参数,最后排查半天发现是条件装配的优先级在作怪。
3. 生产级参数配置:这些数字怎么给才合理
3.1 连接池核心参数参考表
下面是一份可以直接抄到application.yml里的核心配置,我按MySQL场景写的:
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&serverTimezone=Asia/Shanghai username: root password: 你的密码 druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 filters: stat,wall,slf4j connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000每个参数的含义和给值思路,我整理成一张表:
| 参数 | 建议值 | 说明 |
|---|---|---|
| initial-size | 5 | 启动时初始创建的连接数,不应大于max-active |
| min-idle | 5 | 连接池最小空闲连接数,保证低峰期也有可用连接 |
| max-active | 20 | 最大活跃连接数,需要根据并发估算 |
| max-wait | 60000 | 取连接的最大等待时间,超时抛异常,单位毫秒 |
| time-between-eviction-runs-millis | 60000 | 空闲连接检测的执行间隔 |
| min-evictable-idle-time-millis | 300000 | 连接空闲超过5分钟后可能被回收 |
| validation-query | SELECT 1 | 连接可用性检测SQL,MySQL写SELECT 1即可 |
| test-while-idle | true | 空闲时检测连接,推荐开启 |
| test-on-borrow | false | 每次取连接都检测,开销较大,一般不推荐开 |
| test-on-return | false | 归还连接时检测,极其耗时,默认关掉就好 |
| pool-prepared-statements | true | 开启预编译语句缓存 |
| max-pool-prepared-statement-per-connection-size | 20 | 单个连接的预编译语句缓存上限 |
max-active怎么评估?一个简单估算方式:假设你的服务在峰值时有200个并发请求,每个请求占用数据库连接的平均时间是50毫秒,那么同一时刻大约会有200 x 0.05 = 10个连接在活跃状态,预留一些余量,max-active给到20是合理的。这个数字不要拍脑袋,最好压测后回填。
3.2 连接有效性检测:三个test开关的取舍
初学者最容易把testOnBorrow、testWhileIdle、testOnReturn三个开关全部开成true,觉得越是检测越安全。实际上testOnBorrow=true会在每次getConnection()时执行一次validationQuery,在连接池高频使用时会产生大量额外SQL查询。我见过一个项目开了testOnBorrow之后,QPS只涨了10%,数据库的SELECT 1就翻了一倍。
比较稳妥的组合是:testWhileIdle=true+timeBetweenEvictionRunsMillis=60000。这样每隔60秒Druid会扫描一遍空闲连接,对空闲时间超过minEvictableIdleTimeMillis的连接做一次检测,既保证了连接基本可用,又不会对每次取连接造成额外开销。
3.3 监控入口:stat-view-servlet与web-stat-filter的配合
Druid的监控能力要生效,需要同时配好StatViewServlet和WebStatFilter:
spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin123 reset-enable: false web-stat-filter: enabled: true url-pattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'这里要特别注意一个常见误解:stat-view-servlet负责提供/druid/index.html监控页面,web-stat-filter负责拦截Web请求进行URI维度的统计,两者是配合关系,不是替代关系。只配前者,你能看到连接池和SQL列表,但看不到Web请求维度的统计;只配后者,你连页面都访问不了。
提示:监控页面登录账号密码直接写在配置里虽然方便,但生产环境建议用环境变量覆盖,避免密码出现在代码仓库中。
reset-enable默认是false,如果想在页面手动重置统计,就显式改成true。
4. 慢SQL统计、SQL防火墙与日志审计
4.1 StatFilter:让每条SQL都留下痕迹
在filters: stat,wall,slf4j这一行里,stat对应的是StatFilter,它负责收集SQL执行次数、耗时、并发等数据。两个非常有用的附加参数是通过connection-properties传进去的:
druid.stat.mergeSql=true druid.stat.slowSqlMillis=5000mergeSql=true:把结构相同、参数不同的SQL合并成一条统计,比如SELECT * FROM user WHERE id = ?无论传入多少不同id,都归为一条。这样监控页面里不会被几百条同结构的SQL刷屏。slowSqlMillis=5000:执行时间超过5秒的SQL会被标记为慢SQL,配合filter.stat.log-slow-sql=true可以直接输出到日志。
注意,这两个属性走的是connection-properties,不是普通的spring.datasource.druid下配置。踩过坑的人都知道,写错位置之后慢SQL一直统计不出来。
4.2 WallFilter:把危险的SQL挡在数据库之前
WallFilter是Druid里很独特的一块能力,它本质上是一个SQL防火墙。开启方式就是在filters里加上wall,也可以精细化配置:
spring: datasource: druid: filter: wall: enabled: true config: multi-statement-allow: true comment-allow: falsemulti-statement-allow控制是否允许一次执行多条SQL,默认是false。很多项目因为用了allowMultiQueries=true的JDBC连接串,发现多条SQL执行被WallFilter拦截,因此在config里放开它。这里建议想清楚再开:允许multi-statement相当于把SQL拼接的大风险敞口打开了,如果业务确实需要,请务必配合参数化查询使用。
WallFilter对常见SQL注入手法的拦截很有意思,它不靠正则黑名单,而是直接解析SQL语法树。我做过一次测试,把经典的' or 1=1 --拼到条件里,MySQL裸库能查出数据,Druid的WallFilter直接拒绝执行并抛异常。这种语法树级别的防护,比应用层简单拼接检查要可靠得多。
4.3 日志输出:slf4j与logback的配合
filters: stat,wall,slf4j中的slf4j会把SQL执行日志交给SLF4J处理。启动后你会看到类似这样的日志:
INFO com.alibaba.druid.filter.logging.Slf4jLogFilter - {conn-10001, stmt-20001} executed. 5.0 milliseconds. SELECT * FROM user WHERE id = 1如果觉得SQL日志太吵,可以单独控制该Logger的级别:
logging: level: com.alibaba.druid.filter.logging.Slf4jLogFilter: WARN需要保留审计类日志的场景(比如金融项目),可以把这一项调到DEBUG或INFO,它记录的是真实的SQL执行明细,对审计和排障很有价值。
5. 配置了不生效?一套排查链路帮你定位
这一部分我把自己实际遇到过的坑总结成了排查链路,按顺序做基本能定位问题。
5.1 从启动日志和JVM连接数反向判断
Druid启动时会在日志里留下一行关键信息:
com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited看到这行日志,说明DruidDataSource已经被Spring容器创建并初始化完成了。但请注意,inited不代表一切正常,它只代表连接池对象建立完毕,后面真正建立物理连接时如果有异常,会继续打印连接失败的堆栈。
如果你想进一步确认连接池中的连接数是否按配置增长,可以借助JVisualVM或Arthas查看DruidDataSource的activeCount和poolingCount字段。这两个字段分别表示"正在使用的连接数"和"连接池内空闲连接数",拿它们和max-active对照,就能判断参数是否真正生效。
5.2 前缀和排除项的常见错误
我在朋友项目里发现的第一类问题就是配置写错前缀。druid-spring-boot-starter读取的是spring.datasource.druid前缀,而不是spring.datasource.hikari,也不是druid。下面这几种写法都是错的:
# 错误1:漏掉spring.datasource前缀 druid: max-active: 20 # 错误2:沿用默认连接池前缀 spring: datasource: hikari: maximum-pool-size: 20当你把HikariCP的maximum-pool-size还留在配置里时,虽然排查时看到spring.datasource下是有配置的,但Druid一条也不会读。检查要点永远是:max-active、initial-size这些字段必须挂在spring.datasource.druid下面。
另一个高发原因是classpath里同时存在HikariCP和Druid。此时Spring Boot默认的DataSourceAutoConfiguration可能优先创建了HikariDataSource,你用jconsole看到的连接池不是你想的那一个。处理方式就是第2节说的,把HikariCP从spring-boot-starter-jdbc里排除掉。
5.3 监控页面缺数据的排查清单
监控页面开了,但SQL列表没有数据,这个问题排行榜上见得太多了。我整理了一张排查清单:
| 现象 | 排查方向 |
|---|---|
| 页面404 | stat-view-servlet.enabled没开或url-pattern写错 |
| 能进页面但无数据源信息 | mybatis或jpa可能绕过了starter创建的DataSource |
| SQL列表为空 | web-stat-filter.enabled未设置,或exclusions误杀导致请求没被过滤 |
| 连接池参数不对 | 检查配置前缀,用Arthas查看DruidDataSource实际字段值 |
报statement is not allowed | WallFilter拦截,需检查multi-statement-allow等配置 |
5.4 一次真实的生产事故复盘
那次朋友的生产事故,表面症状是某个服务实例的连接数持续打满,应用到数据库的连接按小时增长直到触顶。我上去先是看了启动日志,确认确实走的是Druid,但仔细看配置后发现:spring.datasource.type没写,HikariCP也没排除,Spring Boot自动配置最终创建的是HikariDataSource,服务里真正跑着的是Hikari,而Druid只是被加载但没被使用。把type显式指向Druid并排除Hikari之后,重启实例,连接数回落正常。
这个案例的教训是:自动配置越方便,越要确认"最终生效的组件到底是什么"。如果没有监控页面或Arthas做验证,你可能永远不会发现项目里真正干活的是另一个连接池。
6. 数据库密码加密:ConfigTools使用与常见坑
6.1 生成密钥对与密文
Druid的ConfigTools提供了一套非对称加密方案,专门解决数据库密码在配置文件里明文存放的问题。使用方式很简单,在命令行执行:
java -cp druid-1.2.23.jar com.alibaba.druid.filter.config.ConfigTools 你的数据库密码执行后输出三样东西:
privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASC... publicKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... password: m4gZ6Gm6hYd5QyN7yS9F4zW3lRJkzDmQ...私钥是给ConfigTools自己解密用的,公钥是配置到应用里的。实际部署时私钥不要出现在项目里,保留在运维手里用于定期更换密码。
6.2 yaml配置与filter.config的正确写法
拿到公钥和密文后,按下面方式改配置:
spring: datasource: druid: username: root password: m4gZ6Gm6hYd5QyN7yS9F4zW3lRJkzDmQ connection-properties: config.decrypt=true;config.decrypt.key=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... filter: config: enabled: true这里有两种开启config解密的方式,一种是直接把config写进filters:
filters: stat,wall,slf4j,config另一种就是用上面的filter.config.enabled=true。我个人推荐第二种,可读性更好,而且不会因为filters里写错顺序导致filter链初始化失败。两种方式不要混用,混用偶尔会触发重复解密或属性覆盖的问题。
6.3 解密失败的三个典型原因
踩过ConfigTools的坑,典型原因集中在下面三个:
第一,公钥填错成私钥。复制过程中把privateKey当成publicKey填进config.decrypt.key,启动时直接报RSAException: decrypt error。文本很像,务必核对标签。
第二,filter.config.enabled没开或没有把config加进filters。这种情况下应用启动不会报错,但密码是加密串,最终连接数据库时认证失败。很多人在SQL日志里找半天,最后发现是解密逻辑压根没被激活。
第三,密文里含有特殊字符在YAML中被转义。比如+、/偶尔会被某些YAML解析器转成其他含义,一个稳妥做法是用单引号把整段密文包起来:
password: 'm4gZ6Gm6hYd5QyN7yS9F4zW3lRJkzDmQ'改完之后,重启应用,看到DruidDataSource正常创建连接,再配合监控页面里的数据源状态,基本可以确认解密流程跑通了。
7. 上线后的监控页利用与两个容易被忽视的坑
7.1 监控页面怎么用好:从SQL列表到Session跟踪
Druid监控页面的地址通常是http://localhost:8080/druid/index.html(端口和上下文根按你的项目调整)。登录后第一眼看到的是数据源Tab,那里有activeCount、poolingCount、maxActive这些实时指标。生产环境遇到连接数异常波动时,我基本都先盯着这个页面,通常几分钟就能看出是某个接口把连接借走没还,还是整体并发真的上来了。
SQL Tab更实用。开了mergeSql=true之后,SQL列表会把同结构SQL聚合展示,每一条都有执行次数、总耗时、平均耗时、最大耗时。慢SQL直接按Max列降序排序,一下就能找出拖垮数据库的罪魁祸首。再配合druid.stat.slowSqlMillis=5000,超过5秒的SQL会被单独圈出来,这个功能在性能优化阶段帮助极大。
7.2 连接池泄漏检测:removeAbandoned的正确打开方式
HikariCP没有的经典功能之一就是连接泄漏检测。Druid里相关参数有三个:
spring: datasource: druid: remove-abandoned: true remove-abandoned-timeout: 1800 log-abandoned: true意思是:连接被借出后如果1800秒还没归还,连接池会强制回收,并打印一条日志记录该连接当时的调用栈。这个功能很有用,但我的实际经验是:不要在刚发现问题时就把remove-abandoned直接开成true。因为强制回收的本质是"宁可牺牲一个正在执行的长事务,也不能让连接池被耗尽",如果业务里有合法的长事务,会被这个参数误杀。
推荐的顺序是:先开log-abandoned=true观察一段时间,通过日志确认泄漏的代码位置,修掉根本原因。如果短期无法根治且线上连接即将耗尽,再临时开remove-abandoned兜底。日志里如果出现get/close call inconsistent,那几乎可以断定是连接泄漏点被标记出来了,顺着日志里的堆栈找对应方法即可。
7.3 Spring Boot 3项目的新坑
Spring Boot 3除了要换druid-spring-boot-3-starter,还有几个细节值得注意。首先是包名问题,旧版本的Druid在Web环境里对javax.servlet的直接依赖,在Spring Boot 3下会变成jakarta.servlet,所以如果你在代码里直接引用了DruidFilterConfiguration等类,需要升级到对应支持Jakarta的版本。
其次是Spring Boot 3内置Tomcat的默认行为变化较大,监控页面如果访问不到,先别急着怀疑Druid,用curl http://localhost:8080/druid/index.html看看HTTP状态,再确认stat-view-servlet是否被某些权限配置拦截。
最后提一个容易被忽略的点:Spring Boot 3的自动配置加载方式改成了AutoConfiguration.imports,如果你在旧教程里看到修改spring.factories的操作步骤,那套方案已经过时了。排查自动配置是否生效时,直接看META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里有没有DruidDataSourceAutoConfigure即可。
我在实际运维中还有一个习惯,就是定期去Druid的监控页面看一眼SQL执行Top列表。很多时候你以为数据库层没问题的系统,慢SQL只是被应用层日志淹没了,而连接池层面的统计才能真正客观地反映数据库的负载情况。第一次能清楚看到每条SQL的耗时分布时,你会觉得之前几年排查数据库性能的方式都太原始了。
如果你正准备把项目里的连接池换成Druid,我建议按照本文的顺序一步步来:先引入starter并确认启动日志里的inited标记,再配好stat-view-servlet和web-stat-filter把监控页面跑起来,之后再去折腾max-active、min-idle这些参数。监控页面有了,后面每一次参数调整你都能看到实际效果,而不是靠猜。等你把密码加密、WallFilter、慢SQL日志这些都配上之后,数据库这一层的可观测性就基本到位了。