ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot 3 集成 Druid 踩坑指南:从 javax 到 jakarta 的迁移实战

Spring Boot 3 集成 Druid 踩坑指南:从 javax 到 jakarta 的迁移实战 把 Spring Boot 3 升级提上日程之后我遇到的第一个硬茬不是业务代码而是数据源。Spring Boot 3 基于 Spring Framework 6底层的 Servlet 规范整体从 javax 换到了 jakarta这一换导致大量老版本的 starter 直接失效。今天这篇踩坑记录就围绕 springboot3 集成 Druid 这个事把我实际踩过的坑、查过的日志、试过的方案全捋一遍从依赖选型和 javax/jakarta 冲突到连接池被 Hikari 顶替、监控台 404、filter 不生效、慢 SQL 没输出再到多数据源场景应该怎么处理。如果你是正在迁移 Boot3、或者刚在 IDEA 里建了一个 Spring Boot 3 项目准备接 Druid 连接池的 Java 开发者这篇应该能帮你少走不少弯路。1. 准备阶段先避雷依赖选错直接起不来1.1 先分清两个“Druid”——连接池还是 OLAP 数据库搜资料之前先说明一个非常容易混淆的点。你搜“Druid 性能对比”搜出来的结果大概率是 Apache Druid那个负责 OLAP 分析、搞 Segment 和 Rollup 的列式数据库。而 Java 开发常说的 Druid是阿里巴巴开源的数据库连接池com.alibaba:druid两者除了名字一样没有任何关系。这个乌龙我见过不止一次有人按“Apache Druid”的教程配了半天回头发现项目里根本没有这个依赖也有运维同事问“你们接 Druid 是不是想把报表查询都扔进去”。所以开篇第一件事确认你要的是com.alibaba:druid坐标别把依赖坐标写错。1.2 Boot3 项目必须引入 druid-spring-boot-3-starterSpring Boot 2 时代大家习惯了直接引com.alibaba:druid-spring-boot-starter这个 starter 在 Boot 2 下一切正常。但放到 Spring Boot 3 里如果还引这个旧 starter项目会在启动过程中直接报ClassNotFoundException: javax.servlet.Filter页面都进不去。原因后面第 2 节详细说这里先给结论Boot3 项目请使用官方提供的专用 starter坐标如下dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-3-starter/artifactId version1.2.20/version /dependency从 1.2.16 开始官方就有这个 Boot3 专用 starter 了我这边用得最多的是 1.2.20后面如果有新版本建议直接往上升。注意这个 starter 本身对 Spring Boot 3.0、3.1、3.2、3.3 基本都是兼容的因为 Boot3 的自动装配机制没有大变变的只是包名和低版本 JDK 支持。另外如果你是 Gradle 项目坐标一样写成implementation com.alibaba:druid-spring-boot-3-starter:1.2.20就行。1.3 用 mvn dependency:tree 确认没有两套 Servlet API 打架依赖选对了只是第一步。实际项目里很容易因为某个传递依赖把老 jar 带进来所以我每新建或升级一个 Boot3 项目都会先跑一遍依赖树mvn dependency:tree -Dincludesjavax.servlet:javax.servlet-api mvn dependency:tree -Dincludesjakarta.servlet:jakarta.servlet-api正常情况Boot3 项目里只应该出现jakarta.servlet相关的 jarjavax.servlet一行都不该有。如果出现了八成是某个老 starter 传递进来的把它排除掉否则后面会遇到各种“启动一半就挂”的怪问题。我遇到过最离谱的一次是旧版本的 MyBatis 分页插件把javax.servlet-api带了进来结果 Tomcat 启动时两个 Servlet 容器类互相干扰报的错根本看不出是依赖问题。2. 启动期最刺眼的报错javax.servlet 全家桶集体失踪2.1 报错日志长什么样用错依赖的时候Spring Boot 3 启动会在 Web 容器初始化阶段直接抛异常典型日志是这些java.lang.ClassNotFoundException: javax.servlet.Filter java.lang.NoClassDefFoundError: javax/servlet/ServletContextListener有时候还会故意伪装成别的错比如Failed to instantiate [org.springframework.boot.web.servlet.ServletContextInitializer]但往 Cause 里翻一定能看到 javax 的字样。我第一次看到这个报错时第一反应是“这个类我没用过啊”然后花了不少时间去看自己写的 Filter完全走偏了。2.2 根因Spring Boot 3 全面迁到 jakarta 命名空间这个坑的根因要从 Servlet 规范的变迁说起。一直以来的 Java Web 开发Servlet 相关的类都在javax.servlet包里大家写 Filter、Listener 都这么写习惯了几十年。但从 Jakarta EE 9 开始整个命名空间从javax.*迁移到了jakarta.*Spring Boot 3 底层用的是 Jakarta Servlet 5.0所以运行时容器里只有jakarta.servlet没有javax.servlet。老版本的druid-spring-boot-starter是照着 Boot2 生命周期编译的它的自动配置类里静态引用了javax.servlet下的类。类加载的时候找不到这些类直接抛上面那串异常。这不是版本号冲突那种简单“打架”而是整个坐标体系换了。所以千万不要想着手动塞一个javax.servlet-api进依赖来“补全”那只会让程序里同时存在两套 Servlet APIJVM 加载哪个全看运气行为更不可控。正确做法就一条所有第三方组件都用支持 Jakarta 的新版本。2.3 和 knife4j 是同款问题解决方案一并说这里顺带提一个 Boot3 迁移时几乎人人都会撞的组件knife4j。它的报错机制和 Druid 一模一样也是因为老版本基于 javax 和 SpringFoxBoot3 下根本起不来。解决方案也同样简单——用 4.x 的 Jakarta 版本dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi3-jakarta-spring-boot-starter/artifactId version4.5.0/version /dependency很多人在同一个项目里同时报 Druid 和 knife4j 的错其实根因都是同一个javax → jakarta 迁移。把这条底层逻辑理清之后Boot3 生态里很多“莫名奇妙”的依赖报错都能一眼看穿。3. 连上数据库后发现日志里还是 HikariPoolDruid 被“截胡”3.1 现象描述与第一反应依赖换对了项目顺利启动数据库也能连上看起来一切正常。但我随手翻了翻启动日志发现里面有这样一条Starting HikariPool ...我当时心里一紧我明明引入的是 Druid starter怎么跑的还是 Hikari更别提打开/druid/index.html之后SQL 监控列表干干净净一行记录都没有。这是 Boot3 Druid 集成里最隐蔽的一类坑项目能用但是 Druid 根本没成为真正在用的连接池。3.2 三种典型的截胡原因后来我总结下来Druid 被“静默顶替”基本是下面三种情况之一第一种HikariCP 依赖还在 classpath 里而spring.datasource.type没有显式声明。Spring Boot 的自动装配逻辑里DataSource 的创建是“谁的条件先满足谁上”类路径里有 Hikari 时它经常抢跑。稳妥做法是把 Hikari 直接排除掉或者显式声明type两边都做最保险。第二种项目里存在自定义的Bean DataSource配置类。比如老项目迁移时把 Boot2 时代手写的DruidConfig一起复制了过来里面用DataSourceBuilder或直接new HikariDataSource()创建了一个 dataSource。手动注册的 bean 优先级高于 starter 的自动装配你的 Druid 配置根本轮不到执行。第三种依赖树里看着很正常但是spring-boot-starter-jdbc或者spring-boot-starter-data-jpa把 HikariCP 作为默认连接池带进来了。Spring Boot 的默认连接池就是 Hikari这是它文档里写死的行为。排查方法也很直接先跑mvn dependency:tree看 Hikari 的依赖来源再全局搜一下项目里有没有自己声明的DataSourcebean。我那次就是项目里藏了一个老同事写的数据源配置类把 starter 的自动装配完全盖住了。3.3 我的最终配置模板单数据源版经过几次折腾我目前的单数据源配置模板长这样直接抄就能用spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: root druid: db-type: mysql initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 keep-alive: true validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20这里有个很多人问的点url、username、password放在spring.datasource下面而池子参数放在spring.datasource.druid下面为什么不直接全放druid子树里因为 starter 的自动配置是先读取spring.datasource下的公共数据源属性再通过ConfigurationProperties(spring.datasource.druid)把 Druid 特有属性绑定上去。你如果非把url写进druid子树某些版本下会绑定不到启动时直接报Failed to configure a DataSource: url attribute is not specified。所以按上面这个结构来最稳。另外如果你是 MySQL 8driver-class-name必须用com.mysql.cj.jdbc.Driver老的那个com.mysql.jdbc.Driver已经被移除了。URL 里的allowPublicKeyRetrievaltrue也不能省否则 MySQL 8 默认的caching_sha2_password认证方式会报Public Key Retrieval is not allowed这个报错在网上被问烂了。4. 监控台打不开StatViewServlet 注册这件事4.1 最省事路线starter 的 stat-view-servlet 配置Druid 最吸引人的地方就是带了一个可视化监控台能看到连接池实时状态、SQL 执行统计、URI 访问统计。但不少人把依赖配好之后访问/druid/index.html得到一个大大的 404。如果你用的是druid-spring-boot-3-starter监控台的开启其实不用写一行 Java 代码配置里把开关打开就行spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin reset-enable: false然后重启项目浏览器访问http://localhost:8080/druid/index.html就能看到登录页。这里提醒一下如果项目里配了server.servlet.context-path比如/api那入口地址就是/api/druid/index.html别在/druid/index.html上找半天。重置功能reset-enable我建议设成false防止有人恶作剧一键把监控统计清空。生产环境里login-username和login-password不要用弱口令这东西直接暴露了所有 SQL 语句和调用频率属于敏感信息。4.2 手动注册 Servlet 时容易踩的两个点网上很多教程教你在配置类里手动注册StatViewServletConfiguration public class DruidConfig { Bean public ServletRegistrationBeanStatViewServlet druidStatViewServlet() { ServletRegistrationBeanStatViewServlet registration new ServletRegistrationBean(new StatViewServlet(), /druid/*); registration.addInitParameter(loginUsername, admin); registration.addInitParameter(loginPassword, admin); registration.addInitParameter(resetEnable, false); return registration; } }这条路在 Boot3 下目前也是能走的只要你用的 Druid 是 1.2.16 之后的核心包。但我自己不太推荐手写原因有两个。第一个坑是“双重注册”。如果你同时开了 yaml 里的stat-view-servlet.enabled: true又手写了这个 bean某些版本会报 Servlet 名称冲突或者监控页面出现统计重复。这种问题排查起来很烦因为日志里给的错误信息并不直观。第二个坑是老教程的 import 问题。很多博客写的是import javax.servlet.*相关代码拿到 Boot3 项目里直接编译不过。改的时候要注意ServletRegistrationBean这个类在 Spring Boot 3 里并没有换包名但你自己写的 Filter 或者监听器如果要实现接口必须换成jakarta.servlet下的类型。IDEA 的自动导入经常给出一长串候选一定要选jakarta.servlet开头的那个。4.3 WebStatFilter 没配好监控数据照样是 0有一个比监控台 404 更隐蔽的问题监控台能打开登录也能进但页面上的 “URI 监控”“Session 监控” 全是空的。这个基本就是 WebStatFilter 没启用。WebStatFilter 负责的是 Web 层的访问统计它跟统计 SQL 执行的 StatFilter 是两码事。配置同样走 yamlspring: datasource: druid: web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/* session-stat-enable: true profile-enable: trueexclusions必须把/druid/*排除掉否则监控页面本身的请求也会被算进访问统计里数据会非常难看的。另外这个排除列表是用逗号分隔的不要加空格加了空格可能导致静态资源还是被过滤拦截页面加载慢不少。5. 过滤器和慢 SQLStatFilter/WallFilter 被静默跳过的现场5.1 filters 简写在 Boot 场景下为什么不靠谱老一代 Druid 教程都喜欢写一句配置spring: datasource: druid: filters: stat,wall,log4j2这种写法在纯 Spring 项目里没有任何问题Druid 会自己解析这个名字列表然后加载对应的过滤器。但在 Spring Boot 的自动装配场景下这套简写经常“半生效”统计页里 Stat 数据缺斤少两Wall 防火墙有时开有时关而且你很难说是哪里配置冲突了。我个人的建议是Boot 项目里直接放弃filters简写改用filter.*属性树逐项开关声明式配置更可控也方便别人阅读和维护spring: datasource: druid: filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 2000 merge-sql: true wall: enabled: true slf4j: enabled: true这里有个小细节filter.stat.enabled默认其实是true但如果你在filters简写里也写了stat两边就会出现两份配置在打架的情况。踩过这个以后我所有项目都只用filter.*这一种方式描述过滤器。5.2 慢 SQL 统计没输出的三板斧排查log-slow-sql: true配好之后如果慢 SQL 日志依然没出来按下面三步排查基本能定位第一确认阈值设置合理。slow-sql-millis: 2000表示超过 2000 毫秒才算慢 SQL。测试时可以先故意跑一个几百毫秒的查询如果没触发再往下查。第二确认日志级别没被压掉。慢 SQL 是通过com.alibaba.druid.filter.stat.StatFilter这个 logger 打印的如果你的全局日志级别被调到了 ERROR那 WARN 级别的慢 SQL 日志就看不到了。建议在配置里确认logging: level: com.alibaba.druid.filter.stat.StatFilter: warn第三确认 SQL 真的经过了 Druid 数据源。回到第 3 节说的如果你的应用实际用的是 Hikari那 StatFilter 压根没被挂上去配置什么都是白搭。判断方法最简单看/druid/sql.html页面有没有 SQL 记录有记录说明链路通了没有记录就回头查数据源到底是不是 Druid。顺带一提merge-sql: true会把结构相同的 SQL 合并统计比如只差查询条件的语句会被归到一条记录里这样统计页面不会爆炸式增长。但合并之后看单条 SQL 的平均耗时意义会变小这个要心里有数。5.3 WallFilter 报错和 dbType 探测失败的连带问题WallFilter 是 Druid 的 SQL 防火墙可以拦截很多注入攻击。但开着 WallFilter 的时候有一个经典报错java.sql.SQLException: could not load dbType这个报错看着吓人其实根因是 WallFilter 在做 SQL 解析时拿不到数据库类型。Druid 默认会从连接获取元数据推断数据库类型但在某些场景下这个过程会失败——比如连接还没建立成功、或者驱动返回的 metadata 不完整。解决方案就是在配置里手动指定数据库类型也就是第 3 节模板里的db-type: mysql。如果你用的是 PostgreSQL 或 Oracle改成对应的pgsql、oracle就行。另外我自己在把wall过滤器用于生产环境的时候会对multi-statement-allow格外留意。这个参数控制是否允许多条 SQL 用分号拼在一起执行默认是 false也就是不允许。某次项目里用到了批量执行的 SQL一开 WallFilter 就开始报错最后发现是这个参数挡住了。改之前要想清楚你是不是真的需要多条 SQL 拼接如果只是批量插入正规做法是用addBatch而不是去开这个开关。6. 多数据源与后续扩展starter 自动装配的边界6.1 多数据源时 druid starter 自动装配为什么不够用druid starter 的自动装配逻辑本质上只处理“一个 DataSource”的场景。当你的项目需要连接两套数据库或者读写分离主从两个数据源时自动装配的单个 dataSource bean 就满足不了需求了。这时候有两个选择。第一继续用 starter 管理主数据源第二个数据源手动创建第二干脆全部手动创建把配置从spring.datasource.druid.*里拆出来放到自定义的前缀下面逐个绑定。我个人的习惯是第二套方案因为它在多数据源场景下更直观也是个排障友好的结构app: datasource: primary: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo username: root password: root db-type: mysql initial-size: 5 max-active: 20 secondary: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_archive username: root password: root db-type: mysql对应的 Java 配置Configuration public class DataSourceConfig { Bean(name primaryDataSource, initMethod init) ConfigurationProperties(prefix app.datasource.primary) public DruidDataSource primaryDataSource() { return new DruidDataSource(); } Bean(name secondaryDataSource, initMethod init) ConfigurationProperties(prefix app.datasource.secondary) public DruidDataSource secondaryDataSource() { return new DruidDataSource(); } }6.2 手动声明时容易漏掉的几个点手动创建DruidDataSource的时候有三个点稍微不注意就会掉进坑里。第一initMethod init不能省。DruidDataSource 有一个 init 方法负责真正建立连接池Spring 托管时如果不声明 initMethod连接池可能不初始化。你自己在代码里new DruidDataSource()然后不用 Spring 管理的话则要手动调用 init很多新版 IDE 还会给你标黄提示。第二如果监控台还要继续用注意两个数据源都要能区分开。Druid 的连接池监控页可以聚合显示多个数据源但展示时靠的是数据源内部的名字建议在配置里给每个数据源设置清晰的名字。否则页面上全是dataSource-1、dataSource-2这种你根本分不清是哪个库。第三多数据源场景下MyBatis 或 JPA 的SqlSessionFactory也要分别绑定到对应的 dataSource这块属于老生常谈但每次都会有人忘了配。7. Boot3Druid 常见异常速查表最后把这一年多踩坑经验汇总成一张表遇到了直接对照着看省得再翻一遍日志。症状根因处理办法启动报ClassNotFoundException: javax.servlet.Filter引了 Boot2 时代的 druid-spring-boot-starter换成 druid-spring-boot-3-starter1.2.16 以上启动日志出现Starting HikariPool...HikariCP 未被排除或自定义 DataSource bean 覆盖了自动装配排除 Hikari 依赖、删掉/config 自定义数据源 bean、显式声明 type启动报Failed to configure a DataSource: url attribute is not specifiedurl 没写或写错层级把 url/username/password 放到 spring.datasource 下/druid/index.html404stat-view-servlet 没启用或没带 context-path检查 yaml 配置、确认入口前缀监控台能看到页面但 SQL 全空filter.stat 没启用或应用实际用的是 Hikari开启 filter.stat.enabled确认数据源是 Druid开了 log-slow-sql 但没日志logger 级别被压低或 SQL 没走 Druid调整 StatFilter 日志级别确认连接池链路报Public Key Retrieval is not allowedMySQL 8 的 caching_sha2_password 认证URL 加allowPublicKeyRetrievaltrue报could not load dbTypeDruid 无法自动推断数据库类型手动配置db-type引入 knife4j 后启动失败用了 Boot2 时代的 knife4j 版本换 4.x 的 openapi3-jakarta starter提示 Servlet 重复注册或统计重复即开了 yaml 开关又手动注册了 StatViewServlet只保留一种注册方式最后聊两句我现在的工作习惯。新项目接 Druid我基本是“starter yaml 一条龙”不再手写数据源配置类除非真有多数据源需求。老项目升级 Boot3我第一件事永远是跑依赖树、找 javax 相关传递依赖然后才是改业务代码。慢 SQL 排查我也不是只看日志而是先把 Druid 监控台的 SQL 页面当第一道筛子从里面看命中次数和耗时分布再决定要不要针对某条 SQL 深挖。这套流程踩了这么多坑之后跑下来已经稳定很长一段时间了。
返回列表