Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions build/build-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
The 'revision' property value is conserved by the flatten-maven-plugin, so the model project does not override it.
-->
<version.ai.timefold.solver>${revision}</version.ai.timefold.solver>
<version.ch.qos.logback>1.5.34</version.ch.qos.logback>
<version.ch.qos.logback>1.2.12</version.ch.qos.logback>
<version.com.networknt.json-schem-validator>1.5.9</version.com.networknt.json-schem-validator>
<version.jakarta.enterprise.cdi-api>4.1.0</version.jakarta.enterprise.cdi-api>
<version.io.quarkus>3.36.2</version.io.quarkus>
Expand All @@ -46,12 +46,11 @@
<version.org.mapstruct>1.6.3</version.org.mapstruct>
<version.org.openrewrite.recipe>3.33.0</version.org.openrewrite.recipe>
<version.org.testcontainers>2.0.5</version.org.testcontainers>
<version.org.springframework.boot>4.1.0</version.org.springframework.boot>
<version.org.springframework.boot>2.7.18</version.org.springframework.boot>
<version.org.wiremock>3.13.2</version.org.wiremock>
<version.ow2.asm>9.10.1</version.ow2.asm>
<!-- Upgrade Jackson and its annotations together; otherwise there may be runtime errors. -->
<version.tools.jackson>3.2.0</version.tools.jackson>
<version.com.fasterxml.jackson.core>2.22</version.com.fasterxml.jackson.core>
<!-- Jackson version managed by Spring Boot 2.7 BOM (2.13.5); override for Quarkus compatibility -->
<version.com.fasterxml.jackson.core>2.13.5</version.com.fasterxml.jackson.core>

<!-- ************************************************************************ -->
<!-- Plugins -->
Expand Down Expand Up @@ -347,6 +346,14 @@
<ignoreClass>*</ignoreClass>
</ignoreClasses>
</dependency>
<dependency>
<!-- spring-jcl and jcl-over-slf4j both provide commons-logging API; SB 2.7 uses jcl-over-slf4j -->
<groupId>org.springframework</groupId>
<artifactId>spring-jcl</artifactId>
<ignoreClasses>
<ignoreClass>*</ignoreClass>
</ignoreClasses>
</dependency>
</dependencies>
<scopes>
<scope>compile</scope>
Expand Down Expand Up @@ -637,7 +644,7 @@
<useModulePath>false</useModulePath>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
<skipTests>${skipUTs}</skipTests>
<!-- Avoid defining the argLine; it's configured in a profile for code coverage -->
<argLine>--add-opens ai.timefold.solver.spring.boot.autoconfigure/ai.timefold.solver.spring.boot.autoconfigure.invalid.type=spring.core</argLine>
</configuration>
</plugin>
<plugin>
Expand Down
186 changes: 186 additions & 0 deletions openspec/changes/spring-boot-2x-compatibility/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
## Context

Timefold Solver 当前在 `main` 分支上适配 Spring Boot 4.1.0(Spring Framework 7.x),使用 Java 21 编译,依赖 Jackson 3 (`tools.jackson`)、AOT 编译支持(Spring 6+ 引入的 `BeanFactoryInitializationAot*` API)以及 `spring-boot-persistence` 模块(SB 3.0+ 引入)。本次改造需要在独立分支 `caj/spring-boot-2x` 上将整个项目降级至 Spring Boot 2.7.18 兼容,且后续需频繁 rebase main 以保持与上游同步。

### 当前依赖关系图

```
timefold-solver-spring-integration/
├── spring-integration/pom.xml
│ ├── imports tools.jackson:jackson-bom (Jackson 3) ← 需替换
│ └── imports spring-boot-dependencies (SB 4.1.0) ← 需降级
├── spring-boot-starter/
│ ├── depends spring-boot-starter ← 版本由 BOM 管理
│ ├── depends spring-boot-persistence ⚠️ SB 2.7 不存在 ← 需移除
│ └── depends timefold-solver-jackson (Jackson 3) ← 需降级后适配
├── spring-boot-autoconfigure/
│ ├── depends spring-boot-persistence ⚠️ SB 2.7 不存在 ← 需移除
│ ├── imports EntityScan/EntityScanner/EntityScanPackages
│ │ from org.springframework.boot.persistence.autoconfigure ← 包路径迁移
│ ├── implements BeanFactoryInitializationAotProcessor ← 需移除
│ ├── contains TimefoldSolverAotContribution ← 需删除
│ ├── contains TimefoldSolverAotFactory ← 需重构
│ └── uses NativeDetector (Spring 5.3 中存在 ✓) ← 保留但简化
└── spring-boot-integration-test/
└── native profile uses process-aot goal ← 需移除

timefold-solver-persistence/jackson/
├── 依赖 tools.jackson (Jackson 3) ← 全模块降级
├── JacksonModule 构建 API (Jackson 3 专属) ← 需适配
└── 60+ 文件使用 tools.jackson.* import ← 批量替换
```

