PAYATHON 2026

添加 jasperreports 依赖后 Spring ApplicationContext 加载失败

数据阿明

结论

mysql-connector-java 出现在 Maven dependency tree 中,只说明 Maven 解析到了这个 JAR,并不能证明当前 Spring 测试上下文的类加载器已经成功注册 MySQL JDBC Driver。

根据现有信息,还不能断定 JasperReports 与 MySQL Connector/J 存在冲突。更可能的原因是,添加 JasperReports 后,依赖解析顺序或测试运行时的 classpath 发生了变化,项目中原有的版本混用和 JDBC Driver 自动发现问题随之暴露。

建议先处理两件事:

  1. 显式配置 MySQL Driver。Connector/J 6 对应的驱动类是 com.mysql.cj.jdbc.Driver。
  2. 统一 Spring、Jackson、Cucumber 等依赖的版本,并排除 JasperReports 中不需要或存在冲突的传递依赖。

为什么 JAR 在 classpath 中仍然找不到 Driver

DriverManager 建立连接前,需要先注册一个能够处理 jdbc:mysql: URL 的 Driver。Connector/J 通常通过 JDBC 4 的 Service Provider 机制自动完成注册,但以下情况可能导致自动注册失效:

  • 测试框架使用了不同的上下文类加载器;
  • Surefire、Cucumber 或应用容器创建了隔离的 classpath;
  • 打包或重打包时丢失了 META-INF/services/java.sql.Driver;
  • 实际运行时 classpath 与 mvn dependency:tree 显示的编译 classpath 不一致;
  • 数据源配置仍在使用旧的驱动类名;
  • 同一套框架的模块版本不一致,导致 ApplicationContext 初始化异常。

因此,下面这条异常并不表示”POM 中没有 MySQL 依赖”,而是表示 DriverManager 发起连接时,没有找到已注册且能够接受该 URL 的 Driver:

java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost/my_database

先显式配置 MySQL Driver

Connector/J 6 应使用:

com.mysql.cj.jdbc.Driver

如果使用 Spring XML 数据源,可以明确指定驱动类:

<bean id="dataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/my_database"/>
    <property name="username" value="${database.username}"/>
    <property name="password" value="${database.password}"/>
</bean>

如果使用 EclipseLink/JPA,可以在 persistence.xml 中配置:

<property name="javax.persistence.jdbc.driver"
          value="com.mysql.cj.jdbc.Driver"/>
<property name="javax.persistence.jdbc.url"
          value="jdbc:mysql://localhost:3306/my_database"/>
<property name="javax.persistence.jdbc.user"
          value="${database.username}"/>
<property name="javax.persistence.jdbc.password"
          value="${database.password}"/>

也可以在创建数据源前临时执行:

Class.forName("com.mysql.cj.jdbc.Driver");

Class.forName 更适合用于诊断或兼容旧环境。生产环境通常应由连接池或 Spring 数据源明确声明 Driver。

如果显式加载后连接恢复,基本可以确认问题出在 JDBC Driver 自动发现或类加载器边界,而不是数据库 URL。

检查真正的测试运行时 classpath

先查看 MySQL Driver 来自哪条依赖链,以及最终解析到了哪个版本:

mvn dependency:tree \
  -Dverbose \
  -Dincludes=mysql:mysql-connector-java

再检查其他相关冲突:

mvn dependency:tree \
  -Dverbose \
  -Dincludes=org.springframework,com.fasterxml.jackson.core,commons-digester,io.cucumber,info.cukes

还要确认测试阶段确实能够加载 Driver:

@Test
public void mysqlDriverShouldBeAvailable() throws Exception {
    Class<?> driverClass = Class.forName("com.mysql.cj.jdbc.Driver");
    assertNotNull(driverClass);

    Driver driver = DriverManager.getDriver(
        "jdbc:mysql://localhost:3306/my_database"
    );
    assertNotNull(driver);
}

为了确认 JAR 的实际加载位置,可以临时打印:

System.out.println(
    com.mysql.cj.jdbc.Driver.class
        .getProtectionDomain()
        .getCodeSource()
        .getLocation()
);

System.out.println(
    Thread.currentThread().getContextClassLoader()
);

如果 Class.forName 抛出 ClassNotFoundException,说明 Connector/J 没有进入测试运行时 classpath。如果 Class.forName 成功,但自动发现仍然失败,就应重点检查 Service Provider 文件、测试类加载器和打包方式。

当前依赖树中确实存在高风险的版本混用

Spring 模块没有对齐

依赖树中同时出现了:

  • spring-core、spring-context、spring-webmvc 4.3.8;
  • spring-orm、spring-jdbc、spring-tx 4.1.8;
  • Spring Security 传递引入的 4.1.6;
  • Spring Batch 传递引入的 4.0.x。

