写这篇文章之前,先交代一个背景:我做后端这几年,Spring Boot项目凡是需要接数据库,基本都绕不开连接池这一步。默认推荐的是HikariCP,轻量、性能高,没毛病。但一旦你的项目需要看慢SQL、需要防SQL注入、需要把线上数据库连接情况拎出来做分析,HikariCP那套就显得太“素”了。这时候我第一反应就是Druid,再配合druid-spring-boot-starter,整个接入过程比我想象中顺很多。
这篇文章不聊虚的,直接基于我用druid-spring-boot-starter的真实经历,从依赖引入、yml配置、监控页面、密码加密到生产环境的常见坑,一条线讲完。同时也会照顾到Spring Boot版本差异,毕竟最近不少朋友还在用Boot 2.x,但已经有人开始踩Boot 3.x的坑了。适合刚接触Spring Boot对连接池一脸懵的新手,也适合已经用上Druid但没把监控和防注入用起来的老手。读完你不会只会配个数据源,而是能把这套东西真正用到生产环境里。
1. 连接池选型:为什么最终落到Druid上
1.1 HikariCP、DBCP、Druid三者的定位差异
先坦诚说一句,Spring Boot 2.x以后官方默认数据源是HikariCP,很多人觉得没必要换。HikariCP优点很明显——快,字节码级别优化,连接池大小默认10,绝大多数CRUD场景完全够用。但它只解决连接复用问题,不给你监控面板,不给你SQL审计,也不管SQL注入。DBCP在Spring Boot时代几乎没人用了,配置繁琐,性能还不如HikariCP,除非是老项目维护,否则不建议再碰。
Druid的差异化能力在于“连接池之外”的部分:内置监控统计、慢SQL记录、Web关联监控、SQL防火墙,而且这些能力是原生自带,不需要额外引入中间件。对一个要长期维护的系统来说,能直接在页面上看SQL执行频率、识别慢查询、下线高危SQL,比单纯追求那一点连接池性能更有价值。所以我的选择逻辑很简单:性能差距在现代硬件上几乎感知不到,但Druid多出来的运维能力是实打实能省事的。
1.2 druid-spring-boot-starter帮你完成了哪些工作
如果你用过早期Druid,应该记得需要手动创建DruidDataSource、再new一个FilterRegistrationBean注册StatViewServlet、再new一个WebStatFilter注册WebStatFilter。这套样板代码每个项目复制一遍,稍微漏一步监控功能就废了。druid-spring-boot-starter的出现就是把这些自动装配掉。
starter里最核心的自动配置类是DruidDataSourceAutoConfigure,它会在你引入依赖之后,将DataSource的创建、初始化、监控Filter装配、Servlet注册全部接管。你在application.yml里写druid前缀的配置,就可以控制连接池参数、监控规则、拦截粒度,再也不用写一行Java配置类。这里注意一个前提,spring.datasource.type要指到DruidDataSource,或者完全不写type,让starter的自动配置自己判断。如果你手动创建了一个DataSource的Bean,反而会干扰自动配置的判断,后面我会在踩坑部分细说。
2. 先在项目里把依赖弄对,别在第一步翻车
2.1 Maven与Gradle的坐标写法
Maven项目直接在pom.xml里加,版本记得用你本地环境中能和Spring Boot兼容的版本。以我常用的1.2.20为例:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency>Gradle项目写法更简洁:
implementation 'com.alibaba:druid-spring-boot-starter:1.2.20'这里提醒一下,druid-spring-boot-starter内部依赖了druid的核心包,所以不需要再单独引入druid,重复引入反而可能出现类冲突。如果你之前项目里手动引过druid,记得去重。
2.2 Spring Boot版本“太高”导致启动失败的坑
这条我要特意拿出来说,因为见过太多人问“为什么我的项目一启动就报DataSource相关错误”,结果一看Spring Boot 3.x。Spring Boot 3.x最大的变化是javax.servlet变成jakarta.servlet,而Druid的老版本starter和1.2.x早期的核心包还在用javax命名空间,直接导致类找不到或者服务无法注册。
解决办法就是在1.2.20以上版本中选,我实测下来1.2.20在Spring Boot 3.x上是稳定的,当然Druid后续版本迭代也在持续适配。启动报错如果出现NoClassDefFoundError(类定义找不到),先别怀疑配置,先查看一下项目里有没有tomcat-embed-jakarta相关依赖,或者把Druid版本升到1.2.20以上再启动一遍。用Boot 2.x的朋友建议至少1.2.6以上,因为早期版本存在一些连接池初始化时序问题。
3. application.yml配置详解:一份可以直接抄的配置
3.1 数据源连接与连接池参数逐一说明
先给一份我线上项目在用的核心配置,去掉业务字段后长这样:
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/my_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: my_password 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每个参数背后的逻辑我按实战理解展开说。initial-size是启动时提前建立多少个连接,设为5可以减少第一波请求打过来时临时建连的延迟;min-idle是最小空闲数,系统空闲时也保持至少5个连接,避免流量突然进来时建连压力过大;max-active是最活跃连接数,20对于常规业务绰绰有余,但要注意它不代表数据库能承受的并发上限,如果后面接口出现连接等待,优先看的是这个值是不是设小了。
max-wait是获取连接的超时时间,单位毫秒,60000就是60秒,超过则抛出获取连接超时异常。这个值不要设成-1,否则在高并发下线程会无限等下去,服务线程池直接被拖垮。time-between-eviction-runs-millis是空闲连接检测的周期,设为60000意味着每分钟扫描一次;min-evictable-idle-time-millis表示连接空闲多久之后可以被回收,我这里的300000是5分钟。这两个参数配合起来,目的是让数据库连接的回收不紧不慢,对数据库侧的连接数控制也友好。
test-while-idle设为true时,连接池会在空闲检测时使用validation-query验证连接是否有效,避免把已断开的连接交给应用;test-on-borrow则是每次从池里拿连接都验证,那太浪费性能,通常关掉;test-on-return同理,归还时不验证。这里有一个铁律:如果你的数据库是MySQL,validation-query用SELECT 1就可以;如果是Oracle,最好改成SELECT 1 FROM DUAL;如果数据库不支持这种写法,会在空闲检测时报错,这是排查连接问题的第一步。
还有一个容易被忽略的配置项是connection-properties,比如MySQL驱动在连接串里带了allowPublicKeyRetrieval=true参数时,你可以写进去。但这属于比较偏门的用法,不建议新手一上来就搞这些,先保持上述配置跑通再说。
3.2 监控页面的两大核心组件:StatViewServlet与WebStatFilter
Druid的监控页面不是默认打开的,需要配置StatViewServlet(提供/actuator/druid或/druid路径下的监控HTML页面)和WebStatFilter(拦截Web请求做统计)。配置文件里这样写:
spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin123 reset-enable: false allow: 127.0.0.1 deny: web-stat-filter: enabled: true url-pattern: /* exclusions: /druid/*,*.js,*.css,*.gif,*.jpg,*.png,*.ico session-stat-enable: true profile-enable: true先解释几个让我踩过坑的字段。url-pattern的写法决定你访问监控页的路径,比如写成/druid/*,那访问地址就是http://你的地址:端口/项目上下文/druid/index.html。这里一定注意context-path前缀,如果你的项目配了server.servlet.context-path: /myapp,那完整的监控地址就是/myapp/druid/index.html,很多人打不开页面就是忘了加前缀。
login-username和login-password是访问监控页的基础认证,设置之后进页面会先弹登录框。reset-enable设为false,是为了防止页面上的“重置”按钮把统计数据清零。生产环境肯定要禁用这个按钮,否则监控数据被人一键清空,排查问题就失去依据。
allow和deny是访问IP白名单和黑名单,allow配置的是允许访问的IP,多个地址用逗号分隔。默认情况下不配置allow时允许所有IP访问,我建议生产环境一定要配,或者至少保证登录口令不是弱口令。deny优先级高于allow,在同一个IP同时出现在两个集合里时,deny会生效。
web-stat-filter是拦截Web请求并统计访问量的,url-pattern配/*代表拦截所有请求;exclusions里写的是不参与统计的路径,比如静态资源和监控页自身,不然请求监控页面的行为也会被监听,既浪费性能又把统计数据搞脏。session-stat-enable开启后会统计Session维度的监控,profile-enable开启后可以记录URL关联的SQL执行情况,这俩对性能分析很有用。
3.3 用于随机端口场景下的监控访问问题
有一个热搜词是“springboot yml 随机端口”,如果你在配置里写成server.port: ${random.int[8000,9000]},那每次启动服务端口都在变。用Druid监控的人会遇到一个麻烦:书签里的监控地址端口对不上现在的地址。这不是Druid的问题,是随机端口本身带来的。真想用随机端口又要看监控,建议把端口日志打印清楚,或者把监控页面地址在启动日志里输出。如果你更看重监控的稳定性,那就把端口固定下来,随机端口更适合临时环境。
4. 数据库密码加密:别把账号明文放在配置里
4.1 用ConfigTools生成密钥对和密文
很多项目直接把数据库密码写成明文放在application.yml里,这在开发环境没问题,但一旦配置被推到Git仓库,等于把数据库钥匙交出去了。Druid提供了ConfigTools工具类,可以对密码进行RSA加密,然后把公钥和密文同时配置到yml里,数据库连接时再解密。生成方式有两种。
一种是在代码里临时执行:
public class DruidPasswordGenerator { public static void main(String[] args) throws Exception { String password = "你的真实数据库密码"; String[] keyPair = ConfigTools.genKeyPair(512); System.out.println("privateKey:" + keyPair[0]); System.out.println("publicKey:" + keyPair[1]); System.out.println("password:" + ConfigTools.encrypt(keyPair[0], password)); } }另一种是直接用命令行工具,把druid核心jar包下载下来以后执行java -cp druid-1.2.20.jar com.alibaba.druid.filter.config.ConfigTools你的密码,它会直接输出privateKey、publicKey和password三段内容。注意输出中的privateKey只用于本地生成密文,生成完就可以丢了,不要放进代码仓库;配置文件只需要publicKey和加密后的password。
配置里这样写:
spring: datasource: druid: username: root password: xxxxxxxxxxxxx密文xxxxxxxxxx public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJ等等等 connection-properties: config.decrypt=true;config.decrypt.key=${spring.datasource.druid.public-key}4.2 配置加密后的常见错误
最常见的问题是启动时报DataSource出错,像“Public Key not found”之类。这不是加密本身出了问题,而是你漏了connection-properties里的config.decrypt=true,或者公钥没有正确传入。检查两个地方:公钥有没有拷贝完整、有没有多余换行;connection-properties里的key名是否正确引用了public-key。另一个坑是如果数据库密码本身很短,RSA加密后密文很长,有些人在复制时只复制了上半段,导致密文丢失尾部字符,启动解密失败。只要报错信息里出现Cipher相关异常,第一反应就是去重新生成一组密钥再试一遍。
5. 安全加固:Wall Filter与SQL注入防御的实战配置
5.1 WallFilter究竟拦截了什么
Druid自带一个WallFilter,是基于语义分析的SQL防火墙,它不是简单地拿关键字匹配,而是把SQL语句解析成语法树,然后根据配置的规则判断是否合法。这意味着它能拦截很多常见的注入手法,比如在WHERE条件后面拼接OR 1=1、利用注释符把后面的SQL注释掉、使用UNION SELECT窃取其他表数据等。
启用WallFilter的方式很简单,在yml里配置:
spring: datasource: druid: filter: wall: enabled: true config: multiStatementAllow: false noneBaseStatementAllow: false deleteAllow: true updateAllow: true insertAllow: true selectAllow: true关键在于几个规则项能不能理解透。multiStatementAllow默认false,表示不允许一次执行多条SQL语句,像“SELECT * FROM user; DROP TABLE user”这种直接会被拒掉;noneBaseStatementAllow默认false,表示不允许执行非基础SQL语句以外的操作,比如执行存储过程等相关操作;deleteAllow、updateAllow、insertAllow、selectAllow分别控制四类基础SQL是否能执行。很多团队为了避免误伤,会把delete和update放开,只关掉多语句和特殊调用,这是折中方案,但我个人建议如果系统没有批量更新需求,updateAllow也可以谨慎关掉,放到白名单里单独放行。
还有几个高阶配置项值得补充,比如conditionAndAllow和conditionOrAllow,控制WHERE条件里是否允许AND和OR,如果把OR关掉,那“OR 1=1”这种注入方式直接在语法层面就被拦了。再比如minValue和maxValue能限制查询返回的数据范围,对防止拖库也有帮助。不过这些配置在少数业务场景下会误伤,比如合法的多条件OR查询被拦截,需要结合实际调。
5.2 配合Spring Boot全局过滤器处理异常输入
再说一个网上比较多的问题,就是Spring Boot全局过滤器处理上传PDF时怎么避免XSS攻击。这个问题看起来跟Druid无关,但它和WallFilter的定位一致,都是“在入口处拦截非法输入”,可以放在一起说。XSS攻击的核心是把可执行脚本塞进HTML输出流里,Druid的WallFilter不处理这个,你需要在全局过滤器里对请求参数做HTML转义,比如把<和>转成实体字符。但千万别对上传的PDF二进制流做这种处理,否则文件内容会被破坏。区分方式很简单:文本参数(表单字段、JSON字段)可以走转义过滤,文件二进制流直接放行。
Druid在这个链条里的价值是守住SQL层,全局过滤器守住的是Web入口层,两者配合才能形成有效的纵深防御,这也是我做项目时比较强调的一层。
6. 生产环境的监控配置与日志实践
6.1 慢SQL统计的两种落地方式
Druid的监控页面上能直接看到慢SQL,但生产环境出问题不可能每时每刻盯着页面。推荐搭配慢SQL日志。Druid里有一个log4j或者slf4j记录的慢SQL日志,开启方式是在配置里加上:
spring: datasource: druid: filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000slow-sql-millis设为1000,表示执行时间超过1秒的SQL会被记录到日志里。注意这里的时间单位是毫秒,别写成1,否则所有SQL都会被视为慢SQL,日志直接爆炸。配好之后,你会发现日志里多出类似“slow sql: SELECT ...”这样的记录,再配合分析工具定位问题就方便多了。
另一种方式是把Druid的监控数据接入外部监控系统,比如用Actuator的端点把Druid指标暴露给Prometheus,或者定时把StatFilter的统计数据打印到日志。这块要看团队基础设施,不强求。
6.2 监控页面访问控制的正确姿势
前面在StatViewServlet配置里提过allow和deny字段,这里再补充一个容易被忽略的点:生产环境一定要用内网IP访问监控页,不要通过公网把/druid路径暴露出去。如果你所在网络环境不允许内网直接访问,可以用跳板机,或者把监控页绑定到本机地址后通过SSH隧道访问。这里不展开讲具体隧道方案,核心原则是监控页不能裸奔。
登录密码也很重要,Druid监控页的登录会话默认是servlet session,一段时间不操作就会失效,这反而降低了口令被长期挂着的风险。reset-enable一定要是false,否则任何打开页面的人都能一键清空统计值,你排查历史问题时数据全部归零。
6.3 应用重启后监控数据丢失的问题
Druid的监控统计数据默认都存在内存里,应用一重启数据就没了。你如果要做月度SQL性能趋势分析,光靠控制台页面是不够的。要么定期手动从页面导出JSON或CSV,要么自己写个定时任务把StatFilter的数据存储下来。我没用太重的方案,就是在项目里加了一个定时任务,每隔一段时间读取Druid StatManager的统计快照,把关键指标写入业务库。采样频率不用太高,5分钟一次足以,不然数据量很大,查询分析也变慢。
有一点要提醒,你读到快照的时候,有些类的字段是内部实现的,版本升级后字段名可能会变化,所以采样代码里最好不要依赖具体实现类,而是通过Druid统计API获取,比如通过DruidDataSource的getStatValue方法取出连接数、活跃数等核心指标。
7. 踩坑汇总:版本、配置、多数据源等高频问题
7.1 Spring Boot版本太高导致Druid失效
前面提过一次,这里做成完整排查思路。如果你确认自己引入了druid-spring-boot-starter,但启动后日志显示数据源是HikariDataSource,那大概率是自动配置没生效。一个典型原因就是Spring Boot 3.x配合了旧版Druid,starter的自动配置条件没有满足。先去maven仓库看druid-spring-boot-starter的版本,Boot 3.x选1.2.20以上;其次看项目里有没有显式声明了DataSource的Bean,如果有,先去掉;再看是不是把spring.datasource.type写成了别的类型。按这三步排查,基本能解决。
7.2 数据库连接URL带?参数时的解析问题
“springboot 数据库驱动 ?参数”是热搜里的一条,这类问题通常在配置了带参数的数据库URL时遇到,比如jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC。Druid在解析连接串时不会丢失这些参数,问题往往出在你自己写URL时漏了某个驱动需要的参数,于是数据源初始化时直接报错。比如MySQL 8.x的驱动就要求必须指定serverTimezone,否则会报SQLException。建议先单独用JDBC原生驱动测试这条连接串能否连通,能连通再放到Druid配置里,这样能快速区分是不是Druid本身的问题。另外连接池里有些参数比如connectTimeout和socketTimeout,也可以追加到URL上,但注意值单位是毫秒。
7.3 多数据源项目启动报错:DataSource自动配置冲突
如果你项目里配置了两个数据源,比如一个业务库一个报表库,Druid自动配置默认只会处理main下唯一的数据源,另一个库要自己定义DruidDataSource的Bean。这个过程经常出现“DruidDataSourceAutoConfigure”尝试接管两个数据源导致冲突的情况。解决方法是在自定义数据源的配置类上排除自动配置,做法是在启动类上取消druid数据源的自动配置:
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)或者如果你不需要starter的自动装配,可以完全不用druid-spring-boot-starter,手动创建一个DruidDataSource的Bean,再手动配置StatViewServlet和WebStatFilter。两种方式我都用过,单数据源无脑用starter;多数据源建议第二个及之后的数据源手动创建,第一个主数据源继续用starter,减少冲突面。
7.4 Linux服务器上监控页面打不开
开发环境Windows下访问监控页一切正常,部署到Linux服务器以后页面白屏或者404,这种问题多半不是Druid配置问题,而是项目部署方式不同导致Servlet路径没对上。如果是Docker部署,要确认你映射端口的同时有没有把容器的context-path考虑进去;如果是Spring Boot的fat jar直接java -jar启动,再检查url-pattern是不是/druid/*,并确认访问地址里有没有带项目上下文。最简单的定位方法:项目启动日志里会打印所有已注册的Servlet映射路径,搜索druid关键字,看到注册路径后照着访问肯定能通。
7.5 log4j依赖冲突导致启动异常
最后说一个比较隐蔽的坑,druid-spring-boot-starter在某些版本下会依赖log4j相关组件,如果你的项目还同时引入了logback或者log4j2的另一个版本,启动时可能抛出多个SLF4J绑定错误,或者出现logger创建异常。解决方案是检查依赖树,把druid传递进来的不需要的日志依赖排除掉,保留项目统一的日志实现。具体命令是mvn dependency:tree配合grep,或者在IDEA的Terminal里直接执行,找到druid相关的传递依赖后,在pom里用exclusions排除冲突项。这个问题不一定会百分百触发,但遇到了要能识别出来,别总在配置上反复折腾。
8. 最后分享一点使用进阶:Druid在国产化数据库场景里的适配
热搜里有人提到“金仓读写分离配置”,顺带说一句我实际遇到的类似场景。国产数据库(如金仓、人大金仓KingbaseES、达梦等)本质上仍兼容PostgreSQL或Oracle的JDBC协议,Druid的适配重点在driver-class-name和validation-query。你只需要把driver换成对应的驱动类,比如KingbaseES的驱动类名通常是com.kingbase8.Driver或者cn.com.omc.gbase.Driver,再把validation-query改成SELECT 1 FROM DUAL,其他连接池参数可以保持不变。
读写分离在Druid里可以通过proxy-filter或者内置的多数据源方案实现,不过说实话,Druid的处理能力有限,真正的读写分离建议交给数据库中间层。如果只是想做到读走备库、写走主库,在应用层配置多个数据源并配合@Transactional和@ReadOnly注解,比强行在Druid里搞更清晰。
另一个小技巧是Druid的spring-boot-starter会自动注册actuator端点,Spring Boot 2.x环境下你可以通过/actuator/druid查看一些指标数据。但要注意暴露端点这件事本身也有安全风险,生产环境还是要把actuator的访问权限控制好,跟监控页一个待遇。我在实际项目中用的是内网IP白名单加Basic Authentication双重限制,效果还不错。
说回整体使用感受,druid-spring-boot-starter不是那种“装上就完事”的依赖,它的价值需要靠配置和运维习惯一点点体现。密码加密、SQL防火墙、慢SQL日志、监控页面权限这几块如果全部落地,数据库出问题时你的排查速度会快很多。至少我在一次线上慢查询问题中,就是靠Druid监控页里的慢SQL列表定位到一条没有索引的模糊查询,十几分钟就锁定了问题SQL;换成HikariCP,估计得翻半天业务日志加审计记录,效率完全不同。这也是我为什么在Spring Boot项目里坚持用它的原因——开发时多花十分钟配置,运维时会帮你省十个小时。