### SB 2.7.18 关键依赖版本

| 依赖 | SB 4.1.0 版本 | SB 2.7.18 版本 |
|------|-------------|--------------|
| Spring Framework | 7.x | 5.3.31 |
| Jackson BOM | (不受 SB 管理) | 2.13.5 |
| JUnit Jupiter | 6.1.0 (自定义) | 5.8.2 |
| Hibernate | (受 SB 管理) | 5.6.15.Final |
| Micrometer | (受 SB 管理) | 1.9.17 |
| Logback | 1.5.34 (自定义) | 1.2.12 |
| SLF4J | (受 SB 管理) | 1.7.36 |
| SnakeYAML | (受 SB 管理) | 1.30 |
| Jakarta XML Bind | (自定义版本) | 2.3.3 (SB BOM) |
| Jakarta Persistence | (自定义版本) | 2.2.3 (SB BOM) |
| Jakarta Validation | (自定义版本) | 2.0.2 (SB BOM) |
| Jakarta CDI | 4.1.0 (自定义) | (不在 SB 2.7 BOM 中) |

## Goals / Non-Goals

**Goals:**

- 整个项目(core、persistence、spring-integration、service、tools)在 Java 17 + SB 2.7.18 下成功编译
- spring-integration 和 spring-boot-integration-test 的所有测试通过
- Jackson 序列化/反序列化完全兼容 Jackson 2.13.x API
- 自动配置在 SB 2.7 下的 Bean 装配行为与当前 SB 4.x 一致(多求解器、约束校验器等场景)
- Quarkus 集成模块保持原有版本不变
- 代码结构便于后续 rebase main 时最小化冲突

**Non-Goals:**

- 不支持 SB 2.7 的 GraalVM Native Image(配置保留但不保证可用)
- 不提供 Jackson 3 和 Jackson 2 双版本共存(仅保留 Jackson 2)
- 不修改 core 模块的核心引擎代码
- 不修改 service 模块(基于 Quarkus/CDI)
- 不追求与 SB 3.x 同时兼容(此分支仅针对 SB 2.7.x)

## Decisions

### 决策 1: Jackson 3 → Jackson 2 完整降级

**选择**: 将 `persistence/jackson/` 整个模块从 `tools.jackson` (Jackson 3) 降级到 `com.fasterxml.jackson` (Jackson 2.13.5)

**替代方案及否决理由**:
- ❌ 双版本共存(维护两个 Jackson 模块): 增加维护成本,且此次目标是 SB 2.7 专项适配
- ❌ 仅在 spring-integration 中做转换层: core 和 persistence 已有深度 Jackson 集成,转换层不可行

**具体 API 映射**:

| Jackson 3 (`tools.jackson`) | Jackson 2 (`com.fasterxml.jackson`) |
|------------------------------|--------------------------------------|
| `tools.jackson.databind.JacksonModule` | `com.fasterxml.jackson.databind.Module` |
| `tools.jackson.databind.json.JsonMapper` | `com.fasterxml.jackson.databind.json.JsonMapper` (同名但有细微差异) |
| `tools.jackson.core.JacksonException` | `com.fasterxml.jackson.core.JacksonException` |
| `tools.jackson.databind.ValueDeserializer` | `com.fasterxml.jackson.databind.ValueDeserializer` |
| `tools.jackson.databind.DatabindException` | `com.fasterxml.jackson.databind.DatabindException` |

**风险**: `JsonMapper` 构造方式在 Jackson 3 中有所变化(如 builder 方法名、module 注册方式),需要逐一适配。

### 决策 2: AOT 代码处理策略

**选择**: 完全删除 AOT 相关类,将 `TimefoldSolverAutoConfiguration` 重构为纯 `@Configuration` + `@Bean` 模式

**当前 AOT 代码结构**:
```
TimefoldSolverAutoConfiguration
├── implements BeanFactoryInitializationAotProcessor ← 删除
├── processAheadOfTime() → TimefoldSolverAotContribution ← 删除
└── postProcessBeanDefinitionRegistry() ← 重构为 @Bean 方法
└── 通过注册 RootBeanDefinition 创建 SolverConfig/SolverManager Bean
```