Maven 显示 omitted for conflict,不代表版本问题已经全部解决。例如,依赖树实际保留的可能是:

spring-core    4.3.8.RELEASE
spring-context 4.3.8.RELEASE
spring-orm     4.1.8.RELEASE
spring-jdbc    4.1.8.RELEASE
spring-tx      4.1.8.RELEASE

同一个 Spring Framework 的模块应尽量保持版本一致。可以导入 Spring Framework BOM:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>4.3.8.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

导入 BOM 后,不要再为各个 Spring 模块分别声明不同版本:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
</dependency>

同时还要确认 Spring Data JPA、Spring Security 和 Spring Batch 是否支持最终选定的 Spring Framework 版本。只依赖 Maven 的”最近依赖优先”规则强行覆盖版本,无法保证二进制兼容。

Jackson 组合已经不一致

当前实际解析结果可能是:

jackson-databind     2.9.3
jackson-core         2.1.4
jackson-annotations  2.1.4

这三个版本并不是一套可靠的 Jackson 组合。JasperReports 6.5.1 引入了较旧的 Jackson 组件,而项目又直接声明了 jackson-databind 2.9.3。

如果应用已经统一管理 Jackson,可以排除 JasperReports 带入的这些传递依赖:

<dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports</artifactId>
    <version>6.5.1</version>

    <exclusions>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </exclusion>
    </exclusions>
</dependency>

然后通过 dependency management 将三个 Jackson 核心组件统一到兼容版本。具体选用哪个版本,应根据项目的支持范围决定,不能仅因为某个版本”较新”就直接覆盖。

Commons Digester 版本也被反向选择

JasperReports 请求的是 commons-digester:2.1,但 Maven 最终选择了项目依赖链中更近的 1.8.1:

commons-digester:1.8.1
commons-digester:2.1 - omitted for conflict

如果 JasperReports 的运行路径会用到 Digester,这种版本选择可能引发 NoSuchMethodError、解析异常或初始化失败。需要评估 commons-validator 能否与 Digester 2.1 配合,或者隔离两条依赖链,不能因为 Maven 已经完成版本选择,就认为它选出的一定是正确版本。

Cucumber 同时混用了两个坐标体系

依赖树中同时存在:

io.cucumber:cucumber-java:2.1.0
io.cucumber:cucumber-junit:2.1.0
info.cukes:cucumber-spring:1.2.5

这可能导致 Cucumber 2.1.0 和 1.2.5 的核心模块被同时加载。由于 cucumber-spring 会参与创建 Spring 测试上下文,这组混用应优先清理。

Spring 集成模块应与其他 Cucumber 模块使用相同的坐标和版本。例如,整套依赖都采用 io.cucumber:2.1.0 时,可以配置:

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-spring</artifactId>
    <version>2.1.0</version>
    <scope>test</scope>
</dependency>

同时移除:

<dependency>
    <groupId>info.cukes</groupId>
    <artifactId>cucumber-spring</artifactId>
</dependency>

建议的处理顺序

  1. 在数据源或 JPA 配置中显式使用 com.mysql.cj.jdbc.Driver。
  2. 编写独立测试,验证 Class.forName 和 DriverManager.getDriver()。
  3. 检查 test scope 的实际 classpath,不要只看 compile scope。
  4. 将所有 Spring Framework 模块统一到同一版本。
  5. 将所有 Cucumber 模块统一到同一坐标体系和版本。
  6. 统一 Jackson 三个核心组件的版本。
  7. 逐项排除 JasperReports 中不需要的传递依赖。
  8. 执行干净构建,防止 IDE 或测试插件继续使用旧的 classpath:
mvn clean test
  1. 添加 Maven Enforcer 规则,让构建在依赖版本出现分歧时直接失败:
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <executions>
        <execution>
            <id>enforce-dependency-convergence</id>
            <goals>
                <goal>enforce</goal>
            </goals>
            <configuration>
                <rules>
                    <dependencyConvergence/>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

注意事项

不要通过重复添加 MySQL 依赖来处理这个问题。依赖树已经说明 Maven 解析到了 Connector/J,现在需要确认的是,它是否进入了测试运行时 classpath,以及 Driver 是否已被正确加载。

也不要一次排除 JasperReports 的全部传递依赖。即使应用只加载已经编译好的 .jasper 文件,不需要报表编译器的全部功能,导出 PDF、处理字体、图表、XML 或 JSON 数据源时,仍可能用到其中一部分组件。每排除一项依赖,都应通过实际的报表加载和导出测试进行验证。

No suitable driver found 只是最外层的症状。排查时应查看完整异常链中最早出现的异常,尤其是 ClassNotFoundException、ServiceConfigurationError、NoSuchMethodError 和 LinkageError。与 Spring 最终包装出的 ApplicationContext 加载失败相比,这些异常通常更接近问题的真正原因。

备注:内容仅供参考。