GeoTools 多模块依赖最佳实践:一次 OrderedAxisAuthorityFactory 初始化失败的深度复盘

前言

在基于 Spring Boot 3 + JDK 17 的服务中,由于在多个模块都引入了Geotools,一开始没问题就没管,后面有次突然就报错了

java 复制代码
java.lang.NoClassDefFoundError: Could not initialize class
org.geotools.referencing.factory.OrderedAxisAuthorityFactory

这个报错并不直接指向业务代码,而是卡在了 GeoTools 的底层初始化阶段。经过排查,我们发现:这并非 GeoTools 的 Bug,而是多模块项目中典型的"基础设施依赖管理失控"。

本文将用图示 + 可落地的 POM 示例,完整复盘这一问题的根本原因,并给出 GeoTools 在多模块架构下的标准引入方案。

一、错误现象:OrderedAxisAuthorityFactory 初始化失败

异常堆栈的核心在于 OrderedAxisAuthorityFactory 这个类的静态初始化失败。

OrderedAxisAuthorityFactory 是 GeoTools 内部用于按优先级排序坐标系工厂的关键类。它在静态代码块中依赖 gt-epsg 相关模块来读取 EPSG 坐标系定义。当以下任一条件不满足时,就会触发 ExceptionInInitializerError,进而导致后续的 NoClassDefFoundError

  1. EPSG 工厂实现类在 classpath 中找不到
  2. SPI 配置文件(META-INF/services)被覆盖或丢失
  3. 依赖的 JTS 版本与 GeoTools 不匹配
  4. Spring Boot Fat Jar 未解压导致资源文件不可读

下文将逐一分析这些条件是如何被触发的。

二、根因分析:多模块依赖的"散养"模式

大部分人可能都会犯的错------将 GeoTools 的依赖随意分散到了多个模块中。

问题一:版本分裂

不同模块通过不同的传递依赖,各自引入了不同版本的 GeoTools Jar 包。同一个 JVM 中,GeoTools 的类由不同的 ClassLoader 加载,版本不一致。

问题二:SPI 冲突

GeoTools 重度依赖 Java SPI(Service Provider Interface)机制来注册坐标系工厂。多个模块各自携带 SPI 配置文件,Spring Boot 打包后,这些配置文件相互覆盖或丢失,最终使得 OrderedAxisAuthorityFactory 在寻找实现类时扑空。

问题三:新旧 JTS 的"宫斗"

项目中同时存在旧版 com.vividsolutions:jts(已废弃)和新版 org.locationtech.jts:jts-core。GeoTools 29+ 版本强依赖 LocationTech 的 JTS。当类加载器先加载了旧版 JTS 的类,或者两者在 classpath 中打架时,GeoTools 的几何工厂初始化会直接失败。

三、最终解决方案:建立"GIS 基础设施隔离层"

核心思路是:新增一个独立模块专门承载 GeoTools 和 JTS,其他模块只通过依赖这个模块来间接使用 GIS 能力。

3.1 正确的模块分层结构

假设我们有一个多模块项目,包含以下模块:

  • test-a:业务模块 A
  • test-b:业务模块 B
  • test-c:业务模块 C
  • gt-spatial:GIS 基础设施模块(新增)
  • boot:启动模块

正确的分层和依赖方向如下:

层级 模块 说明
infrastructure gt-spatial GeoTools + JTS 的唯一住所
domain test-a 业务核心,依赖 infrastructure
business test-b / test-c 业务模块,依赖 test-a
application boot 启动入口,依赖以上所有

依赖方向永远向下,绝不反向,绝不横向互吸。

3.2 模块依赖关系图

以下文字示意图展示了正确的模块依赖方向:

复制代码
  gt-spatial        (infrastructure 层)
      ^
      |
  test-a            (domain 层)
      ^
      |
  test-b / test-c   (business 层)
      ^
      |
  boot              (application 层)

3.3 各模块 POM 文件示例

第一步:Parent POM 统一版本管理

在根 POM 的 dependencyManagement 中锁定所有 GeoTools 和 JTS 版本