**重构后**:
```
TimefoldSolverAutoConfiguration
└── @Bean 方法直接创建 SolverConfig/SolverManager Bean
通过 TimefoldSolverAotFactory → 重命名为 SolverConfigFactory
作为普通的 @Configuration 内部工厂方法
```

**理由**: SB 2.7 中 `@Configuration` 类本身就支持 `@Bean` 方法的条件装配,不需要通过 `BeanDefinitionRegistryPostProcessor` 手动注册 Bean。简化了代码路径,同时保持了多求解器配置场景的正确性。

**保留**: `TimefoldSolverAotFactory` 重命名为 `SolverConfigFactory`,保留其从 XML 字符串反序列化 SolverConfig 的逻辑,但不再作为 AOT 工厂,而是作为普通 Spring Bean 工厂使用。

### 决策 3: spring-boot-persistence 替换方案

**选择**: 移除 `spring-boot-persistence` 依赖,将 `EntityScan`/`EntityScanner`/`EntityScanPackages` 的 import 改为 `org.springframework.boot.autoconfigure.domain.*`

**影响细节**:
- `IncludeAbstractClassesEntityScanner` 继承的 `EntityScanner` 在 SB 2.7 中位于 `org.springframework.boot.autoconfigure.domain.EntityScanner`
- API 签名完全一致,仅包路径不同
- `@EntityScan` 注解移到 `org.springframework.boot.autoconfigure.domain.EntityScan`

### 决策 4: 自动配置注册文件

**选择**: 创建 `META-INF/spring.factories`,同时保留 `AutoConfiguration.imports`(SB 2.7 会忽略后者,不会产生冲突)

```
# spring.factories (SB 2.7 使用)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
ai.timefold.solver.spring.boot.autoconfigure.TimefoldSolverAutoConfiguration,\
ai.timefold.solver.spring.boot.autoconfigure.TimefoldBenchmarkAutoConfiguration,\
ai.timefold.solver.spring.boot.autoconfigure.TimefoldSolverBeanFactory
```

### 决策 5: 依赖版本管理策略

**选择**: 尽量使用 SB 2.7.18 BOM 管理的版本,仅对 Timefold 明确需要的版本进行显式覆盖

**显式保留版本覆盖的依赖**:
- `jakarta.xml.bind-api` / `jaxb-runtime`: core 深度依赖,版本由 Timefold 自己管理
- `jakarta.persistence-api`: JPA 模块使用,版本独立于 SB
- `jakarta.enterprise.cdi-api`: service 模块使用(Quarkus 生态,独立于 Spring)
- `gizmo2`: Quarkus Gizmo 字节码库,独立版本
- `ow2.asm`: 字节码操作,独立版本
- `freemarker`: 模板引擎,独立版本
- `commons-math3`: 数学库,独立版本
- `jspecify`: JSpecify 注解,独立版本
- `json-schema-validator`: JSON Schema 验证,独立版本

**交由 SB 2.7 BOM 管理的依赖**:
- Jackson 2 (com.fasterxml.jackson)
- JUnit Jupiter
- Mockito
- AssertJ
- Logback / SLF4J
- Micrometer
- SnakeYAML
- Hibernate (仅 persistence/jpa 测试中使用)

### 决策 6: Java 编译器目标版本

**选择**: `maven.compiler.release` 从 21 改为 17

**理由**: SB 2.7.18 官方支持 Java 17,且 Timefold 当前代码使用的语言特性(records、var、text blocks、Stream.toList()、String.formatted())全部在 Java 17 中已 GA。

**需要检查的风险点**: 代码中是否使用了 Java 18+ 的 API(如 `Character.isEmoji()` 等),如有需要替换为兼容实现。

## Risks / Trade-offs

| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|----------|
| Jackson 2 API 细微差异导致序列化行为变化 | 中 | 高 | 运行全部 persistence/jackson 测试 + spring-integration 测试,验证 round-trip 序列化 |
| AOT 重构后多求解器场景下 Bean 装配顺序问题 | 中 | 中 | 保留现有的多求解器测试用例(`TimefoldSolverMultipleSolverAutoConfigurationTest`),确保全部通过 |
| SB 2.7 的 Spring 5.3 `@Configuration(proxyBeanMethods=false)` 行为差异 | 低 | 低 | 两个版本均支持此配置,行为一致 |
| WireMock 版本降级可能导致测试 API 不兼容 | 中 | 低 | WireMock 仅在测试中使用,如遇到不兼容的 API 可单独调整测试代码 |
| Rebase main 时 `build-parent/pom.xml` 持续冲突 | 高 | 中 | 尽量减少不必要的版本变量变更;只改与降级直接相关的版本属性 |
| `jakarta.*` 版本在 SB 2.7 BOM 和 Timefold 现有版本间可能冲突 | 中 | 中 | 对 JAXB/JPA/CDI 等使用显式版本覆盖,不依赖 SB BOM 管理这些 Jakarta API 版本 |
| Testcontainers 大版本差异(2.0.5 → 1.17.x) | 中 | 中 | 仅在少数集成测试中使用,如遇到不兼容需逐个适配 |

## Open Questions

- **WireMock 版本**: 当前使用 3.13.2,SB 2.7 不管理 WireMock。是否需要显式降级到 2.x 版本?还是可以保留 3.x(WireMock 3 需要 Java 11+,与 Java 17 兼容)?
- **Logback 版本冲突风险**: SB 2.7.18 管理 logback 1.2.12,当前 Timefold 声明了 1.5.34。降级后是否会有 SLF4J 绑定兼容问题?
- **Testcontainers 兼容性**: SB 2.7.18 BOM 管理的是旧版 Testcontainers,而当前代码使用 2.0.5 版本。是否应该保持较新版本(手动覆盖)?
42 changes: 42 additions & 0 deletions openspec/changes/spring-boot-2x-compatibility/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## Why

当前 Timefold Solver 基于 Spring Boot 4.1.0 和 Java 21 构建,而大量企业生产环境仍运行在 Spring Boot 2.7.x + Java 17 生态上。此次降级旨在让 Timefold Solver 能够在 Spring Boot 2.7.18 环境中正常编译、运行和测试,使存量企业用户无需升级 Spring Boot 大版本即可使用最新版本的 Timefold Solver。

## What Changes

- **Spring Boot 版本降级**: 4.1.0 → 2.7.18,连带 Spring Framework 7.x → 5.3.31
- **Java 编译版本降级**: 21 → 17
- **Jackson 版本降级**: Jackson 3 (`tools.jackson`) → Jackson 2 (`com.fasterxml.jackson`),影响 `persistence/jackson/` 整个模块和 `spring-integration/` 中的相关自动配置
- **移除 AOT 编译支持**: 删除 `BeanFactoryInitializationAotContribution`、`BeanFactoryInitializationAotProcessor` 相关代码,这些是 Spring Framework 6+ 才有的 API
- **替换 spring-boot-persistence 依赖**: SB 2.7 中不存在 `spring-boot-persistence` 模块,`EntityScan`/`EntityScanner`/`EntityScanPackages` 改为从 `spring-boot-autoconfigure` 中引入(包路径从 `o.s.b.persistence.autoconfigure` → `o.s.b.autoconfigure.domain`)
- **自动配置注册机制迁移**: `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`(SB 3.0+)→ `META-INF/spring.factories`(SB 2.7)
- **JUnit 版本降级**: 6.1.0 → 由 SB 2.7 BOM 管理(5.8.2),移除显式版本声明
- **关联依赖版本对齐**: WireMock、Testcontainers、Logback、Micrometer 等版本随 SB 2.7 BOM 调整
- **JPMS module-info.java 更新**: 移除 `requires spring.boot.persistence`,更新 Jackson module require 语句

## Capabilities

### New Capabilities

- `spring-boot-2x-autoconfiguration`: Spring Boot 2.7.x 自动配置支持,包括 SolverFactory/SolverManager Bean 自动装配、多求解器配置、基准测试自动配置
- `jackson2-integration`: 基于 Jackson 2 的 Score 序列化/反序列化、SolutionFileIO、TimefoldJacksonModule 注册

### Modified Capabilities

(无——当前不存在已有 specs,本次为全新定义)

## Impact

