部署 Spring 6 war 至 Tomcat 10 时没有合适的驱动程序可用

No suitable drivers available when deploying a Spring 6 war to Tomcat 10

在过去的几年里,我没有使用这种传统方式将 war 部署到 tomcat。

这几天,我尝试更新到Spring 6/Jakarta EE 9/Java 17,并创建了一个简单的示例来演示Spring WebMvc war 应用程序,当通过 cargo maven 插件或手动部署到 Tomcat 10 时,我在初始化休眠时在应用程序启动阶段得到了著名的 no suitable drivers

但它在单元测试中有效。

我也试过部署到外部Tomcat服务器,先复制一个postgres jar到tomcat/lib文件夹,然后复制war打包到tomcat/webapps文件夹,手动启动,同样报错

我的示例项目:https://github.com/hantsy/spring6-sandbox/tree/master/jpa,基于以下技术栈:

要重现问题,请在项目文件夹中通过 docker compose up postgres 和 运行 mvn clean package cargo:run -Ptomcat 启动 Postgres,以部署到 cargo managed tomcat 服务器。

PS: 我还配置了 Jetty 和 WildFly,都运行良好。

更新:这个问题有点奇怪,因为将 Postgres jdbc 驱动程序打包到 war 与 Spring5/Tomcat9 一起工作得很好。但是在这个升级到 Spring6/Tomcat10 的样本中它失败了。为了让它工作,我必须 从 war 包 中排除 pg Jdbc 驱动程序,并将其复制到公共 tomcat/lib文件夹,查看integration tests了解更多详情。有关 Jdbc 驱动程序加载的更多信息,请参阅@PiotrP.Karwasz 的回答。

顺便说一句,Tomcat 是 Java 开发人员的首选 Web 服务器,原因之一是大多数流行的 Jdbc 驱动程序在将其打包到 wars 时运行良好,相比之下,大多数传统应用服务器(WildFly、Glassfish 等)在过去几年都不支持它(您必须通过应用服务器命令行工具或管理控制台将其安装到应用服务器)。但是从 Java EE 7 开始,它允许开发人员定义应用程序范围的 DataSources(通过 @DataSourceDefinition)并将 Jdbc 驱动程序直接打包到应用程序 war 包中,当应用程序正在部署时,它会自动注册 Jdbc 个驱动程序。不确定为什么最新的 Tomcat 增加了开发复杂性并使用遗留应用程序服务器方法。

简短回答:在您的 DataSourceConfig class you don't call DriverManagerDataSource#setDriverClassName 中,因此 DriverManager 不知道在哪里可以找到适合 jdbc:postgresql: URI 的驱动程序.

长答案:等等,我真的需要指定驱动程序的 class 名称吗,JDBC 驱动程序不会被自动检测到吗?是的,因为 Java 6 和 JDBC 4,JDBC 驱动程序通过 ServiceLoader 机制自动检测。

然而,在第一次调用 DriverManager.getDrivers() 期间,这种情况只发生 一次 。 Tomcat 确保这发生在服务器的 class 路径(参见 JreMemoryLeakPrevention),因此只有全局安装的驱动程序被注册(没有对应用程序 classloader 的引用被保存系统 classloader).

因此,无需指定驱动程序的 class 名称,您可以:

  • 在服务器的 class 路径(例如 $CATALINA_BASE/lib)中安装 PostgreSQL JDBC 驱动程序,然后 将其从应用程序中删除
  • 或者确保在您的应用程序启动时加载驱动程序(并在停止时注销):
    Class.forName("org.postgresql.Driver");
    
  • 您还可以模仿 DriverManager 使用的自动发现机制来注册随您的应用程序分发的所有驱动程序:
     private static void registerJdbcDrivers(ServletContext context) {
         final ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class, context.getClassLoader());
         final Iterator<Driver> iter = serviceLoader.iterator();
         while (iter.hasNext()) {
             // Just for the side-effect of loading the class and registering the driver
             iter.next();
         }
     }