xml 复制代码
<dependencyManagement>
  <dependencies>
    <!-- GeoTools -->
    <dependency>
      <groupId>org.geotools</groupId>
      <artifactId>gt-referencing</artifactId>
      <version>31.2</version>
    </dependency>
    <dependency>
      <groupId>org.geotools</groupId>
      <artifactId>gt-epsg-wkt</artifactId>
      <version>31.2</version>
    </dependency>
    <dependency>
      <groupId>org.geotools</groupId>
      <artifactId>gt-main</artifactId>
      <version>31.2</version>
    </dependency>
    <dependency>
      <groupId>org.geotools</groupId>
      <artifactId>gt-geojson</artifactId>
      <version>31.2</version>
    </dependency>
    <dependency>
      <groupId>org.geotools.jdbc</groupId>
      <artifactId>gt-jdbc-postgis</artifactId>
      <version>31.2</version>
    </dependency>
    <!-- JTS -->
    <dependency>
      <groupId>org.locationtech.jts</groupId>
      <artifactId>jts-core</artifactId>
      <version>1.19.0</version>
    </dependency>
  </dependencies>
</dependencyManagement>
第二步:gt-spatial 模块(基础设施层)

这个模块的唯一职责就是封装 GeoTools 和 JTS,相关的GIS方法都统一写在这个模块,不写其他任何业务逻辑:

xml 复制代码
<!-- gt-spatial/pom.xml -->
<artifactId>gt-spatial</artifactId>
<dependencies>
  <!-- GeoTools 核心 -->
  <dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-referencing</artifactId>
  </dependency>
  <dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-epsg-wkt</artifactId>
  </dependency>
  <dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-main</artifactId>
  </dependency>
  <!-- 实际用到的能力 -->
  <dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-geojson</artifactId>
  </dependency>
  <dependency>
    <groupId>org.geotools.jdbc</groupId>
    <artifactId>gt-jdbc-postgis</artifactId>
  </dependency>
  <!-- JTS(唯一入口) -->
  <dependency>
    <groupId>org.locationtech.jts</groupId>
    <artifactId>jts-core</artifactId>
  </dependency>
</dependencies>
第三步:test-a 模块(业务核心层)

只依赖 gt-spatial,不自己引 GeoTools:

xml 复制代码
<!-- test-a/pom.xml -->
<dependencies>
  <!-- 通过 gt-spatial 间接获得 GeoTools -->
  <dependency>
    <groupId>com.example</groupId>
    <artifactId>gt-spatial</artifactId>
  </dependency>
</dependencies>
第四步:test-b / test-c 模块(业务模块层)

只依赖 test-a,不直接碰 GeoTools:

xml 复制代码
<!-- test-b/pom.xml -->
<dependencies>
  <dependency>
    <groupId>com.example</groupId>
    <artifactId>test-a</artifactId>
  </dependency>
</dependencies>

3.4 GeoTools 多模块引入的铁律

经过此次事故,我们总结了 GeoTools 在多模块项目中的引入铁律:

铁律 说明
单点引入 整个项目有且只有一个 Module 负责引入 GeoTools
版本托管 在 Root POM 的 dependencyManagement 中锁定版本,子模块不写版本号
JTS 隔离 JTS 必须跟随 GeoTools 待在同一个 Module 里,严禁业务模块单独引入
最小化依赖 按需引入,不用的 GeoTools 模块坚决不加,减少 SPI 文件冲突概率

总结

NoClassDefFoundError: OrderedAxisAuthorityFactory 这个报错,本质上是类加载机制与依赖管理混乱的碰撞。

GeoTools 作为一个重度依赖 SPI 和静态初始化的老牌 GIS 库,对运行环境的"纯净度"要求极高。在多模块项目中,它不应该被当作"工具 Jar"随意散落在各个业务模块中,而应该被封装在独立的"基础设施层",像 Spring、Hibernate 一样被对待。

通过这次重构,我们不仅修复了启动报错,更重要的是确立了一种防御性的依赖管理思维:对于基础设施级依赖,应当将其封装在独立的"防腐层"中,严格限制其对外暴露的范围。这样既能避免依赖地狱,也能让未来的升级维护变得可控。

如果你也在 Spring Boot 多模块项目中遇到了类似的 GeoTools 初始化问题,不妨检查一下你的依赖树------也许,是时候给 GeoTools 安一个"单间"了。