| 影响范围 | 说明 |
|----------|------|
| `build/build-parent/pom.xml` | 版本属性变更(SB、Spring Framework、Jackson、JUnit、Java 编译器版本等) |
| `spring-integration/pom.xml` | Jackson BOM 和 Spring Boot BOM 导入替换 |
| `spring-integration/spring-boot-autoconfigure/` | AOT 代码移除、EntityScan 包路径迁移、Jackson 2 适配、spring.factories 创建(约 8 个主代码文件 + 30+ 测试文件) |
| `spring-integration/spring-boot-starter/` | 移除 spring-boot-persistence 依赖、module-info 更新 |
| `spring-integration/spring-boot-integration-test/` | 移除 native profile 中的 process-aot goal |
| `persistence/jackson/` | 全部约 60 个 Java 文件:`tools.jackson.*` → `com.fasterxml.jackson.*` 导入替换、API 适配 |
| `core/` | 无改动(核心引擎独立于 Spring 版本) |
| `persistence/jaxb/`, `persistence/jpa/` | 无改动(jakarta.* 依赖独立管理,不受 Spring 版本影响) |
| `service/` | 无改动(基于 Quarkus/CDI,独立于 Spring 版本) |
| `quarkus-integration/` | 无改动 |
| `tools/` | 无改动 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## ADDED Requirements

### Requirement: Jackson 2 序列化支持

系统 SHALL 使用 Jackson 2 (`com.fasterxml.jackson`) API 提供 Score 类型的序列化和反序列化支持,而非 Jackson 3 (`tools.jackson`)。

#### Scenario: Score 对象序列化

- **WHEN** 通过 Jackson 2 `ObjectMapper` 将 `HardSoftScore` 对象序列化为 JSON
- **THEN** 输出格式为 `{"hardScore": <long>, "softScore": <long>}`,与当前格式一致

#### Scenario: Score 对象反序列化

- **WHEN** 通过 Jackson 2 `ObjectMapper` 将 JSON 字符串反序列化为 `HardSoftScore` 对象
- **THEN** 正确还原 Score 对象的 hardScore 和 softScore 值

### Requirement: Jackson Module 自动注册

系统 SHALL 提供 `TimefoldJacksonModule`(实现 `com.fasterxml.jackson.databind.Module`)用于注册所有 Timefold 自定义序列化器和反序列化器。

#### Scenario: Module 注册

- **WHEN** 用户调用 `TimefoldJacksonModule.createModule()` 并将返回的 Module 注册到 `ObjectMapper`
- **THEN** ObjectMapper 能够正确序列化和反序列化所有 Timefold Score 类型、ConstraintRef 类型、以及常用的约束流数据类型(Break、Sequence、SequenceChain、LoadBalance)

#### Scenario: Spring Boot 自动配置下自动注册

- **WHEN** Spring Boot 应用 classpath 上同时存在 `JsonMapper` 类和 Score 类
- **THEN** `TimefoldJacksonConfiguration` 自动创建一个 `JacksonModule` Bean 并注册到 Spring 的 Jackson 自动配置中

### Requirement: Score 全类型 Jackson 2 支持

系统 SHALL 支持以下 Score 类型在 Jackson 2 下的完整序列化/反序列化 round-trip:

- `SimpleScore`
- `SimpleBigDecimalScore`
- `HardSoftScore`
- `HardSoftBigDecimalScore`
- `HardMediumSoftScore`
- `HardMediumSoftBigDecimalScore`
- `BendableScore`
- `BendableBigDecimalScore`

#### Scenario: 每种 Score 类型的 round-trip

- **WHEN** 将每种 Score 类型的实例通过 Jackson 2 序列化再反序列化
- **THEN** 反序列化后的对象 `equals` 原始对象

### Requirement: SolutionFileIO 使用 Jackson 2

系统 SHALL 通过 `JacksonSolutionFileIO` 使用 Jackson 2 `ObjectMapper` 读写规划求解的输入输出文件。

#### Scenario: 读取 Solution JSON 文件

- **WHEN** 使用 `JacksonSolutionFileIO` 读取一个包含规划解决方案的 JSON 文件
- **THEN** 正确反序列化为 `@PlanningSolution` 注解的 Java 对象

#### Scenario: 写入 Solution JSON 文件

- **WHEN** 使用 `JacksonSolutionFileIO` 将规划解决方案写入 JSON 文件
- **THEN** 输出格式正确的 JSON 文件,可被同一 `JacksonSolutionFileIO` 再次读取

### Requirement: 泛型 Score 多态序列化

系统 SHALL 支持 `Score` 接口的泛型多态序列化/反序列化,允许字段声明为 `Score` 类型而运行时确定具体实现类。

#### Scenario: 多态反序列化

- **WHEN** JSON 中包含 `@class` 属性指定具体 Score 类型(如 `ai.timefold.solver.core.api.score.HardSoftScore`)
- **THEN** Jackson 2 正确反序列化为指定的具体 Score 实现类
Loading