From 707b4e5208e8ed5a55f4ea9adc58d5b9674ca6d4 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Thu, 15 Oct 2026 00:38:26 +0800 Subject: [PATCH] Add GraalVM Native Image Builds and Native Test CI for Spring Boot 3 and 4 Samples --- .github/workflows/ci.yml | 32 + CONTRIBUTING.md | 5 + docs/graalvm-native-image.md | 88 ++ docs/upstream-todo.md | 63 + pom.xml | 12 + spring-boot-3/pom.xml | 146 ++- spring-boot-3/springboot3-sample/pom.xml | 85 +- .../sample/boot3/SpringBoot3Application.java | 8 +- .../config/MyBatisNativeConfiguration.java | 88 +- .../config/ProjectRuntimeHintRegister.java | 31 - .../4.6.0/reachability-metadata.json | 15 + .../3.5.14/reachability-metadata.json | 14 + .../reachability-metadata.json | 13 + .../sample/boot3/test/UserServiceTest.java | 52 + .../add-remove-datasource-sample/pom.xml | 16 + spring-boot-4/beetlsql-sample/pom.xml | 16 + .../reachability-metadata.json | 22 + .../4.6.0/reachability-metadata.json | 15 + .../src/main/resources/application.yml | 2 +- spring-boot-4/config-consul-sample/pom.xml | 20 +- .../4.6.0/reachability-metadata.json | 15 + .../5.0.3/reachability-metadata.json | 15 + .../src/main/resources/application.yml | 23 +- .../src/main/resources/bootstrap.yml | 8 - .../samples/consul/test/ConsulSampleTest.java | 7 +- spring-boot-4/config-nacos-sample/pom.xml | 16 + .../4.6.0/reachability-metadata.json | 15 + .../src/main/resources/application.yml | 4 +- .../samples/nacos/test/NacosSampleTest.java | 65 +- spring-boot-4/druid-sample/pom.xml | 16 + .../samples/druid/DruidApplication.java | 8 +- .../config/MyBatisNativeConfiguration.java | 290 +++++ .../druid-sample/reachability-metadata.json | 22 + .../4.6.0/reachability-metadata.json | 15 + .../4.1.0/reachability-metadata.json | 14 + .../p6spy/3.9.1/reachability-metadata.json | 64 + spring-boot-4/jdbc-template-sample/pom.xml | 16 + .../4.6.0/reachability-metadata.json | 15 + .../reachability-metadata.json | 13 + .../load-datasource-from-jdbc-sample/pom.xml | 16 + spring-boot-4/mybatis-sample/pom.xml | 16 + .../samples/mybatis/MybatisApplication.java | 10 +- .../config/MyBatisNativeConfiguration.java | 290 +++++ .../4.6.0/reachability-metadata.json | 15 + .../mybatis-sample/reachability-metadata.json | 13 + .../4.1.0/reachability-metadata.json | 14 + .../samples/mybatis/test/UserServiceTest.java | 52 + spring-boot-4/mybatisplus3-sample/pom.xml | 16 + .../mybatisplus3/MybatisPlus3Application.java | 10 +- .../4.6.0/reachability-metadata.json | 15 + .../3.5.17/reachability-metadata.json | 14 + .../reachability-metadata.json | 13 + .../4.1.0/reachability-metadata.json | 14 + .../mybatis/3.5.19/reachability-metadata.json | 24 + spring-boot-4/name-pattern-sample/pom.xml | 16 + .../samples/pattern/PatternApplication.java | 10 +- .../config/MyBatisNativeConfiguration.java | 290 +++++ .../4.6.0/reachability-metadata.json | 15 + .../reachability-metadata.json | 13 + .../4.1.0/reachability-metadata.json | 14 + spring-boot-4/pom.xml | 164 +++ spring-boot-4/quartz-sample/pom.xml | 16 + .../quartz-sample/reachability-metadata.json | 14 + .../4.1.0/reachability-metadata.json | 14 + .../quartz/2.5.2/reachability-metadata.json | 13 + .../samples/quartz/test/QuartzSampleTest.java | 50 +- .../pom.xml | 34 + .../v5/core/ShardingSphereApplication.java | 10 +- .../config/MyBatisNativeConfiguration.java | 290 +++++ .../4.6.0/reachability-metadata.json | 15 + .../reachability-metadata.json | 22 + .../caffeine/3.2.4/reachability-metadata.json | 14 + .../9.7.0/reachability-metadata.json | 25 + .../5.5.3/reachability-metadata.json | 227 ++++ .../4.1.0/reachability-metadata.json | 14 + .../src/main/resources/application.yml | 4 +- .../src/main/resources/config.yaml | 24 +- .../core/AbstractNativeImageSupportTest.java | 123 ++ .../jdbc/v5/core/ControllerLayerTests.java | 2 +- .../jdbc/v5/core/MapperLayerTests.java | 2 +- .../src/test/resources/config-native.yaml | 62 + spring-boot-4/spel-sample/pom.xml | 16 + .../samples/spel/SpelApplication.java | 8 +- .../config/MyBatisNativeConfiguration.java | 290 +++++ .../4.6.0/reachability-metadata.json | 15 + .../spel-sample/reachability-metadata.json | 13 + .../4.1.0/reachability-metadata.json | 14 + .../baomidou/samples/spel/test/SPELTest.java | 3 +- spring-boot-4/tx-local-sample/pom.xml | 16 + .../samples/localtx/LocalTxApplication.java | 10 +- .../4.6.0/reachability-metadata.json | 15 + .../3.5.17/reachability-metadata.json | 14 + .../reachability-metadata.json | 13 + .../mybatis/3.5.19/reachability-metadata.json | 14 + spring-boot-4/tx-seata-sample/pom.xml | 16 + .../samples/seata/SeataApplication.java | 8 +- .../4.6.0/reachability-metadata.json | 15 + .../3.5.17/reachability-metadata.json | 14 + .../reachability-metadata.json | 13 + .../2.6.0/reachability-metadata.json | 1140 +++++++++++++++++ .../mybatis/3.5.19/reachability-metadata.json | 14 + .../src/main/resources/application.yml | 9 +- .../samples/seata/test/SeataSampleTest.java | 5 +- 103 files changed, 4833 insertions(+), 255 deletions(-) create mode 100644 docs/graalvm-native-image.md create mode 100644 docs/upstream-todo.md delete mode 100644 spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/config/ProjectRuntimeHintRegister.java create mode 100644 spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.14/reachability-metadata.json create mode 100644 spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/springboot3-sample/reachability-metadata.json create mode 100644 spring-boot-3/springboot3-sample/src/test/java/com/baomidou/sample/boot3/test/UserServiceTest.java create mode 100644 spring-boot-4/beetlsql-sample/src/main/resources/META-INF/native-image/com.baomidou/beetlsql-sample/reachability-metadata.json create mode 100644 spring-boot-4/beetlsql-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/config-consul-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/config-consul-sample/src/main/resources/META-INF/native-image/org.springframework.cloud/spring-cloud-consul-core/5.0.3/reachability-metadata.json delete mode 100644 spring-boot-4/config-consul-sample/src/main/resources/bootstrap.yml create mode 100644 spring-boot-4/config-nacos-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/druid-sample/src/main/java/com/baomidou/samples/druid/config/MyBatisNativeConfiguration.java create mode 100644 spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/com.baomidou/druid-sample/reachability-metadata.json create mode 100644 spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json create mode 100644 spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/p6spy/p6spy/3.9.1/reachability-metadata.json create mode 100644 spring-boot-4/jdbc-template-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/jdbc-template-sample/src/main/resources/META-INF/native-image/com.baomidou/jdbc-template-sample/reachability-metadata.json create mode 100644 spring-boot-4/mybatis-sample/src/main/java/com/baomidou/samples/mybatis/config/MyBatisNativeConfiguration.java create mode 100644 spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-sample/reachability-metadata.json create mode 100644 spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json create mode 100644 spring-boot-4/mybatis-sample/src/test/java/com/baomidou/samples/mybatis/test/UserServiceTest.java create mode 100644 spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json create mode 100644 spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatisplus3-sample/reachability-metadata.json create mode 100644 spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json create mode 100644 spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json create mode 100644 spring-boot-4/name-pattern-sample/src/main/java/com/baomidou/samples/pattern/config/MyBatisNativeConfiguration.java create mode 100644 spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/com.baomidou/name-pattern-sample/reachability-metadata.json create mode 100644 spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json create mode 100644 spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/com.baomidou/quartz-sample/reachability-metadata.json create mode 100644 spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json create mode 100644 spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/org.quartz-scheduler/quartz/2.5.2/reachability-metadata.json create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/config/MyBatisNativeConfiguration.java create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.baomidou/shardingsphere-jdbc-5.x-core-sample/reachability-metadata.json create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.github.ben-manes.caffeine/caffeine/3.2.4/reachability-metadata.json create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.mysql/mysql-connector-j/9.7.0/reachability-metadata.json create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/org.apache.shardingsphere/shardingsphere-jdbc/5.5.3/reachability-metadata.json create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/AbstractNativeImageSupportTest.java create mode 100644 spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/resources/config-native.yaml create mode 100644 spring-boot-4/spel-sample/src/main/java/com/baomidou/samples/spel/config/MyBatisNativeConfiguration.java create mode 100644 spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/com.baomidou/spel-sample/reachability-metadata.json create mode 100644 spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json create mode 100644 spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json create mode 100644 spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/tx-local-sample/reachability-metadata.json create mode 100644 spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json create mode 100644 spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json create mode 100644 spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json create mode 100644 spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/tx-seata-sample/reachability-metadata.json create mode 100644 spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/org.apache.seata/seata-all/2.6.0/reachability-metadata.json create mode 100644 spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8580aeeb..4a9daa39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,38 @@ jobs: - name: Build spring-boot-2 samples with Maven run: | ./mvnw -am -pl spring-boot-2/shardingsphere-jdbc-4.x-spring-sample,spring-boot-2/shardingsphere-jdbc-5.x-spring-sample -T1C -B -e clean test + test-native-ci: + if: github.repository == 'dynamic-datasource/dynamic-datasource-samples' + name: Native Test CI - GraalVM shard ${{ matrix.shard }} on ubuntu-latest + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7] + steps: + - uses: actions/checkout@v7 + - name: Setup GraalVM + uses: graalvm/setup-graalvm@v1 + with: + version: '25.3.4.1' + distribution: 'graalvm-community' + cache: 'maven' + # 各模块的 native / nativeTest 配置由 spring-boot-3 / spring-boot-4 父模块统一管理, + # 子模块声明插件即 opt-in。本地可用 `mvn -Pnative package` 构建 native image, + # 用 `mvn -PnativeTest test` 在 native image 中运行测试。CI 覆盖的模块清单按分片定义在 + # 根 pom.xml 的 `native.test.modules.shard1..7` 属性中,矩阵并行执行以缩短总时长; + # 测试镜像在建 native image 时启用 quick build(`quickBuild`),依赖 Testcontainers + # 的模块使用 runner 自带的 Docker。以下模块未纳入该任务: + # beetlsql-sample 与 druid-sample 依赖的第三方库暂未提供 native 支持; + # tx-seata-sample 依赖的 Seata 在 AOT 处理阶段就会构造 GlobalTransactionScanner + # 并要求 Seata Server 可达,且其应用上下文在 AOT 类型预判定阶段存在 null Advisor + # 问题,故暂不纳入。 + - name: Resolve native test modules + run: echo "NATIVE_TEST_MODULES=$(./mvnw -B -q help:evaluate -Dexpression=native.test.modules.shard${{ matrix.shard }} -DforceStdout)" >> "$GITHUB_ENV" + - name: Native test with Maven + run: | + ./mvnw -B -e -PnativeTest clean test -pl "$NATIVE_TEST_MODULES" lint-format-ci: if: github.repository == 'dynamic-datasource/dynamic-datasource-samples' name: Lint & Format CI diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11602f84..34edf902 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,6 +59,11 @@ cd ./dynamic-datasource-samples/ 因此 JDK 8 的 CI 任务直接对 `shardingsphere-jdbc-4.x-spring-sample` 与 `shardingsphere-jdbc-5.x-spring-sample` 执行 `clean test`; JDK 17/21/25/27 的全量 `clean test` 同样覆盖这两个模块。 +## GraalVM Native Image + +native image 的构建与测试、Native Test CI 分片、Reachability Metadata 写法与上游 issue 跟进见 +[docs/graalvm-native-image.md](./docs/graalvm-native-image.md)。 + ## 代码风格(Lint & Format) CI 的 `Lint & Format CI` 任务在 JDK 21 下执行格式与静态检查,本地提交前请在**仓库根目录**执行: diff --git a/docs/graalvm-native-image.md b/docs/graalvm-native-image.md new file mode 100644 index 00000000..8f008608 --- /dev/null +++ b/docs/graalvm-native-image.md @@ -0,0 +1,88 @@ +# GraalVM Native Image + +`spring-boot-3` 与 `spring-boot-4` 下所有模块都已配置 native image 支持:插件版本由根 +`pom.xml` 的 `native-maven-plugin.version` 统一指定,`native` / `nativeTest` profile +由版本线父模块统一管理(未继承 `spring-boot-starter-parent`,故显式定义,语义与官方一致), +子模块声明两个插件即 opt-in: + +```shell +# 默认打出可执行 jar(HotSpot 运行) +./mvnw -B -e package -pl spring-boot-4/jdbc-template-sample +# 构建 GraalVM native image(需要 GraalVM,Boot 3 需 22.3+,Boot 4 需 25+; +# 也可用 `spring-boot:build-image` 走 Paketo 构建容器镜像) +./mvnw -B -e -Pnative package -pl spring-boot-4/jdbc-template-sample +# 在 native image 中运行测试 +./mvnw -B -e -PnativeTest test -pl spring-boot-4/jdbc-template-sample +``` + +CI 的 `Native Test CI` 用 7 分片矩阵并行跑 `-PnativeTest test`(清单在根 `pom.xml` 的 +`native.test.modules.shard1..7`,分片内串行;单模块本地跑 +`mvn -PnativeTest clean test -pl `)。测试镜像开 quick build(`-Ob`,勿与 +`-H:+EnableFallbackCompilation` 同用)。未纳入的只有 `beetlsql-sample`(BeetlSQL 的 +AOT 值序列化不支持)与 `tx-seata-sample`(Seata AOT 期要求 Server 可达、且有关闭后 +依旧的 null Advisor 问题,见 apache/incubator-seata#8303)。 + +## Reachability Metadata 的写法 + +元数据一律以 JSON 写在模块的 `src/main/resources/META-INF/native-image/` 下,按 +`//` 分目录(样例自身目录不带版本号),不再使用 Java 的 +`RuntimeHintsRegistrar`;只有 AOT 扫描结果驱动的逻辑才留在 Java 中 +(如 MyBatis 模板的 `MyBatisBeanFactoryInitializationAotProcessor`): + +```text +src/main/resources/META-INF/native-image/ +├── com.baomidou// # 样例自身的资源与类(不带版本号) +├── com.baomidou/dynamic-datasource-creator/4.6.0/ # dynamic-datasource +├── com.baomidou/mybatis-plus/3.5.17/ # MyBatis-Plus +├── org.mybatis/mybatis/3.5.19/ # MyBatis +├── org.mybatis/mybatis-spring/4.1.0/ # MyBatis-Spring +└── com.mysql/mysql-connector-j/9.7.0/ # 其它依赖 +``` + +两条硬规则:每个条目都带 `condition.typeReached`,键序为 `condition` → `type`/`glob` → +`reason` → 其余属性;条件类必须在该元数据首次被使用之前完成初始化,否则条目会被静默丢弃 +(自条件对纯反射加载的类型永远不生效)。各目录的条件类见下表,改动后务必本地跑一遍 +对应模块的 `-PnativeTest`。 + +| 目录 | `condition.typeReached` | +| --- | --- | +| `com.baomidou/dynamic-datasource-creator` | `com.baomidou.dynamic.datasource.creator.DataSourceProperty` | +| `com.baomidou/mybatis-plus` | `com.baomidou.mybatisplus.core.MybatisConfiguration` | +| `org.mybatis/mybatis` | `org.apache.ibatis.session.Configuration` | +| `org.mybatis/mybatis-spring` | `org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration` | +| `com.mysql/mysql-connector-j` | `com.mysql.cj.jdbc.Driver` | +| `p6spy/p6spy` | `com.p6spy.engine.spy.P6ModuleManager` | +| `org.springframework.cloud/spring-cloud-consul-core` | `org.springframework.cloud.consul.config.ConsulPropertySources` | +| `org.quartz-scheduler/quartz` | `com.baomidou.dynamic.datasource.creator.DataSourceProperty` | +| `org.apache.shardingsphere/shardingsphere-jdbc` | `org.apache.shardingsphere.driver.ShardingSphereDriver` | +| `com.github.ben-manes.caffeine/caffeine` | `com.github.benmanes.caffeine.cache.Caffeine` | +| `org.apache.seata/seata-all` | `org.apache.seata.spring.annotation.GlobalTransactionScanner` | +| 样例自身(`db/**`、`config*.yaml`、`HelloworldJob` 等) | 触发读取的依赖入口类(`DataSourceProperty`、`DockerClientFactory`、`ShardingSphereDriver` 等) | + +`mvn -PnativeTest test` 变红时,按报错补条目即可(GraalVM 报错精确到类/字段);条件不生效 +会被静默丢弃,改条件必跑一遍 nativeTest。常见坑: + +- 按路径加载的资源(`db/schema.sql`、`config*.yaml`、`META-INF/services/**` 等)放 + `resources.glob`;`DatasourceInitProperties` 等嵌套绑定类型缺了会导致建表静默跳过; +- `@MapperScan` 显式声明 `sqlSessionFactoryRef`,否则 AOT 丢 `AUTOWIRE_BY_TYPE` 装配; +- `spring-cloud-starter-bootstrap` 与 AOT 不兼容,改用 `spring.config.import`; +- MyBatis mapper 代理与实体由官方 `MyBatisNativeConfiguration` 模板的 AOT 处理器注册, + bean 名勿与 MP 自带的 `MyBatisPlusNativeImageConfiguration` 重复; +- `MySQLContainer` 的挂载问题见 testcontainers-java#7954(子类跳过挂载或用 `GenericContainer`); +- ShardingSphere 用 `config-native.yaml`(算法与行表达式限制见官方文档),另需显式依赖 + `javax.transaction-api`;其自带元数据在 JDBC 用法下多不生效,以本仓库条目为准; +- JSON 只保留实测必需(agent 输出是超集,可删;与官方元数据重复的也删,如 caffeine 只留 + 官方缺的 `SIMS`;MP 3.5.14 的驱动条目不可删);flags 按实际访问收紧:实例化留 + `allDeclaredConstructors`,绑定/反序列化再加 `allDeclaredMethods`,MyBatis 的 `MetaObject` + 对(OGNL 调用)只要 `allDeclaredMethods`,caffeine 的 `SIMS` 用 `allDeclaredFields`, + `SqlSessionTemplate` 用 `allPublicMethods`,只做 `Class.forName` 的只登记类型; + `allPublicClasses`/`allDeclaredClasses` 不在 schema 里,勿用; +- `beetlsql-sample` 与 `tx-seata-sample` 都卡在 AOT 阶段,元数据尚未被 native 构建读取过: + seata 按 JVM 测试的 agent 实测裁剪,其余条目对齐其他模块的实测写法,待上游修复后用 + nativeTest 复核; +- 每个条目用 `reason`(字符串数组)记录用途,符合 reachability-metadata-schema-v1.2.0, + 当前 GraalVM 构建报 Unknown attribute 警告但忽略之、实测无影响,见 quarkusio/quarkus#49660。 + +## 上游 issue 跟进(TODO) + +依赖上游修复的待办统一记录在 [upstream-todo.md](./upstream-todo.md),新增条目请写在那里。 diff --git a/docs/upstream-todo.md b/docs/upstream-todo.md new file mode 100644 index 00000000..e5f7d289 --- /dev/null +++ b/docs/upstream-todo.md @@ -0,0 +1,63 @@ +# 上游 issue 跟进(TODO) + +依赖上游修复的待办。每条以 `TODO` 开头,写明上游 issue 与修复后本仓库要做的事;背景见 +[graalvm-native-image.md](./graalvm-native-image.md)。 + +- TODO https://github.com/apache/incubator-seata/issues/8303 :Seata 修复 AOT 问题后, + 把 `tx-seata-sample` 纳入 native CI,并用 nativeTest 复核按 JVM agent 裁剪的 + `seata-all` 条目(agent 另采到 111 个尚未登记的类,届时按报错补)。 +- TODO https://github.com/spring-projects/spring-framework/issues/37500 :Spring AOT + 保留 `autowireMode` 后,去掉各模块 `@MapperScan` 的 `sqlSessionFactoryRef` + (逐模块去属性并跑 nativeTest 验证)。 +- TODO https://github.com/baomidou/dynamic-datasource/issues/809 :DD 补上 + `@NestedConfigurationProperty` 或自带元数据后,删除各模块 + `dynamic-datasource-creator` 目录下的 `DatasourceInitProperties` 条目。 +- TODO https://github.com/baomidou/dynamic-datasource/issues/811 :DD 把 mybatis-spring + 改为 `compileOnly` 后,父 POM 对 mybatis-spring 的钉版本只剩统一 MP 模块(MP starter + 自带 4.0.0)的作用,届时再决定去留,改动后跑 JVM 与 nativeTest 验证。 +- TODO https://github.com/oracle/graal/issues/14708 :GraalVM parser 接受 `reason` + 字段、不再报 Unknown attribute 后,确认各模块构建日志无新增警告。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/542 :官方收录 + `seata-all` 后,对照精简 `tx-seata-sample` 的 `org.apache.seata/seata-all` 条目 + (验证需等 apache/incubator-seata#8303 修复、该模块能跑 nativeTest)。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/9432 :官方补测 + `mysql-connector-j` 8.0.31 元数据(覆盖到 9.7.0);关闭后升级 native-maven-plugin + (官方元数据随插件版本发布),若已含 `Driver` 与 `TimeZoneMapping.properties`,删除 + `shardingsphere-jdbc-5.x-core-sample` 的 `com.mysql/mysql-connector-j` 目录并跑 + nativeTest 验证。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/9437 :官方补测 + `mybatis-plus` 3.5.3 元数据(覆盖到 3.5.17,目前是空的 `{}`);关闭后同样升级插件, + 若已含 `MybatisXMLLanguageDriver` 与 `CompositeEnumTypeHandler`,删除各模块 + `com.baomidou/mybatis-plus` 目录并跑 nativeTest 验证。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/10906 :官方补上 + caffeine 的 `SIMS`(`maximumSize` + `softValues`)后升级插件,删除 + `shardingsphere-jdbc-5.x-core-sample` 的 `com.github.ben-manes.caffeine` 目录并跑 + nativeTest 验证。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/10907 :官方补上 + OGNL 用到的 `SystemMetaObject`/`MetaObject` 与 `EnumTypeHandler(Class)` 构造器后升级 + 插件,删除 `mybatisplus3-sample`、`tx-local-sample` 的 `org.mybatis/mybatis` 目录并跑 + nativeTest 验证(`tx-seata-sample` 的同类目录待 Seata 修复后再动)。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/10908 :官方让 + `tables_*.sql` 在 Spring Boot 建表初始化时已可用后升级插件,删除 `quartz-sample` 的 + `org.quartz-scheduler` 目录并跑 nativeTest 验证。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/10909 :官方收录 + `dynamic-datasource-creator` 4.6.0 后升级插件,删除各模块 + `com.baomidou/dynamic-datasource-creator` 目录并跑 nativeTest 验证(与上面 #809 + 哪个先落地都行)。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/10910 :官方收录 + `mybatis-spring` 4.1.0 后升级插件,删除各模块 `org.mybatis/mybatis-spring` 目录并跑 + nativeTest 验证。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/10911 :官方收录 + `spring-cloud-consul-core` 5.0.3 后升级插件,删除 `config-consul-sample` 的 + `org.springframework.cloud` 目录并跑 nativeTest 验证。 +- TODO https://github.com/oracle/graalvm-reachability-metadata/issues/10912 :官方收录 + `p6spy` 3.9.1 后升级插件,删除 `druid-sample` 的 `p6spy` 目录并跑 nativeTest 验证。 +- TODO ShardingSphere 5.5.3 自带元数据(`shardingsphere-infra-reachability-metadata`)的问题: + SnakeYAML 反射构造的 7 个 YAML 配置类用的是自条件、不生效,`YamlSingleRuleConfiguration` + 与 8 个 MySQL 解析器 SPI 类完全缺失;跟进地址 https://github.com/apache/shardingsphere/issues , + 目前暂未提 issue(提到 GRM 会让机器人为 65 个未收录的传递依赖各开一个 issue,故暂缓)。 + 修复后精简 `shardingsphere-jdbc-5.x-core-sample` 的 `org.apache.shardingsphere` 目录并跑 + nativeTest 验证;其中条件为 `YamlEngine` 等的 4 条可能已与自带元数据重复,届时一并验证。 +- TODO BeetlSQL 的 native 问题(`sqlManager` 的 `MySqlStyle` 不支持 AOT 值序列化), + 跟进地址 https://gitee.com/xiandafu/beetlsql/issues ,目前暂无已知 issue; + 问题修复后把 `beetlsql-sample` 纳入 native CI,并用 nativeTest 复核其元数据。 diff --git a/pom.xml b/pom.xml index 90d2b54f..5f8881f5 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,18 @@ 1.36.1 3.6.0 14.3.0 + + 1.1.14 + + spring-boot-4/add-remove-datasource-sample,spring-boot-4/config-nacos-sample + spring-boot-4/load-datasource-from-jdbc-sample,spring-boot-4/shardingsphere-jdbc-5.x-core-sample + spring-boot-4/name-pattern-sample,spring-boot-4/quartz-sample + spring-boot-4/config-consul-sample,spring-boot-4/spel-sample + spring-boot-4/mybatis-sample,spring-boot-4/tx-local-sample + spring-boot-3/springboot3-sample,spring-boot-4/mybatisplus3-sample + spring-boot-4/jdbc-template-sample,spring-boot-4/druid-sample diff --git a/spring-boot-3/pom.xml b/spring-boot-3/pom.xml index cb2152c6..93537f9c 100644 --- a/spring-boot-3/pom.xml +++ b/spring-boot-3/pom.xml @@ -21,7 +21,6 @@ 3.5.16 3.5.14 2.8.13 - 0.9.28 @@ -69,4 +68,149 @@ + + + + + + + org.graalvm.buildtools + native-maven-plugin + ${native-maven-plugin.version} + true + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot-dependencies.version} + + + repackage + + repackage + + + + + + + + + + + native + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot-dependencies.version} + + + paketobuildpacks/builder:tiny + + true + + + + + + process-aot + + process-aot + + + + + + org.graalvm.buildtools + native-maven-plugin + + ${project.build.outputDirectory} + 22.3 + + + + add-reachability-metadata + + add-reachability-metadata + + + + native-compile + package + + compile + + + + + + + + + + nativeTest + + + org.junit.platform + junit-platform-launcher + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + process-test-aot + + process-test-aot + + + + + + org.graalvm.buildtools + native-maven-plugin + + ${project.build.outputDirectory} + + true + 22.3 + + + + native-test + + test + + + + + + + + + diff --git a/spring-boot-3/springboot3-sample/pom.xml b/spring-boot-3/springboot3-sample/pom.xml index 050be50d..d9cfd714 100644 --- a/spring-boot-3/springboot3-sample/pom.xml +++ b/spring-boot-3/springboot3-sample/pom.xml @@ -53,31 +53,25 @@ lombok true + + org.springframework.boot + spring-boot-starter-test + test + + org.graalvm.buildtools native-maven-plugin - ${native-maven-plugin.version} - - - --enable-preview - - org.springframework.boot spring-boot-maven-plugin - ${spring-boot-dependencies.version} - - - - repackage - - - @@ -89,67 +83,4 @@ - - - - native - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - true - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot-dependencies.version} - - - paketobuildpacks/builder:tiny - - true - - - - - - process-aot - - process-aot - - - - - - org.graalvm.buildtools - native-maven-plugin - - ${project.build.outputDirectory} - - true - - 22.3 - - - - add-reachability-metadata - - add-reachability-metadata - - - - - - - - - diff --git a/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/SpringBoot3Application.java b/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/SpringBoot3Application.java index 544053a9..580c4481 100644 --- a/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/SpringBoot3Application.java +++ b/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/SpringBoot3Application.java @@ -20,9 +20,15 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * Spring Boot 3 版本样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @Slf4j @SpringBootApplication -@MapperScan("com.baomidou.sample.boot3.mapper") +@MapperScan(value = "com.baomidou.sample.boot3.mapper", sqlSessionFactoryRef = "sqlSessionFactory") public class SpringBoot3Application { public static void main(String[] args) { diff --git a/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/config/MyBatisNativeConfiguration.java b/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/config/MyBatisNativeConfiguration.java index b343d9d7..8257bcb0 100644 --- a/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/config/MyBatisNativeConfiguration.java +++ b/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/config/MyBatisNativeConfiguration.java @@ -19,12 +19,9 @@ import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.TreeSet; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -33,30 +30,11 @@ import org.apache.ibatis.annotations.InsertProvider; import org.apache.ibatis.annotations.SelectProvider; import org.apache.ibatis.annotations.UpdateProvider; -import org.apache.ibatis.cache.decorators.FifoCache; -import org.apache.ibatis.cache.decorators.LruCache; -import org.apache.ibatis.cache.decorators.SoftCache; -import org.apache.ibatis.cache.decorators.WeakCache; -import org.apache.ibatis.cache.impl.PerpetualCache; -import org.apache.ibatis.javassist.util.proxy.ProxyFactory; -import org.apache.ibatis.javassist.util.proxy.RuntimeSupport; -import org.apache.ibatis.logging.Log; -import org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl; -import org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl; -import org.apache.ibatis.logging.log4j2.Log4j2Impl; -import org.apache.ibatis.logging.nologging.NoLoggingImpl; -import org.apache.ibatis.logging.slf4j.Slf4jImpl; -import org.apache.ibatis.logging.stdout.StdOutImpl; import org.apache.ibatis.reflection.TypeParameterResolver; -import org.apache.ibatis.scripting.defaults.RawLanguageDriver; -import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; -import org.apache.ibatis.session.SqlSessionFactory; -import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.mapper.MapperFactoryBean; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.PropertyValue; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -66,18 +44,31 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.ResolvableType; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +/** + * MyBatis 的 Spring AOT 集成。 + * + *

取自 MyBatis 官方的 MyBatisNativeConfiguration 模板;静态的 GraalVM 元数据(日志实现、 缓存装饰器、dtd/xsd 资源等)已按依赖分目录改为 + * {@code META-INF/native-image////reachability-metadata.json}, + * 这里只保留无法用静态元数据表达的部分: + * + *

    + *
  • {@code MyBatisMapperFactoryBeanPostProcessor}:为 MapperFactoryBean 的 BeanDefinition + * 补齐类型信息,避免 AOT 代码生成退回到自动装配 Class; + *
  • {@code MyBatisBeanFactoryInitializationAotProcessor}:处理 mapper 扫描器生成的 MapperFactoryBean + * 定义,并为 mapper 接口及其实体注册反射(由 AOT 扫描结果驱动)。 + *
+ */ @Configuration(proxyBeanMethods = false) -@ImportRuntimeHints(MyBatisNativeConfiguration.MyBaitsRuntimeHintsRegistrar.class) public class MyBatisNativeConfiguration { @Bean @@ -90,39 +81,6 @@ MyBatisBeanFactoryInitializationAotProcessor myBatisBeanFactoryInitializationAot return new MyBatisBeanFactoryInitializationAotProcessor(); } - static class MyBaitsRuntimeHintsRegistrar implements RuntimeHintsRegistrar { - - @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - Stream.of( - RawLanguageDriver.class, - XMLLanguageDriver.class, - RuntimeSupport.class, - ProxyFactory.class, - Slf4jImpl.class, - Log.class, - JakartaCommonsLoggingImpl.class, - Log4j2Impl.class, - Jdk14LoggingImpl.class, - StdOutImpl.class, - NoLoggingImpl.class, - SqlSessionFactory.class, - PerpetualCache.class, - FifoCache.class, - LruCache.class, - SoftCache.class, - WeakCache.class, - SqlSessionFactoryBean.class, - ArrayList.class, - HashMap.class, - TreeSet.class, - HashSet.class) - .forEach(x -> hints.reflection().registerType(x, MemberCategory.values())); - Stream.of("org/apache/ibatis/builder/xml/*.dtd", "org/apache/ibatis/builder/xml/*.xsd") - .forEach(hints.resources()::registerPattern); - } - } - static class MyBatisBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter { @@ -229,9 +187,8 @@ private void registerReflectionTypeIfNecessary(Class type, RuntimeHints hints } static class MyBatisMapperTypeUtils { - private MyBatisMapperTypeUtils() { - // NOP - } + /** 工具类,禁止实例化。 */ + private MyBatisMapperTypeUtils() {} static Class resolveReturnClass(Class mapperInterface, Method method) { Type resolvedReturnType = @@ -296,6 +253,12 @@ public void postProcessMergedBeanDefinition( } } + /** + * 为 {@code MapperFactoryBean} 的 BeanDefinition 补齐类型信息。 + * + *

向上下文暴露泛型类型信息以避免过早初始化;同时把构造参数重新注册为 {@code Class} 字面量——扫描器只会记录 mapper 类名字符串,这会使 AOT + * 代码生成退回到 自动装配 {@code MapperFactoryBean} 构造器,native 测试下会尝试自动装配 {@code Class}。 + */ private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) { if (!beanDefinition.hasBeanClass() || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) { @@ -304,10 +267,13 @@ private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefi if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) { Class mapperInterface = getMapperInterface(beanDefinition); if (mapperInterface != null) { - // Exposes a generic type information to context for prevent early initializing beanDefinition.setTargetType( ResolvableType.forClassWithGenerics( beanDefinition.getBeanClass(), mapperInterface)); + ConstructorArgumentValues constructorArgumentValues = + new ConstructorArgumentValues(); + constructorArgumentValues.addGenericArgumentValue(mapperInterface); + beanDefinition.setConstructorArgumentValues(constructorArgumentValues); } } } diff --git a/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/config/ProjectRuntimeHintRegister.java b/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/config/ProjectRuntimeHintRegister.java deleted file mode 100644 index 7f641d1d..00000000 --- a/spring-boot-3/springboot3-sample/src/main/java/com/baomidou/sample/boot3/config/ProjectRuntimeHintRegister.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright © ${project.inceptionYear} organization baomidou - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.baomidou.sample.boot3.config; - -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportRuntimeHints; - -@Configuration(proxyBeanMethods = false) -@ImportRuntimeHints(ProjectRuntimeHintRegister.class) -public class ProjectRuntimeHintRegister implements RuntimeHintsRegistrar { - - @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - hints.resources().registerPattern("db/schema.sql"); - } -} diff --git a/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.14/reachability-metadata.json b/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.14/reachability-metadata.json new file mode 100644 index 00000000..b6e9fc43 --- /dev/null +++ b/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.14/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.mybatisplus.core.MybatisConfiguration" + }, + "type": "com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver", + "reason": [ + "MP 3.5.14 反射加载语言驱动,缺之报 Failed to load language driver" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/springboot3-sample/reachability-metadata.json b/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/springboot3-sample/reachability-metadata.json new file mode 100644 index 00000000..2321ae72 --- /dev/null +++ b/spring-boot-3/springboot3-sample/src/main/resources/META-INF/native-image/com.baomidou/springboot3-sample/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "db/schema.sql", + "reason": [ + "dynamic-datasource 初始化脚本,建表用" + ] + } + ] +} diff --git a/spring-boot-3/springboot3-sample/src/test/java/com/baomidou/sample/boot3/test/UserServiceTest.java b/spring-boot-3/springboot3-sample/src/test/java/com/baomidou/sample/boot3/test/UserServiceTest.java new file mode 100644 index 00000000..6668662b --- /dev/null +++ b/spring-boot-3/springboot3-sample/src/test/java/com/baomidou/sample/boot3/test/UserServiceTest.java @@ -0,0 +1,52 @@ +/* + * Copyright © ${project.inceptionYear} organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.sample.boot3.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.baomidou.sample.boot3.entity.User; +import com.baomidou.sample.boot3.service.UserService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserServiceTest { + + @Autowired private UserService userService; + + @AfterEach + void cleanUp() { + userService + .selectMasterUsers() + .forEach(user -> userService.deleteUserById(user.getId().longValue())); + } + + @Test + void crudAcrossMasterAndSlave() { + User user = new User(); + user.setName("Boot3"); + user.setAge(21); + userService.addUser(user); + + assertThat(userService.selectMasterUsers()).isNotEmpty(); + assertThat(userService.selectSlaveUsers()).isNotEmpty(); + + userService.deleteUserById(userService.selectMasterUsers().get(0).getId().longValue()); + assertThat(userService.selectMasterUsers()).isEmpty(); + } +} diff --git a/spring-boot-4/add-remove-datasource-sample/pom.xml b/spring-boot-4/add-remove-datasource-sample/pom.xml index fee6c2be..495d4e07 100644 --- a/spring-boot-4/add-remove-datasource-sample/pom.xml +++ b/spring-boot-4/add-remove-datasource-sample/pom.xml @@ -59,4 +59,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/beetlsql-sample/pom.xml b/spring-boot-4/beetlsql-sample/pom.xml index 24b32f87..e590cae6 100644 --- a/spring-boot-4/beetlsql-sample/pom.xml +++ b/spring-boot-4/beetlsql-sample/pom.xml @@ -47,4 +47,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/beetlsql-sample/src/main/resources/META-INF/native-image/com.baomidou/beetlsql-sample/reachability-metadata.json b/spring-boot-4/beetlsql-sample/src/main/resources/META-INF/native-image/com.baomidou/beetlsql-sample/reachability-metadata.json new file mode 100644 index 00000000..d58c1e16 --- /dev/null +++ b/spring-boot-4/beetlsql-sample/src/main/resources/META-INF/native-image/com.baomidou/beetlsql-sample/reachability-metadata.json @@ -0,0 +1,22 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "db/schema.sql", + "reason": [ + "dynamic-datasource 初始化脚本,建表用" + ] + }, + { + "condition": { + "typeReached": "org.beetl.sql.starter.BeetlSqlStater4" + }, + "glob": "btsql.properties", + "reason": [ + "BeetlSQL 配置文件,启动时读取" + ] + } + ] +} diff --git a/spring-boot-4/beetlsql-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/beetlsql-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/beetlsql-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/beetlsql-sample/src/main/resources/application.yml b/spring-boot-4/beetlsql-sample/src/main/resources/application.yml index f9448b3a..94a6aae7 100644 --- a/spring-boot-4/beetlsql-sample/src/main/resources/application.yml +++ b/spring-boot-4/beetlsql-sample/src/main/resources/application.yml @@ -27,4 +27,4 @@ beetlsql: sqlManager: ds: dataSource basePackage: com.baomidou.samples.beetl - daoSuffix: Dao \ No newline at end of file + daoSuffix: Dao diff --git a/spring-boot-4/config-consul-sample/pom.xml b/spring-boot-4/config-consul-sample/pom.xml index 20a5ff74..b3942010 100644 --- a/spring-boot-4/config-consul-sample/pom.xml +++ b/spring-boot-4/config-consul-sample/pom.xml @@ -20,10 +20,6 @@ org.springframework.cloud spring-cloud-starter-consul-config - - org.springframework.cloud - spring-cloud-starter-bootstrap - org.springdoc springdoc-openapi-starter-webmvc-ui @@ -62,4 +58,20 @@ + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/spring-boot-4/config-consul-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/config-consul-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/config-consul-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/config-consul-sample/src/main/resources/META-INF/native-image/org.springframework.cloud/spring-cloud-consul-core/5.0.3/reachability-metadata.json b/spring-boot-4/config-consul-sample/src/main/resources/META-INF/native-image/org.springframework.cloud/spring-cloud-consul-core/5.0.3/reachability-metadata.json new file mode 100644 index 00000000..1f6dd01a --- /dev/null +++ b/spring-boot-4/config-consul-sample/src/main/resources/META-INF/native-image/org.springframework.cloud/spring-cloud-consul-core/5.0.3/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.springframework.cloud.consul.config.ConsulPropertySources" + }, + "type": "org.springframework.cloud.consul.model.http.kv.GetValue", + "reason": [ + "Consul KV 反序列化,Jackson 构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/config-consul-sample/src/main/resources/application.yml b/spring-boot-4/config-consul-sample/src/main/resources/application.yml index e277c079..51f58b31 100644 --- a/spring-boot-4/config-consul-sample/src/main/resources/application.yml +++ b/spring-boot-4/config-consul-sample/src/main/resources/application.yml @@ -1,7 +1,16 @@ -#spring: -# cloud: -# consul: -# host: 110.40.253.205 -# config: -# format: yaml -# name: dynamic-datasource \ No newline at end of file +spring: + config: + # 用 Spring Cloud 官方的 config data import 读取 Consul KV(bootstrap 上下文与 AOT/native 不兼容)。 + # `optional:` 前缀的含义见官方文档 + # https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/#config-data-import : + # 去掉它则连不上 Consul 时启动(含 AOT 处理)直接失败;构建期无 server,必须保留。 + # 默认 context 为 spring.application.name(即 application),与 KV 路径 + # /{spring.cloud.consul.config.prefix}/{spring.application.name}/data 对应。 + import: "optional:consul:" + cloud: + consul: + host: localhost + port: 8500 + config: + prefix: dynamic-datasource + format: YAML diff --git a/spring-boot-4/config-consul-sample/src/main/resources/bootstrap.yml b/spring-boot-4/config-consul-sample/src/main/resources/bootstrap.yml deleted file mode 100644 index 9f6188a7..00000000 --- a/spring-boot-4/config-consul-sample/src/main/resources/bootstrap.yml +++ /dev/null @@ -1,8 +0,0 @@ -spring: - cloud: - consul: - host: ali - config: - prefix: dynamic-datasource - enabled: true - format: YAML diff --git a/spring-boot-4/config-consul-sample/src/test/java/com/baomidou/samples/consul/test/ConsulSampleTest.java b/spring-boot-4/config-consul-sample/src/test/java/com/baomidou/samples/consul/test/ConsulSampleTest.java index 560aa932..3f9efaff 100644 --- a/spring-boot-4/config-consul-sample/src/test/java/com/baomidou/samples/consul/test/ConsulSampleTest.java +++ b/spring-boot-4/config-consul-sample/src/test/java/com/baomidou/samples/consul/test/ConsulSampleTest.java @@ -41,6 +41,12 @@ class ConsulSampleTest { static ConsulContainer consul = new ConsulContainer(DockerImageName.parse("hashicorp/consul:2.0.4")); + /** + * 向 Consul KV 写入数据源配置,并把 Consul 地址写入系统属性。 + * + *

使用系统属性而不是 {@code @DynamicPropertySource},是为了让 config data import + * 在环境准备阶段(早于测试上下文注入)也能读到这些值。 + */ @BeforeAll static void setUp() throws Exception { String yaml = @@ -62,7 +68,6 @@ static void setUp() throws Exception { HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); assertThat(response.statusCode()).isEqualTo(200); - // 这里用系统属性而不用 @DynamicPropertySource,这样 Spring Cloud bootstrap 上下文也能读到这些值。 System.setProperty("spring.cloud.consul.host", consul.getHost()); System.setProperty("spring.cloud.consul.port", String.valueOf(consul.getMappedPort(8500))); } diff --git a/spring-boot-4/config-nacos-sample/pom.xml b/spring-boot-4/config-nacos-sample/pom.xml index 0d61a1f2..d0883c3f 100644 --- a/spring-boot-4/config-nacos-sample/pom.xml +++ b/spring-boot-4/config-nacos-sample/pom.xml @@ -58,4 +58,20 @@ + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/spring-boot-4/config-nacos-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/config-nacos-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/config-nacos-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/config-nacos-sample/src/main/resources/application.yml b/spring-boot-4/config-nacos-sample/src/main/resources/application.yml index b492c796..6a951106 100644 --- a/spring-boot-4/config-nacos-sample/src/main/resources/application.yml +++ b/spring-boot-4/config-nacos-sample/src/main/resources/application.yml @@ -3,8 +3,10 @@ spring: name: dynamic-datasource-nacos-sample # 按 https://nacos.io/docs/latest/ecology/use-nacos-with-spring-boot3/ 的官方用法, # 通过 spring.config.import 把 Nacos 上的配置作为属性源(无需 bootstrap 依赖)。 + # 注意这里刻意不用 `optional:` 前缀:Nacos 客户端连不上 server 时只打日志重试, + # 不会让 AOT 处理失败(已实测去掉前缀仍全绿),保持与官方文档一致的写法即可。 config: - import: nacos:dynamic-datasource.yaml?group=DEFAULT_GROUP + import: "nacos:dynamic-datasource.yaml?group=DEFAULT_GROUP" nacos: config: server-addr: 127.0.0.1:8848 diff --git a/spring-boot-4/config-nacos-sample/src/test/java/com/baomidou/samples/nacos/test/NacosSampleTest.java b/spring-boot-4/config-nacos-sample/src/test/java/com/baomidou/samples/nacos/test/NacosSampleTest.java index 356b4392..2ed3c427 100644 --- a/spring-boot-4/config-nacos-sample/src/test/java/com/baomidou/samples/nacos/test/NacosSampleTest.java +++ b/spring-boot-4/config-nacos-sample/src/test/java/com/baomidou/samples/nacos/test/NacosSampleTest.java @@ -25,6 +25,7 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.List; import javax.sql.DataSource; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -37,48 +38,55 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; +/** + * Nacos 配置中心样例的测试。 + * + *

按 Nacos 官方文档的用法,Spring Boot 4 通过 {@code com.alibaba.cloud:spring-alibaba-nacos-config} 与 + * {@code spring.config.import} 接入,测试服务端使用同代的 Nacos 3.x。Nacos 3.x 的 HTTP 客户端 API 只读不写,发布配置需要走运维 + * API,且运维 API 自带独立鉴权开关,测试里一并关闭。 + */ @Testcontainers @SpringBootTest class NacosSampleTest { - // 按 https://nacos.io/docs/latest/ecology/use-nacos-with-spring-boot3/ 的官方用法, - // Spring Boot 4 通过 com.alibaba.cloud:spring-alibaba-nacos-config + spring.config.import 接入, - // 测试服务端用同代的 Nacos 3.x。Nacos 3.x 的 HTTP 客户端 API 只读不写, - // 发布配置要走运维 API,且运维 API 自带独立鉴权开关,测试里一并关闭。 - @Container - static GenericContainer nacos = - new GenericContainer<>(DockerImageName.parse("nacos/nacos-server:v3.2.4")) - .withEnv("MODE", "standalone") - .withEnv("NACOS_AUTH_ENABLE", "false") - .withEnv("NACOS_AUTH_ADMIN_ENABLE", "false") - .withEnv("NACOS_AUTH_CONSOLE_ENABLE", "false") - .withEnv("NACOS_AUTH_TOKEN", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=") - .withEnv("NACOS_AUTH_IDENTITY_KEY", "serverIdentity") - .withEnv("NACOS_AUTH_IDENTITY_VALUE", "security") - .withExposedPorts(8848, 9848, 9849) - .waitingFor( - Wait.forLogMessage(".*Nacos Server API started successfully.*", 1) - .withStartupTimeout(Duration.ofMinutes(5))); + @Container static GenericContainer nacos = createNacosContainer(); + + /** + * 创建 Nacos 服务端容器,并把 8848/9848/9849 固定发布到本机同名端口。 + * + *

native image 会在构建期把 {@code NacosConfigDataResource}(含配置文件中默认的 127.0.0.1:8848 与默认 gRPC 偏移量 + * 1000)固化进镜像,运行期的配置拉取仍使用这些值,因此需要让构建期固化的地址在运行期可达。 + */ + private static GenericContainer createNacosContainer() { + GenericContainer container = + new GenericContainer<>(DockerImageName.parse("nacos/nacos-server:v3.2.4")) + .withEnv("MODE", "standalone") + .withEnv("NACOS_AUTH_ENABLE", "false") + .withEnv("NACOS_AUTH_ADMIN_ENABLE", "false") + .withEnv("NACOS_AUTH_CONSOLE_ENABLE", "false") + .withEnv("NACOS_AUTH_TOKEN", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=") + .withEnv("NACOS_AUTH_IDENTITY_KEY", "serverIdentity") + .withEnv("NACOS_AUTH_IDENTITY_VALUE", "security") + .waitingFor( + Wait.forLogMessage(".*Nacos Server API started successfully.*", 1) + .withStartupTimeout(Duration.ofMinutes(5))); + container.setPortBindings(List.of("8848:8848", "9848:9848", "9849:9849")); + return container; + } + /** 把 Nacos 服务地址写入系统属性:{@code spring.config.import} 在环境准备的最早期求值, 地址只能用系统属性覆盖。 */ @BeforeAll static void setUp() throws Exception { - // spring.config.import 在环境准备的最早期求值,地址只能用系统属性覆盖。 - System.setProperty( - "spring.nacos.config.server-addr", - nacos.getHost() + ":" + nacos.getMappedPort(8848)); - // 3.x 客户端按(http 端口 + 偏移量)计算服务端的 gRPC 端口; - // testcontainers 的随机端口映射下默认的 1000 偏移不再成立,因此按实际映射端口动态计算偏移。 - int offset = nacos.getMappedPort(9848) - nacos.getMappedPort(8848); - System.setProperty("nacos.server.grpc.port.offset", String.valueOf(offset)); + System.setProperty("spring.nacos.config.server-addr", "127.0.0.1:8848"); publishConfig(); } @AfterAll static void tearDown() { System.clearProperty("spring.nacos.config.server-addr"); - System.clearProperty("nacos.server.grpc.port.offset"); } + /** 通过运维 API 发布数据源配置,并等待配置在服务端可读后再启动 Spring 上下文 (服务端内部同步需要一点时间)。 */ static void publishConfig() throws Exception { String yaml = "spring:\n" @@ -90,7 +98,7 @@ static void publishConfig() throws Exception { + " url: jdbc:h2:mem:nacos_master\n" + " username: sa\n" + " password: \"\"\n"; - String baseUrl = "http://" + nacos.getHost() + ":" + nacos.getMappedPort(8848); + String baseUrl = "http://127.0.0.1:8848"; String body = "dataId=dynamic-datasource.yaml&groupName=DEFAULT_GROUP&type=yaml&content=" + URLEncoder.encode(yaml, StandardCharsets.UTF_8); @@ -103,7 +111,6 @@ static void publishConfig() throws Exception { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertThat(response.statusCode()).isEqualTo(200); assertThat(response.body()).contains("\"data\":true"); - // 发布后服务端内部同步需要一点时间,先等到配置可读再启动 Spring 上下文。 HttpRequest query = HttpRequest.newBuilder( URI.create( diff --git a/spring-boot-4/druid-sample/pom.xml b/spring-boot-4/druid-sample/pom.xml index 119103f2..ed445b9c 100644 --- a/spring-boot-4/druid-sample/pom.xml +++ b/spring-boot-4/druid-sample/pom.xml @@ -55,4 +55,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/druid-sample/src/main/java/com/baomidou/samples/druid/DruidApplication.java b/spring-boot-4/druid-sample/src/main/java/com/baomidou/samples/druid/DruidApplication.java index 7f7ae954..53ea5152 100644 --- a/spring-boot-4/druid-sample/src/main/java/com/baomidou/samples/druid/DruidApplication.java +++ b/spring-boot-4/druid-sample/src/main/java/com/baomidou/samples/druid/DruidApplication.java @@ -20,9 +20,15 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * Druid 数据源样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @Slf4j @SpringBootApplication -@MapperScan("com.baomidou.samples.druid.mapper") +@MapperScan(value = "com.baomidou.samples.druid.mapper", sqlSessionFactoryRef = "sqlSessionFactory") public class DruidApplication { public static void main(String[] args) { diff --git a/spring-boot-4/druid-sample/src/main/java/com/baomidou/samples/druid/config/MyBatisNativeConfiguration.java b/spring-boot-4/druid-sample/src/main/java/com/baomidou/samples/druid/config/MyBatisNativeConfiguration.java new file mode 100644 index 00000000..0b5d43b5 --- /dev/null +++ b/spring-boot-4/druid-sample/src/main/java/com/baomidou/samples/druid/config/MyBatisNativeConfiguration.java @@ -0,0 +1,290 @@ +/* + * Copyright © ${project.inceptionYear} organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.samples.druid.config; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.annotations.DeleteProvider; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.annotations.SelectProvider; +import org.apache.ibatis.annotations.UpdateProvider; +import org.apache.ibatis.reflection.TypeParameterResolver; +import org.mybatis.spring.mapper.MapperFactoryBean; +import org.mybatis.spring.mapper.MapperScannerConfigurer; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * MyBatis 的 Spring AOT 集成。 + * + *

取自 MyBatis 官方的 MyBatisNativeConfiguration 模板;静态的 GraalVM 元数据(日志实现、 缓存装饰器、dtd/xsd 资源等)已按依赖分目录改为 + * {@code META-INF/native-image////reachability-metadata.json}, + * 这里只保留无法用静态元数据表达的部分: + * + *

    + *
  • {@code MyBatisMapperFactoryBeanPostProcessor}:为 MapperFactoryBean 的 BeanDefinition + * 补齐类型信息,避免 AOT 代码生成退回到自动装配 Class; + *
  • {@code MyBatisBeanFactoryInitializationAotProcessor}:处理 mapper 扫描器生成的 MapperFactoryBean + * 定义,并为 mapper 接口及其实体注册反射(由 AOT 扫描结果驱动)。 + *
+ */ +@Configuration(proxyBeanMethods = false) +public class MyBatisNativeConfiguration { + + @Bean + static MyBatisMapperFactoryBeanPostProcessor myBatisMapperFactoryBeanPostProcessor() { + return new MyBatisMapperFactoryBeanPostProcessor(); + } + + @Bean + MyBatisBeanFactoryInitializationAotProcessor myBatisBeanFactoryInitializationAotProcessor() { + return new MyBatisBeanFactoryInitializationAotProcessor(); + } + + static class MyBatisBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter { + + private final Set> excludeClasses = new HashSet<>(); + + MyBatisBeanFactoryInitializationAotProcessor() { + excludeClasses.add(MapperScannerConfigurer.class); + } + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return excludeClasses.contains(registeredBean.getBeanClass()); + } + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + String[] beanNames = beanFactory.getBeanNamesForType(MapperFactoryBean.class); + if (beanNames.length == 0) { + return null; + } + return (context, code) -> { + RuntimeHints hints = context.getRuntimeHints(); + for (String beanName : beanNames) { + BeanDefinition beanDefinition = + beanFactory.getBeanDefinition(beanName.substring(1)); + PropertyValue mapperInterface = + beanDefinition.getPropertyValues().getPropertyValue("mapperInterface"); + if (mapperInterface != null && mapperInterface.getValue() != null) { + Class mapperInterfaceType = (Class) mapperInterface.getValue(); + if (mapperInterfaceType != null) { + registerReflectionTypeIfNecessary(mapperInterfaceType, hints); + hints.proxies().registerJdkProxy(mapperInterfaceType); + hints.resources() + .registerPattern( + mapperInterfaceType + .getName() + .replace('.', '/') + .concat(".xml")); + registerMapperRelationships(mapperInterfaceType, hints); + } + } + } + }; + } + + private void registerMapperRelationships(Class mapperInterfaceType, RuntimeHints hints) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(mapperInterfaceType); + for (Method method : methods) { + if (method.getDeclaringClass() != Object.class) { + ReflectionUtils.makeAccessible(method); + registerSqlProviderTypes( + method, + hints, + SelectProvider.class, + SelectProvider::value, + SelectProvider::type); + registerSqlProviderTypes( + method, + hints, + InsertProvider.class, + InsertProvider::value, + InsertProvider::type); + registerSqlProviderTypes( + method, + hints, + UpdateProvider.class, + UpdateProvider::value, + UpdateProvider::type); + registerSqlProviderTypes( + method, + hints, + DeleteProvider.class, + DeleteProvider::value, + DeleteProvider::type); + Class returnType = + MyBatisMapperTypeUtils.resolveReturnClass(mapperInterfaceType, method); + registerReflectionTypeIfNecessary(returnType, hints); + MyBatisMapperTypeUtils.resolveParameterClasses(mapperInterfaceType, method) + .forEach(x -> registerReflectionTypeIfNecessary(x, hints)); + } + } + } + + @SafeVarargs + private void registerSqlProviderTypes( + Method method, + RuntimeHints hints, + Class annotationType, + Function>... providerTypeResolvers) { + for (T annotation : method.getAnnotationsByType(annotationType)) { + for (Function> providerTypeResolver : providerTypeResolvers) { + registerReflectionTypeIfNecessary( + providerTypeResolver.apply(annotation), hints); + } + } + } + + private void registerReflectionTypeIfNecessary(Class type, RuntimeHints hints) { + if (!type.isPrimitive() && !type.getName().startsWith("java")) { + hints.reflection().registerType(type, MemberCategory.values()); + } + } + } + + static class MyBatisMapperTypeUtils { + /** 工具类,禁止实例化。 */ + private MyBatisMapperTypeUtils() {} + + static Class resolveReturnClass(Class mapperInterface, Method method) { + Type resolvedReturnType = + TypeParameterResolver.resolveReturnType(method, mapperInterface); + return typeToClass(resolvedReturnType, method.getReturnType()); + } + + static Set> resolveParameterClasses(Class mapperInterface, Method method) { + return Stream.of(TypeParameterResolver.resolveParamTypes(method, mapperInterface)) + .map(x -> typeToClass(x, x instanceof Class ? (Class) x : Object.class)) + .collect(Collectors.toSet()); + } + + private static Class typeToClass(Type src, Class fallback) { + Class result = null; + if (src instanceof Class) { + if (((Class) src).isArray()) { + result = ((Class) src).getComponentType(); + } else { + result = (Class) src; + } + } else if (src instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) src; + int index = + (parameterizedType.getRawType() instanceof Class + && Map.class.isAssignableFrom( + (Class) parameterizedType.getRawType()) + && parameterizedType.getActualTypeArguments().length > 1) + ? 1 + : 0; + Type actualType = parameterizedType.getActualTypeArguments()[index]; + result = typeToClass(actualType, fallback); + } + if (result == null) { + result = fallback; + } + return result; + } + } + + static class MyBatisMapperFactoryBeanPostProcessor + implements MergedBeanDefinitionPostProcessor, BeanFactoryAware { + + private static final org.apache.commons.logging.Log LOG = + LogFactory.getLog(MyBatisMapperFactoryBeanPostProcessor.class); + + private static final String MAPPER_FACTORY_BEAN = + "org.mybatis.spring.mapper.MapperFactoryBean"; + + private ConfigurableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + + @Override + public void postProcessMergedBeanDefinition( + RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (ClassUtils.isPresent(MAPPER_FACTORY_BEAN, this.beanFactory.getBeanClassLoader())) { + resolveMapperFactoryBeanTypeIfNecessary(beanDefinition); + } + } + + /** + * 为 {@code MapperFactoryBean} 的 BeanDefinition 补齐类型信息。 + * + *

向上下文暴露泛型类型信息以避免过早初始化;同时把构造参数重新注册为 {@code Class} 字面量——扫描器只会记录 mapper 类名字符串,这会使 AOT + * 代码生成退回到 自动装配 {@code MapperFactoryBean} 构造器,native 测试下会尝试自动装配 {@code Class}。 + */ + private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) { + if (!beanDefinition.hasBeanClass() + || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) { + return; + } + if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) { + Class mapperInterface = getMapperInterface(beanDefinition); + if (mapperInterface != null) { + beanDefinition.setTargetType( + ResolvableType.forClassWithGenerics( + beanDefinition.getBeanClass(), mapperInterface)); + ConstructorArgumentValues constructorArgumentValues = + new ConstructorArgumentValues(); + constructorArgumentValues.addGenericArgumentValue(mapperInterface); + beanDefinition.setConstructorArgumentValues(constructorArgumentValues); + } + } + } + + private Class getMapperInterface(RootBeanDefinition beanDefinition) { + try { + return (Class) beanDefinition.getPropertyValues().get("mapperInterface"); + } catch (Exception e) { + LOG.debug("Fail getting mapper interface type.", e); + return null; + } + } + } +} diff --git a/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/com.baomidou/druid-sample/reachability-metadata.json b/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/com.baomidou/druid-sample/reachability-metadata.json new file mode 100644 index 00000000..7f15d63b --- /dev/null +++ b/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/com.baomidou/druid-sample/reachability-metadata.json @@ -0,0 +1,22 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "db/schema.sql", + "reason": [ + "dynamic-datasource 初始化脚本,建表用" + ] + }, + { + "condition": { + "typeReached": "com.p6spy.engine.spy.P6ModuleManager" + }, + "glob": "spy.properties", + "reason": [ + "P6Spy 配置文件,无它则回退默认 FileLogger 输出 spy.log" + ] + } + ] +} diff --git a/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json b/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json new file mode 100644 index 00000000..a330c674 --- /dev/null +++ b/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration" + }, + "type": "org.mybatis.spring.SqlSessionFactoryBean", + "reason": [ + "mybatis-spring 工厂 Bean,反射调用无参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/p6spy/p6spy/3.9.1/reachability-metadata.json b/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/p6spy/p6spy/3.9.1/reachability-metadata.json new file mode 100644 index 00000000..e45a2ab7 --- /dev/null +++ b/spring-boot-4/druid-sample/src/main/resources/META-INF/native-image/p6spy/p6spy/3.9.1/reachability-metadata.json @@ -0,0 +1,64 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.p6spy.engine.spy.P6ModuleManager" + }, + "type": "com.p6spy.engine.spy.appender.SingleLineFormat", + "reason": [ + "P6Spy 默认消息格式类,按类名加载,见 p6spy/p6spy#568" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "com.p6spy.engine.spy.P6ModuleManager" + }, + "type": "com.p6spy.engine.spy.appender.FileLogger", + "reason": [ + "P6Spy 默认 appender,按类名加载" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "com.p6spy.engine.spy.P6ModuleManager" + }, + "type": "com.p6spy.engine.spy.appender.Slf4JLogger", + "reason": [ + "本模块 spy.properties 配置的 appender,按类名加载" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "com.p6spy.engine.spy.P6ModuleManager" + }, + "type": "com.p6spy.engine.spy.P6SpyFactory", + "reason": [ + "P6Spy 模块工厂,按类名加载" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "com.p6spy.engine.spy.P6ModuleManager" + }, + "type": "com.p6spy.engine.logging.P6LogFactory", + "reason": [ + "P6Spy 默认 modulelist 成员,按类名加载" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "com.p6spy.engine.spy.P6ModuleManager" + }, + "type": "com.p6spy.engine.logging.format.HexEncodedBinaryFormat", + "reason": [ + "P6Spy 默认二进制格式类,按类名加载" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/jdbc-template-sample/pom.xml b/spring-boot-4/jdbc-template-sample/pom.xml index 5e229302..4cfb2861 100644 --- a/spring-boot-4/jdbc-template-sample/pom.xml +++ b/spring-boot-4/jdbc-template-sample/pom.xml @@ -47,4 +47,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/jdbc-template-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/jdbc-template-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/jdbc-template-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/jdbc-template-sample/src/main/resources/META-INF/native-image/com.baomidou/jdbc-template-sample/reachability-metadata.json b/spring-boot-4/jdbc-template-sample/src/main/resources/META-INF/native-image/com.baomidou/jdbc-template-sample/reachability-metadata.json new file mode 100644 index 00000000..2321ae72 --- /dev/null +++ b/spring-boot-4/jdbc-template-sample/src/main/resources/META-INF/native-image/com.baomidou/jdbc-template-sample/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "db/schema.sql", + "reason": [ + "dynamic-datasource 初始化脚本,建表用" + ] + } + ] +} diff --git a/spring-boot-4/load-datasource-from-jdbc-sample/pom.xml b/spring-boot-4/load-datasource-from-jdbc-sample/pom.xml index 1e39004f..b40f9837 100644 --- a/spring-boot-4/load-datasource-from-jdbc-sample/pom.xml +++ b/spring-boot-4/load-datasource-from-jdbc-sample/pom.xml @@ -51,4 +51,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/mybatis-sample/pom.xml b/spring-boot-4/mybatis-sample/pom.xml index 7546fba2..7e6e76ad 100644 --- a/spring-boot-4/mybatis-sample/pom.xml +++ b/spring-boot-4/mybatis-sample/pom.xml @@ -51,4 +51,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/mybatis-sample/src/main/java/com/baomidou/samples/mybatis/MybatisApplication.java b/spring-boot-4/mybatis-sample/src/main/java/com/baomidou/samples/mybatis/MybatisApplication.java index 6efb790f..e851fd14 100644 --- a/spring-boot-4/mybatis-sample/src/main/java/com/baomidou/samples/mybatis/MybatisApplication.java +++ b/spring-boot-4/mybatis-sample/src/main/java/com/baomidou/samples/mybatis/MybatisApplication.java @@ -20,9 +20,17 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * MyBatis 样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @Slf4j @SpringBootApplication -@MapperScan("com.baomidou.samples.mybatis.mapper") +@MapperScan( + value = "com.baomidou.samples.mybatis.mapper", + sqlSessionFactoryRef = "sqlSessionFactory") public class MybatisApplication { public static void main(String[] args) { diff --git a/spring-boot-4/mybatis-sample/src/main/java/com/baomidou/samples/mybatis/config/MyBatisNativeConfiguration.java b/spring-boot-4/mybatis-sample/src/main/java/com/baomidou/samples/mybatis/config/MyBatisNativeConfiguration.java new file mode 100644 index 00000000..97ea16e4 --- /dev/null +++ b/spring-boot-4/mybatis-sample/src/main/java/com/baomidou/samples/mybatis/config/MyBatisNativeConfiguration.java @@ -0,0 +1,290 @@ +/* + * Copyright © ${project.inceptionYear} organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.samples.mybatis.config; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.annotations.DeleteProvider; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.annotations.SelectProvider; +import org.apache.ibatis.annotations.UpdateProvider; +import org.apache.ibatis.reflection.TypeParameterResolver; +import org.mybatis.spring.mapper.MapperFactoryBean; +import org.mybatis.spring.mapper.MapperScannerConfigurer; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * MyBatis 的 Spring AOT 集成。 + * + *

取自 MyBatis 官方的 MyBatisNativeConfiguration 模板;静态的 GraalVM 元数据(日志实现、 缓存装饰器、dtd/xsd 资源等)已按依赖分目录改为 + * {@code META-INF/native-image////reachability-metadata.json}, + * 这里只保留无法用静态元数据表达的部分: + * + *

    + *
  • {@code MyBatisMapperFactoryBeanPostProcessor}:为 MapperFactoryBean 的 BeanDefinition + * 补齐类型信息,避免 AOT 代码生成退回到自动装配 Class; + *
  • {@code MyBatisBeanFactoryInitializationAotProcessor}:处理 mapper 扫描器生成的 MapperFactoryBean + * 定义,并为 mapper 接口及其实体注册反射(由 AOT 扫描结果驱动)。 + *
+ */ +@Configuration(proxyBeanMethods = false) +public class MyBatisNativeConfiguration { + + @Bean + static MyBatisMapperFactoryBeanPostProcessor myBatisMapperFactoryBeanPostProcessor() { + return new MyBatisMapperFactoryBeanPostProcessor(); + } + + @Bean + MyBatisBeanFactoryInitializationAotProcessor myBatisBeanFactoryInitializationAotProcessor() { + return new MyBatisBeanFactoryInitializationAotProcessor(); + } + + static class MyBatisBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter { + + private final Set> excludeClasses = new HashSet<>(); + + MyBatisBeanFactoryInitializationAotProcessor() { + excludeClasses.add(MapperScannerConfigurer.class); + } + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return excludeClasses.contains(registeredBean.getBeanClass()); + } + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + String[] beanNames = beanFactory.getBeanNamesForType(MapperFactoryBean.class); + if (beanNames.length == 0) { + return null; + } + return (context, code) -> { + RuntimeHints hints = context.getRuntimeHints(); + for (String beanName : beanNames) { + BeanDefinition beanDefinition = + beanFactory.getBeanDefinition(beanName.substring(1)); + PropertyValue mapperInterface = + beanDefinition.getPropertyValues().getPropertyValue("mapperInterface"); + if (mapperInterface != null && mapperInterface.getValue() != null) { + Class mapperInterfaceType = (Class) mapperInterface.getValue(); + if (mapperInterfaceType != null) { + registerReflectionTypeIfNecessary(mapperInterfaceType, hints); + hints.proxies().registerJdkProxy(mapperInterfaceType); + hints.resources() + .registerPattern( + mapperInterfaceType + .getName() + .replace('.', '/') + .concat(".xml")); + registerMapperRelationships(mapperInterfaceType, hints); + } + } + } + }; + } + + private void registerMapperRelationships(Class mapperInterfaceType, RuntimeHints hints) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(mapperInterfaceType); + for (Method method : methods) { + if (method.getDeclaringClass() != Object.class) { + ReflectionUtils.makeAccessible(method); + registerSqlProviderTypes( + method, + hints, + SelectProvider.class, + SelectProvider::value, + SelectProvider::type); + registerSqlProviderTypes( + method, + hints, + InsertProvider.class, + InsertProvider::value, + InsertProvider::type); + registerSqlProviderTypes( + method, + hints, + UpdateProvider.class, + UpdateProvider::value, + UpdateProvider::type); + registerSqlProviderTypes( + method, + hints, + DeleteProvider.class, + DeleteProvider::value, + DeleteProvider::type); + Class returnType = + MyBatisMapperTypeUtils.resolveReturnClass(mapperInterfaceType, method); + registerReflectionTypeIfNecessary(returnType, hints); + MyBatisMapperTypeUtils.resolveParameterClasses(mapperInterfaceType, method) + .forEach(x -> registerReflectionTypeIfNecessary(x, hints)); + } + } + } + + @SafeVarargs + private void registerSqlProviderTypes( + Method method, + RuntimeHints hints, + Class annotationType, + Function>... providerTypeResolvers) { + for (T annotation : method.getAnnotationsByType(annotationType)) { + for (Function> providerTypeResolver : providerTypeResolvers) { + registerReflectionTypeIfNecessary( + providerTypeResolver.apply(annotation), hints); + } + } + } + + private void registerReflectionTypeIfNecessary(Class type, RuntimeHints hints) { + if (!type.isPrimitive() && !type.getName().startsWith("java")) { + hints.reflection().registerType(type, MemberCategory.values()); + } + } + } + + static class MyBatisMapperTypeUtils { + /** 工具类,禁止实例化。 */ + private MyBatisMapperTypeUtils() {} + + static Class resolveReturnClass(Class mapperInterface, Method method) { + Type resolvedReturnType = + TypeParameterResolver.resolveReturnType(method, mapperInterface); + return typeToClass(resolvedReturnType, method.getReturnType()); + } + + static Set> resolveParameterClasses(Class mapperInterface, Method method) { + return Stream.of(TypeParameterResolver.resolveParamTypes(method, mapperInterface)) + .map(x -> typeToClass(x, x instanceof Class ? (Class) x : Object.class)) + .collect(Collectors.toSet()); + } + + private static Class typeToClass(Type src, Class fallback) { + Class result = null; + if (src instanceof Class) { + if (((Class) src).isArray()) { + result = ((Class) src).getComponentType(); + } else { + result = (Class) src; + } + } else if (src instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) src; + int index = + (parameterizedType.getRawType() instanceof Class + && Map.class.isAssignableFrom( + (Class) parameterizedType.getRawType()) + && parameterizedType.getActualTypeArguments().length > 1) + ? 1 + : 0; + Type actualType = parameterizedType.getActualTypeArguments()[index]; + result = typeToClass(actualType, fallback); + } + if (result == null) { + result = fallback; + } + return result; + } + } + + static class MyBatisMapperFactoryBeanPostProcessor + implements MergedBeanDefinitionPostProcessor, BeanFactoryAware { + + private static final org.apache.commons.logging.Log LOG = + LogFactory.getLog(MyBatisMapperFactoryBeanPostProcessor.class); + + private static final String MAPPER_FACTORY_BEAN = + "org.mybatis.spring.mapper.MapperFactoryBean"; + + private ConfigurableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + + @Override + public void postProcessMergedBeanDefinition( + RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (ClassUtils.isPresent(MAPPER_FACTORY_BEAN, this.beanFactory.getBeanClassLoader())) { + resolveMapperFactoryBeanTypeIfNecessary(beanDefinition); + } + } + + /** + * 为 {@code MapperFactoryBean} 的 BeanDefinition 补齐类型信息。 + * + *

向上下文暴露泛型类型信息以避免过早初始化;同时把构造参数重新注册为 {@code Class} 字面量——扫描器只会记录 mapper 类名字符串,这会使 AOT + * 代码生成退回到 自动装配 {@code MapperFactoryBean} 构造器,native 测试下会尝试自动装配 {@code Class}。 + */ + private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) { + if (!beanDefinition.hasBeanClass() + || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) { + return; + } + if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) { + Class mapperInterface = getMapperInterface(beanDefinition); + if (mapperInterface != null) { + beanDefinition.setTargetType( + ResolvableType.forClassWithGenerics( + beanDefinition.getBeanClass(), mapperInterface)); + ConstructorArgumentValues constructorArgumentValues = + new ConstructorArgumentValues(); + constructorArgumentValues.addGenericArgumentValue(mapperInterface); + beanDefinition.setConstructorArgumentValues(constructorArgumentValues); + } + } + } + + private Class getMapperInterface(RootBeanDefinition beanDefinition) { + try { + return (Class) beanDefinition.getPropertyValues().get("mapperInterface"); + } catch (Exception e) { + LOG.debug("Fail getting mapper interface type.", e); + return null; + } + } + } +} diff --git a/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-sample/reachability-metadata.json b/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-sample/reachability-metadata.json new file mode 100644 index 00000000..2321ae72 --- /dev/null +++ b/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-sample/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "db/schema.sql", + "reason": [ + "dynamic-datasource 初始化脚本,建表用" + ] + } + ] +} diff --git a/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json b/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json new file mode 100644 index 00000000..a330c674 --- /dev/null +++ b/spring-boot-4/mybatis-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration" + }, + "type": "org.mybatis.spring.SqlSessionFactoryBean", + "reason": [ + "mybatis-spring 工厂 Bean,反射调用无参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/mybatis-sample/src/test/java/com/baomidou/samples/mybatis/test/UserServiceTest.java b/spring-boot-4/mybatis-sample/src/test/java/com/baomidou/samples/mybatis/test/UserServiceTest.java new file mode 100644 index 00000000..762f9738 --- /dev/null +++ b/spring-boot-4/mybatis-sample/src/test/java/com/baomidou/samples/mybatis/test/UserServiceTest.java @@ -0,0 +1,52 @@ +/* + * Copyright © ${project.inceptionYear} organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.samples.mybatis.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.baomidou.samples.mybatis.entity.User; +import com.baomidou.samples.mybatis.service.UserService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserServiceTest { + + @Autowired private UserService userService; + + @AfterEach + void cleanUp() { + userService + .selectMasterUsers() + .forEach(user -> userService.deleteUserById(user.getId().longValue())); + } + + @Test + void crudAcrossMasterAndSlave() { + User user = new User(); + user.setName("MyBatis"); + user.setAge(21); + userService.addUser(user); + + assertThat(userService.selectMasterUsers()).isNotEmpty(); + assertThat(userService.selectSlaveUsers()).isNotEmpty(); + + userService.deleteUserById(userService.selectMasterUsers().get(0).getId().longValue()); + assertThat(userService.selectMasterUsers()).isEmpty(); + } +} diff --git a/spring-boot-4/mybatisplus3-sample/pom.xml b/spring-boot-4/mybatisplus3-sample/pom.xml index 6c9c922b..39c3558c 100644 --- a/spring-boot-4/mybatisplus3-sample/pom.xml +++ b/spring-boot-4/mybatisplus3-sample/pom.xml @@ -47,4 +47,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/mybatisplus3-sample/src/main/java/com/baomidou/samples/mybatisplus3/MybatisPlus3Application.java b/spring-boot-4/mybatisplus3-sample/src/main/java/com/baomidou/samples/mybatisplus3/MybatisPlus3Application.java index bd1c9a69..62544af5 100644 --- a/spring-boot-4/mybatisplus3-sample/src/main/java/com/baomidou/samples/mybatisplus3/MybatisPlus3Application.java +++ b/spring-boot-4/mybatisplus3-sample/src/main/java/com/baomidou/samples/mybatisplus3/MybatisPlus3Application.java @@ -20,9 +20,17 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * MyBatis-Plus 样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @Slf4j @SpringBootApplication -@MapperScan("com.baomidou.samples.mybatisplus3.mapper") +@MapperScan( + value = "com.baomidou.samples.mybatisplus3.mapper", + sqlSessionFactoryRef = "sqlSessionFactory") public class MybatisPlus3Application { public static void main(String[] args) { diff --git a/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json new file mode 100644 index 00000000..c0453209 --- /dev/null +++ b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.mybatisplus.core.MybatisConfiguration" + }, + "type": "com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler", + "reason": [ + "MyBatis-Plus 默认枚举处理器,反射调用构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatisplus3-sample/reachability-metadata.json b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatisplus3-sample/reachability-metadata.json new file mode 100644 index 00000000..2321ae72 --- /dev/null +++ b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatisplus3-sample/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "db/schema.sql", + "reason": [ + "dynamic-datasource 初始化脚本,建表用" + ] + } + ] +} diff --git a/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json new file mode 100644 index 00000000..68d657f6 --- /dev/null +++ b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration" + }, + "type": "org.mybatis.spring.SqlSessionTemplate", + "reason": [ + "MP 仓库方法经 OGNL 反射调用 getSqlSessionFactory" + ], + "allPublicMethods": true + } + ] +} diff --git a/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json new file mode 100644 index 00000000..20b54a3a --- /dev/null +++ b/spring-boot-4/mybatisplus3-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json @@ -0,0 +1,24 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.apache.ibatis.session.Configuration" + }, + "type": "org.apache.ibatis.reflection.SystemMetaObject", + "reason": [ + "MP 批量方法经 OGNL 静态调用 forObject,类与方法缺一不可" + ], + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.ibatis.session.Configuration" + }, + "type": "org.apache.ibatis.reflection.MetaObject", + "reason": [ + "MP 批量方法经 OGNL 反射调用 findProperty 等方法" + ], + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/name-pattern-sample/pom.xml b/spring-boot-4/name-pattern-sample/pom.xml index e38690cf..4cab60ee 100644 --- a/spring-boot-4/name-pattern-sample/pom.xml +++ b/spring-boot-4/name-pattern-sample/pom.xml @@ -47,4 +47,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/name-pattern-sample/src/main/java/com/baomidou/samples/pattern/PatternApplication.java b/spring-boot-4/name-pattern-sample/src/main/java/com/baomidou/samples/pattern/PatternApplication.java index a9038d39..d88595cb 100644 --- a/spring-boot-4/name-pattern-sample/src/main/java/com/baomidou/samples/pattern/PatternApplication.java +++ b/spring-boot-4/name-pattern-sample/src/main/java/com/baomidou/samples/pattern/PatternApplication.java @@ -20,9 +20,17 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * 数据源名称匹配(name-pattern)样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @Slf4j @SpringBootApplication -@MapperScan("com.baomidou.samples.pattern.mapper") +@MapperScan( + value = "com.baomidou.samples.pattern.mapper", + sqlSessionFactoryRef = "sqlSessionFactory") public class PatternApplication { public static void main(String[] args) { diff --git a/spring-boot-4/name-pattern-sample/src/main/java/com/baomidou/samples/pattern/config/MyBatisNativeConfiguration.java b/spring-boot-4/name-pattern-sample/src/main/java/com/baomidou/samples/pattern/config/MyBatisNativeConfiguration.java new file mode 100644 index 00000000..8006a80f --- /dev/null +++ b/spring-boot-4/name-pattern-sample/src/main/java/com/baomidou/samples/pattern/config/MyBatisNativeConfiguration.java @@ -0,0 +1,290 @@ +/* + * Copyright © ${project.inceptionYear} organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.samples.pattern.config; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.annotations.DeleteProvider; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.annotations.SelectProvider; +import org.apache.ibatis.annotations.UpdateProvider; +import org.apache.ibatis.reflection.TypeParameterResolver; +import org.mybatis.spring.mapper.MapperFactoryBean; +import org.mybatis.spring.mapper.MapperScannerConfigurer; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * MyBatis 的 Spring AOT 集成。 + * + *

取自 MyBatis 官方的 MyBatisNativeConfiguration 模板;静态的 GraalVM 元数据(日志实现、 缓存装饰器、dtd/xsd 资源等)已按依赖分目录改为 + * {@code META-INF/native-image////reachability-metadata.json}, + * 这里只保留无法用静态元数据表达的部分: + * + *

    + *
  • {@code MyBatisMapperFactoryBeanPostProcessor}:为 MapperFactoryBean 的 BeanDefinition + * 补齐类型信息,避免 AOT 代码生成退回到自动装配 Class; + *
  • {@code MyBatisBeanFactoryInitializationAotProcessor}:处理 mapper 扫描器生成的 MapperFactoryBean + * 定义,并为 mapper 接口及其实体注册反射(由 AOT 扫描结果驱动)。 + *
+ */ +@Configuration(proxyBeanMethods = false) +public class MyBatisNativeConfiguration { + + @Bean + static MyBatisMapperFactoryBeanPostProcessor myBatisMapperFactoryBeanPostProcessor() { + return new MyBatisMapperFactoryBeanPostProcessor(); + } + + @Bean + MyBatisBeanFactoryInitializationAotProcessor myBatisBeanFactoryInitializationAotProcessor() { + return new MyBatisBeanFactoryInitializationAotProcessor(); + } + + static class MyBatisBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter { + + private final Set> excludeClasses = new HashSet<>(); + + MyBatisBeanFactoryInitializationAotProcessor() { + excludeClasses.add(MapperScannerConfigurer.class); + } + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return excludeClasses.contains(registeredBean.getBeanClass()); + } + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + String[] beanNames = beanFactory.getBeanNamesForType(MapperFactoryBean.class); + if (beanNames.length == 0) { + return null; + } + return (context, code) -> { + RuntimeHints hints = context.getRuntimeHints(); + for (String beanName : beanNames) { + BeanDefinition beanDefinition = + beanFactory.getBeanDefinition(beanName.substring(1)); + PropertyValue mapperInterface = + beanDefinition.getPropertyValues().getPropertyValue("mapperInterface"); + if (mapperInterface != null && mapperInterface.getValue() != null) { + Class mapperInterfaceType = (Class) mapperInterface.getValue(); + if (mapperInterfaceType != null) { + registerReflectionTypeIfNecessary(mapperInterfaceType, hints); + hints.proxies().registerJdkProxy(mapperInterfaceType); + hints.resources() + .registerPattern( + mapperInterfaceType + .getName() + .replace('.', '/') + .concat(".xml")); + registerMapperRelationships(mapperInterfaceType, hints); + } + } + } + }; + } + + private void registerMapperRelationships(Class mapperInterfaceType, RuntimeHints hints) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(mapperInterfaceType); + for (Method method : methods) { + if (method.getDeclaringClass() != Object.class) { + ReflectionUtils.makeAccessible(method); + registerSqlProviderTypes( + method, + hints, + SelectProvider.class, + SelectProvider::value, + SelectProvider::type); + registerSqlProviderTypes( + method, + hints, + InsertProvider.class, + InsertProvider::value, + InsertProvider::type); + registerSqlProviderTypes( + method, + hints, + UpdateProvider.class, + UpdateProvider::value, + UpdateProvider::type); + registerSqlProviderTypes( + method, + hints, + DeleteProvider.class, + DeleteProvider::value, + DeleteProvider::type); + Class returnType = + MyBatisMapperTypeUtils.resolveReturnClass(mapperInterfaceType, method); + registerReflectionTypeIfNecessary(returnType, hints); + MyBatisMapperTypeUtils.resolveParameterClasses(mapperInterfaceType, method) + .forEach(x -> registerReflectionTypeIfNecessary(x, hints)); + } + } + } + + @SafeVarargs + private void registerSqlProviderTypes( + Method method, + RuntimeHints hints, + Class annotationType, + Function>... providerTypeResolvers) { + for (T annotation : method.getAnnotationsByType(annotationType)) { + for (Function> providerTypeResolver : providerTypeResolvers) { + registerReflectionTypeIfNecessary( + providerTypeResolver.apply(annotation), hints); + } + } + } + + private void registerReflectionTypeIfNecessary(Class type, RuntimeHints hints) { + if (!type.isPrimitive() && !type.getName().startsWith("java")) { + hints.reflection().registerType(type, MemberCategory.values()); + } + } + } + + static class MyBatisMapperTypeUtils { + /** 工具类,禁止实例化。 */ + private MyBatisMapperTypeUtils() {} + + static Class resolveReturnClass(Class mapperInterface, Method method) { + Type resolvedReturnType = + TypeParameterResolver.resolveReturnType(method, mapperInterface); + return typeToClass(resolvedReturnType, method.getReturnType()); + } + + static Set> resolveParameterClasses(Class mapperInterface, Method method) { + return Stream.of(TypeParameterResolver.resolveParamTypes(method, mapperInterface)) + .map(x -> typeToClass(x, x instanceof Class ? (Class) x : Object.class)) + .collect(Collectors.toSet()); + } + + private static Class typeToClass(Type src, Class fallback) { + Class result = null; + if (src instanceof Class) { + if (((Class) src).isArray()) { + result = ((Class) src).getComponentType(); + } else { + result = (Class) src; + } + } else if (src instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) src; + int index = + (parameterizedType.getRawType() instanceof Class + && Map.class.isAssignableFrom( + (Class) parameterizedType.getRawType()) + && parameterizedType.getActualTypeArguments().length > 1) + ? 1 + : 0; + Type actualType = parameterizedType.getActualTypeArguments()[index]; + result = typeToClass(actualType, fallback); + } + if (result == null) { + result = fallback; + } + return result; + } + } + + static class MyBatisMapperFactoryBeanPostProcessor + implements MergedBeanDefinitionPostProcessor, BeanFactoryAware { + + private static final org.apache.commons.logging.Log LOG = + LogFactory.getLog(MyBatisMapperFactoryBeanPostProcessor.class); + + private static final String MAPPER_FACTORY_BEAN = + "org.mybatis.spring.mapper.MapperFactoryBean"; + + private ConfigurableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + + @Override + public void postProcessMergedBeanDefinition( + RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (ClassUtils.isPresent(MAPPER_FACTORY_BEAN, this.beanFactory.getBeanClassLoader())) { + resolveMapperFactoryBeanTypeIfNecessary(beanDefinition); + } + } + + /** + * 为 {@code MapperFactoryBean} 的 BeanDefinition 补齐类型信息。 + * + *

向上下文暴露泛型类型信息以避免过早初始化;同时把构造参数重新注册为 {@code Class} 字面量——扫描器只会记录 mapper 类名字符串,这会使 AOT + * 代码生成退回到 自动装配 {@code MapperFactoryBean} 构造器,native 测试下会尝试自动装配 {@code Class}。 + */ + private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) { + if (!beanDefinition.hasBeanClass() + || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) { + return; + } + if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) { + Class mapperInterface = getMapperInterface(beanDefinition); + if (mapperInterface != null) { + beanDefinition.setTargetType( + ResolvableType.forClassWithGenerics( + beanDefinition.getBeanClass(), mapperInterface)); + ConstructorArgumentValues constructorArgumentValues = + new ConstructorArgumentValues(); + constructorArgumentValues.addGenericArgumentValue(mapperInterface); + beanDefinition.setConstructorArgumentValues(constructorArgumentValues); + } + } + } + + private Class getMapperInterface(RootBeanDefinition beanDefinition) { + try { + return (Class) beanDefinition.getPropertyValues().get("mapperInterface"); + } catch (Exception e) { + LOG.debug("Fail getting mapper interface type.", e); + return null; + } + } + } +} diff --git a/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/com.baomidou/name-pattern-sample/reachability-metadata.json b/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/com.baomidou/name-pattern-sample/reachability-metadata.json new file mode 100644 index 00000000..2321ae72 --- /dev/null +++ b/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/com.baomidou/name-pattern-sample/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "db/schema.sql", + "reason": [ + "dynamic-datasource 初始化脚本,建表用" + ] + } + ] +} diff --git a/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json b/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json new file mode 100644 index 00000000..a330c674 --- /dev/null +++ b/spring-boot-4/name-pattern-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration" + }, + "type": "org.mybatis.spring.SqlSessionFactoryBean", + "reason": [ + "mybatis-spring 工厂 Bean,反射调用无参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/pom.xml b/spring-boot-4/pom.xml index 05e650ae..73311ac7 100644 --- a/spring-boot-4/pom.xml +++ b/spring-boot-4/pom.xml @@ -18,6 +18,7 @@ UTF-8 17 4.1.0 + 4.1.0 3.5.17 3.9.1 4.1.1 @@ -84,6 +85,14 @@ mybatis-spring-boot-starter ${mybatis-spring-boot-starter.version} + + + org.mybatis + mybatis-spring + ${mybatis-spring.version} + com.alibaba druid @@ -126,4 +135,159 @@ + + + + + + + org.graalvm.buildtools + native-maven-plugin + ${native-maven-plugin.version} + true + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot-dependencies.version} + + + repackage + + repackage + + + + + + + + + + + native + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot-dependencies.version} + + + paketobuildpacks/builder:tiny + + true + + + + + + process-aot + + process-aot + + + + + + org.graalvm.buildtools + native-maven-plugin + + ${project.build.outputDirectory} + + + org.springframework.boot + spring-boot-devtools + + + + + + add-reachability-metadata + + add-reachability-metadata + + + + native-compile + package + + compile + + + + + + + + + + nativeTest + + + org.junit.platform + junit-platform-launcher + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + process-test-aot + + process-test-aot + + + + + + org.graalvm.buildtools + native-maven-plugin + + ${project.build.outputDirectory} + + true + + + org.springframework.boot + spring-boot-devtools + + + + + + native-test + + test + + + + + + + + + diff --git a/spring-boot-4/quartz-sample/pom.xml b/spring-boot-4/quartz-sample/pom.xml index 6cccc596..46e4280c 100644 --- a/spring-boot-4/quartz-sample/pom.xml +++ b/spring-boot-4/quartz-sample/pom.xml @@ -67,4 +67,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/com.baomidou/quartz-sample/reachability-metadata.json b/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/com.baomidou/quartz-sample/reachability-metadata.json new file mode 100644 index 00000000..a2e45497 --- /dev/null +++ b/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/com.baomidou/quartz-sample/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.quartz.impl.StdSchedulerFactory" + }, + "type": "com.baomidou.samples.quartz.job.HelloworldJob", + "reason": [ + "Quartz 按类名实例化 Job,无参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json b/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json new file mode 100644 index 00000000..a330c674 --- /dev/null +++ b/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration" + }, + "type": "org.mybatis.spring.SqlSessionFactoryBean", + "reason": [ + "mybatis-spring 工厂 Bean,反射调用无参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/org.quartz-scheduler/quartz/2.5.2/reachability-metadata.json b/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/org.quartz-scheduler/quartz/2.5.2/reachability-metadata.json new file mode 100644 index 00000000..08f08978 --- /dev/null +++ b/spring-boot-4/quartz-sample/src/main/resources/META-INF/native-image/org.quartz-scheduler/quartz/2.5.2/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "org/quartz/impl/jdbcjobstore/tables_*.sql", + "reason": [ + "Quartz 建表脚本,Boot 初始化器读取" + ] + } + ] +} diff --git a/spring-boot-4/quartz-sample/src/test/java/com/baomidou/samples/quartz/test/QuartzSampleTest.java b/spring-boot-4/quartz-sample/src/test/java/com/baomidou/samples/quartz/test/QuartzSampleTest.java index 69d45eeb..e4e7aff7 100644 --- a/spring-boot-4/quartz-sample/src/test/java/com/baomidou/samples/quartz/test/QuartzSampleTest.java +++ b/spring-boot-4/quartz-sample/src/test/java/com/baomidou/samples/quartz/test/QuartzSampleTest.java @@ -24,6 +24,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -35,24 +37,31 @@ class QuartzSampleTest { @Container static MySQLContainer mysql = - new MySQLContainer<>(DockerImageName.parse("mysql:26.7.0-oraclelinux9")) + new NativeAwareMySQLContainer<>(DockerImageName.parse("mysql:26.7.0-oraclelinux9")) .withDatabaseName("quartz"); - @org.springframework.test.context.DynamicPropertySource - static void datasourceProperties( - org.springframework.test.context.DynamicPropertyRegistry registry) { - registry.add( - "spring.datasource.url", - () -> mysql.getJdbcUrl() + "?useSSL=false&allowPublicKeyRetrieval=true"); + @DynamicPropertySource + static void datasourceProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", QuartzSampleTest::mysqlJdbcUrl); registry.add("spring.datasource.username", mysql::getUsername); registry.add("spring.datasource.password", mysql::getPassword); registry.add( - "spring.datasource.dynamic.datasource.quartz.url", - () -> mysql.getJdbcUrl() + "?useSSL=false&allowPublicKeyRetrieval=true"); + "spring.datasource.dynamic.datasource.quartz.url", QuartzSampleTest::mysqlJdbcUrl); registry.add("spring.datasource.dynamic.datasource.quartz.username", mysql::getUsername); registry.add("spring.datasource.dynamic.datasource.quartz.password", mysql::getPassword); } + /** + * 返回 MySQL 容器地址。 + * + *

AOT 处理测试上下文时容器尚未启动,条件求值需要一个等价的 MySQL URL 占位;容器启动后(JVM 测试与 native 测试运行期)始终返回真实地址。 + */ + private static String mysqlJdbcUrl() { + return mysql.isRunning() + ? mysql.getJdbcUrl() + "?useSSL=false&allowPublicKeyRetrieval=true" + : "jdbc:mysql://localhost:3306/quartz?useSSL=false&allowPublicKeyRetrieval=true"; + } + @Autowired private Scheduler scheduler; @Autowired private DataSource dataSource; @@ -68,4 +77,27 @@ void schedulerUsesJdbcStore() throws Exception { .queryForObject("select count(*) from QRTZ_JOB_DETAILS", Integer.class); assertThat(quartzTables).isGreaterThanOrEqualTo(1); } + + /** + * native image 下跳过 {@code mysql-default-conf} 目录的挂载:Testcontainers 无法把 classpath 目录 资源复制进容器,会把 + * {@code /etc/mysql/conf.d} 破坏成普通文件导致 mysqld 启动失败,见 #7954;JVM + * 测试行为保持不变。 + */ + static class NativeAwareMySQLContainer> + extends MySQLContainer { + + NativeAwareMySQLContainer(DockerImageName dockerImageName) { + super(dockerImageName); + } + + @Override + protected void optionallyMapResourceParameterAsVolume( + String paramName, String containerPath, String resourceName, Integer fileMode) { + if (System.getProperty("org.graalvm.nativeimage.imagecode") == null) { + super.optionallyMapResourceParameterAsVolume( + paramName, containerPath, resourceName, fileMode); + } + } + } } diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/pom.xml b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/pom.xml index cc1022ae..eb81b7de 100644 --- a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/pom.xml +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/pom.xml @@ -13,6 +13,10 @@ 5.5.3 + + 1.3 @@ -104,6 +108,18 @@ com.baomidou dynamic-datasource-spring-boot4-starter + + + org.apache.shardingsphere + shardingsphere-infra-reachability-metadata + ${shardingsphere.version} + + + + javax.transaction + javax.transaction-api + ${javax-transaction-api.version} + org.testcontainers testcontainers-mysql @@ -113,4 +129,22 @@ springdoc-openapi-starter-webmvc-ui + + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/ShardingSphereApplication.java b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/ShardingSphereApplication.java index 4a548750..a77b35b1 100644 --- a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/ShardingSphereApplication.java +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/ShardingSphereApplication.java @@ -19,8 +19,16 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * ShardingSphere-JDBC 5.x 样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @SpringBootApplication -@MapperScan("com.baomidou.samples.shardingsphere.jdbc.v5.core.mapper") +@MapperScan( + value = "com.baomidou.samples.shardingsphere.jdbc.v5.core.mapper", + sqlSessionFactoryRef = "sqlSessionFactory") public class ShardingSphereApplication { public static void main(String[] args) { diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/config/MyBatisNativeConfiguration.java b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/config/MyBatisNativeConfiguration.java new file mode 100644 index 00000000..3f031ea6 --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/config/MyBatisNativeConfiguration.java @@ -0,0 +1,290 @@ +/* + * Copyright © ${project.inceptionYear} organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.samples.shardingsphere.jdbc.v5.core.config; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.annotations.DeleteProvider; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.annotations.SelectProvider; +import org.apache.ibatis.annotations.UpdateProvider; +import org.apache.ibatis.reflection.TypeParameterResolver; +import org.mybatis.spring.mapper.MapperFactoryBean; +import org.mybatis.spring.mapper.MapperScannerConfigurer; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * MyBatis 的 Spring AOT 集成。 + * + *

取自 MyBatis 官方的 MyBatisNativeConfiguration 模板;静态的 GraalVM 元数据(日志实现、 缓存装饰器、dtd/xsd 资源等)已按依赖分目录改为 + * {@code META-INF/native-image////reachability-metadata.json}, + * 这里只保留无法用静态元数据表达的部分: + * + *

    + *
  • {@code MyBatisMapperFactoryBeanPostProcessor}:为 MapperFactoryBean 的 BeanDefinition + * 补齐类型信息,避免 AOT 代码生成退回到自动装配 Class; + *
  • {@code MyBatisBeanFactoryInitializationAotProcessor}:处理 mapper 扫描器生成的 MapperFactoryBean + * 定义,并为 mapper 接口及其实体注册反射(由 AOT 扫描结果驱动)。 + *
+ */ +@Configuration(proxyBeanMethods = false) +public class MyBatisNativeConfiguration { + + @Bean + static MyBatisMapperFactoryBeanPostProcessor myBatisMapperFactoryBeanPostProcessor() { + return new MyBatisMapperFactoryBeanPostProcessor(); + } + + @Bean + MyBatisBeanFactoryInitializationAotProcessor myBatisBeanFactoryInitializationAotProcessor() { + return new MyBatisBeanFactoryInitializationAotProcessor(); + } + + static class MyBatisBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter { + + private final Set> excludeClasses = new HashSet<>(); + + MyBatisBeanFactoryInitializationAotProcessor() { + excludeClasses.add(MapperScannerConfigurer.class); + } + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return excludeClasses.contains(registeredBean.getBeanClass()); + } + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + String[] beanNames = beanFactory.getBeanNamesForType(MapperFactoryBean.class); + if (beanNames.length == 0) { + return null; + } + return (context, code) -> { + RuntimeHints hints = context.getRuntimeHints(); + for (String beanName : beanNames) { + BeanDefinition beanDefinition = + beanFactory.getBeanDefinition(beanName.substring(1)); + PropertyValue mapperInterface = + beanDefinition.getPropertyValues().getPropertyValue("mapperInterface"); + if (mapperInterface != null && mapperInterface.getValue() != null) { + Class mapperInterfaceType = (Class) mapperInterface.getValue(); + if (mapperInterfaceType != null) { + registerReflectionTypeIfNecessary(mapperInterfaceType, hints); + hints.proxies().registerJdkProxy(mapperInterfaceType); + hints.resources() + .registerPattern( + mapperInterfaceType + .getName() + .replace('.', '/') + .concat(".xml")); + registerMapperRelationships(mapperInterfaceType, hints); + } + } + } + }; + } + + private void registerMapperRelationships(Class mapperInterfaceType, RuntimeHints hints) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(mapperInterfaceType); + for (Method method : methods) { + if (method.getDeclaringClass() != Object.class) { + ReflectionUtils.makeAccessible(method); + registerSqlProviderTypes( + method, + hints, + SelectProvider.class, + SelectProvider::value, + SelectProvider::type); + registerSqlProviderTypes( + method, + hints, + InsertProvider.class, + InsertProvider::value, + InsertProvider::type); + registerSqlProviderTypes( + method, + hints, + UpdateProvider.class, + UpdateProvider::value, + UpdateProvider::type); + registerSqlProviderTypes( + method, + hints, + DeleteProvider.class, + DeleteProvider::value, + DeleteProvider::type); + Class returnType = + MyBatisMapperTypeUtils.resolveReturnClass(mapperInterfaceType, method); + registerReflectionTypeIfNecessary(returnType, hints); + MyBatisMapperTypeUtils.resolveParameterClasses(mapperInterfaceType, method) + .forEach(x -> registerReflectionTypeIfNecessary(x, hints)); + } + } + } + + @SafeVarargs + private void registerSqlProviderTypes( + Method method, + RuntimeHints hints, + Class annotationType, + Function>... providerTypeResolvers) { + for (T annotation : method.getAnnotationsByType(annotationType)) { + for (Function> providerTypeResolver : providerTypeResolvers) { + registerReflectionTypeIfNecessary( + providerTypeResolver.apply(annotation), hints); + } + } + } + + private void registerReflectionTypeIfNecessary(Class type, RuntimeHints hints) { + if (!type.isPrimitive() && !type.getName().startsWith("java")) { + hints.reflection().registerType(type, MemberCategory.values()); + } + } + } + + static class MyBatisMapperTypeUtils { + /** 工具类,禁止实例化。 */ + private MyBatisMapperTypeUtils() {} + + static Class resolveReturnClass(Class mapperInterface, Method method) { + Type resolvedReturnType = + TypeParameterResolver.resolveReturnType(method, mapperInterface); + return typeToClass(resolvedReturnType, method.getReturnType()); + } + + static Set> resolveParameterClasses(Class mapperInterface, Method method) { + return Stream.of(TypeParameterResolver.resolveParamTypes(method, mapperInterface)) + .map(x -> typeToClass(x, x instanceof Class ? (Class) x : Object.class)) + .collect(Collectors.toSet()); + } + + private static Class typeToClass(Type src, Class fallback) { + Class result = null; + if (src instanceof Class) { + if (((Class) src).isArray()) { + result = ((Class) src).getComponentType(); + } else { + result = (Class) src; + } + } else if (src instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) src; + int index = + (parameterizedType.getRawType() instanceof Class + && Map.class.isAssignableFrom( + (Class) parameterizedType.getRawType()) + && parameterizedType.getActualTypeArguments().length > 1) + ? 1 + : 0; + Type actualType = parameterizedType.getActualTypeArguments()[index]; + result = typeToClass(actualType, fallback); + } + if (result == null) { + result = fallback; + } + return result; + } + } + + static class MyBatisMapperFactoryBeanPostProcessor + implements MergedBeanDefinitionPostProcessor, BeanFactoryAware { + + private static final org.apache.commons.logging.Log LOG = + LogFactory.getLog(MyBatisMapperFactoryBeanPostProcessor.class); + + private static final String MAPPER_FACTORY_BEAN = + "org.mybatis.spring.mapper.MapperFactoryBean"; + + private ConfigurableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + + @Override + public void postProcessMergedBeanDefinition( + RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (ClassUtils.isPresent(MAPPER_FACTORY_BEAN, this.beanFactory.getBeanClassLoader())) { + resolveMapperFactoryBeanTypeIfNecessary(beanDefinition); + } + } + + /** + * 为 {@code MapperFactoryBean} 的 BeanDefinition 补齐类型信息。 + * + *

向上下文暴露泛型类型信息以避免过早初始化;同时把构造参数重新注册为 {@code Class} 字面量——扫描器只会记录 mapper 类名字符串,这会使 AOT + * 代码生成退回到 自动装配 {@code MapperFactoryBean} 构造器,native 测试下会尝试自动装配 {@code Class}。 + */ + private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) { + if (!beanDefinition.hasBeanClass() + || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) { + return; + } + if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) { + Class mapperInterface = getMapperInterface(beanDefinition); + if (mapperInterface != null) { + beanDefinition.setTargetType( + ResolvableType.forClassWithGenerics( + beanDefinition.getBeanClass(), mapperInterface)); + ConstructorArgumentValues constructorArgumentValues = + new ConstructorArgumentValues(); + constructorArgumentValues.addGenericArgumentValue(mapperInterface); + beanDefinition.setConstructorArgumentValues(constructorArgumentValues); + } + } + } + + private Class getMapperInterface(RootBeanDefinition beanDefinition) { + try { + return (Class) beanDefinition.getPropertyValues().get("mapperInterface"); + } catch (Exception e) { + LOG.debug("Fail getting mapper interface type.", e); + return null; + } + } + } +} diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.baomidou/shardingsphere-jdbc-5.x-core-sample/reachability-metadata.json b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.baomidou/shardingsphere-jdbc-5.x-core-sample/reachability-metadata.json new file mode 100644 index 00000000..47d6e663 --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.baomidou/shardingsphere-jdbc-5.x-core-sample/reachability-metadata.json @@ -0,0 +1,22 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "org.testcontainers.DockerClientFactory" + }, + "glob": "db/**", + "reason": [ + "dynamic-datasource 初始化脚本目录,建表用" + ] + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "glob": "config*.yaml", + "reason": [ + "ShardingSphere native 专用配置,驱动启动时读取" + ] + } + ] +} diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.github.ben-manes.caffeine/caffeine/3.2.4/reachability-metadata.json b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.github.ben-manes.caffeine/caffeine/3.2.4/reachability-metadata.json new file mode 100644 index 00000000..3b3d9f26 --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.github.ben-manes.caffeine/caffeine/3.2.4/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.github.benmanes.caffeine.cache.Caffeine" + }, + "type": "com.github.benmanes.caffeine.cache.SIMS", + "reason": [ + "maximumSize + softValues 对应的缓存实现类(ShardingSphere 行表达式缓存所用),LocalCacheFactory 按类名经 FACTORY 字段取工厂,官方元数据未覆盖" + ], + "allDeclaredFields": true + } + ] +} diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.mysql/mysql-connector-j/9.7.0/reachability-metadata.json b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.mysql/mysql-connector-j/9.7.0/reachability-metadata.json new file mode 100644 index 00000000..f6cb89e1 --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/com.mysql/mysql-connector-j/9.7.0/reachability-metadata.json @@ -0,0 +1,25 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.mysql.cj.jdbc.Driver" + }, + "type": "com.mysql.cj.jdbc.Driver", + "reason": [ + "DriverManager 按名加载,反射调用无参构造器" + ], + "allDeclaredConstructors": true + } + ], + "resources": [ + { + "condition": { + "typeReached": "com.mysql.cj.jdbc.Driver" + }, + "glob": "com/mysql/cj/util/TimeZoneMapping.properties", + "reason": [ + "MySQL 时区映射表,连接时读取" + ] + } + ] +} diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/org.apache.shardingsphere/shardingsphere-jdbc/5.5.3/reachability-metadata.json b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/org.apache.shardingsphere/shardingsphere-jdbc/5.5.3/reachability-metadata.json new file mode 100644 index 00000000..46cf689e --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/org.apache.shardingsphere/shardingsphere-jdbc/5.5.3/reachability-metadata.json @@ -0,0 +1,227 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.driver.yaml.YamlJDBCConfiguration", + "reason": [ + "ShardingSphere 驱动 YAML 根配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.infra.yaml.config.pojo.mode.YamlModeConfiguration", + "reason": [ + "ShardingSphere 运行模式配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.infra.yaml.config.pojo.mode.YamlPersistRepositoryConfiguration", + "reason": [ + "ShardingSphere 持久化仓库配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.infra.algorithm.core.yaml.YamlAlgorithmConfiguration", + "reason": [ + "ShardingSphere 算法配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sharding.yaml.config.YamlShardingRuleConfiguration", + "reason": [ + "ShardingSphere 分片规则配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sharding.yaml.config.rule.YamlTableRuleConfiguration", + "reason": [ + "ShardingSphere 逻辑表配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sharding.yaml.config.strategy.sharding.YamlShardingStrategyConfiguration", + "reason": [ + "ShardingSphere 分片策略配置基类,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sharding.yaml.config.strategy.sharding.YamlStandardShardingStrategyConfiguration", + "reason": [ + "ShardingSphere 标准分片策略配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sharding.yaml.config.strategy.keygen.YamlKeyGenerateStrategyConfiguration", + "reason": [ + "ShardingSphere 主键生成策略配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.readwritesplitting.yaml.config.YamlReadwriteSplittingRuleConfiguration", + "reason": [ + "ShardingSphere 读写分离规则配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.readwritesplitting.yaml.config.rule.YamlReadwriteSplittingDataSourceGroupRuleConfiguration", + "reason": [ + "ShardingSphere 读写分离数据源组配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.single.yaml.config.YamlSingleRuleConfiguration", + "reason": [ + "ShardingSphere 单表规则配置,SnakeYAML 反射构造并设值" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sql.parser.engine.mysql.parser.MySQLLexer", + "reason": [ + "ShardingSphere MySQL 词法分析器,经 SPI 按名加载实例化" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sql.parser.engine.mysql.parser.MySQLParser", + "reason": [ + "ShardingSphere MySQL 语法分析器,经 SPI 按名加载实例化" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sql.parser.engine.mysql.parser.MySQLParserFacade", + "reason": [ + "ShardingSphere MySQL 解析门面,经 SPI 按名加载实例化" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sql.parser.engine.mysql.visitor.statement.MySQLStatementVisitorFacade", + "reason": [ + "ShardingSphere MySQL 访问器门面,经 SPI 按名加载实例化" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sql.parser.engine.mysql.visitor.statement.type.MySQLDALStatementVisitor", + "reason": [ + "ShardingSphere MySQL DAL 访问器,经 SPI 按名加载实例化" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sql.parser.engine.mysql.visitor.statement.type.MySQLDDLStatementVisitor", + "reason": [ + "ShardingSphere MySQL DDL 访问器,经 SPI 按名加载实例化" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sql.parser.engine.mysql.visitor.statement.type.MySQLDMLStatementVisitor", + "reason": [ + "ShardingSphere MySQL DML 访问器,经 SPI 按名加载实例化" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "type": "org.apache.shardingsphere.sql.parser.engine.mysql.visitor.statement.type.MySQLTCLStatementVisitor", + "reason": [ + "ShardingSphere MySQL TCL 访问器,经 SPI 按名加载实例化" + ], + "allDeclaredConstructors": true + } + ], + "resources": [ + { + "condition": { + "typeReached": "org.apache.shardingsphere.driver.ShardingSphereDriver" + }, + "glob": "META-INF/services/**", + "reason": [ + "SPI 描述文件,ServiceLoader 读取" + ] + } + ] +} diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json new file mode 100644 index 00000000..a330c674 --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration" + }, + "type": "org.mybatis.spring.SqlSessionFactoryBean", + "reason": [ + "mybatis-spring 工厂 Bean,反射调用无参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/application.yml b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/application.yml index be84b951..cbebb052 100644 --- a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/application.yml +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/application.yml @@ -15,7 +15,9 @@ spring: password: "" shardingSphere: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver - url: jdbc:shardingsphere:classpath:config.yaml + # 使用 system_props 占位符解析 config.yaml,默认值即 testcontainers-java 的 `jdbc:tc:` 数据源; + # native 测试通过系统属性切换到符合 native 限制的 config-native.yaml。 + url: jdbc:shardingsphere:classpath:${sample.shardingsphere.config:config.yaml}?placeholder-type=system_props logging: level: com.baomidou: debug diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/config.yaml b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/config.yaml index c43b3588..383a1e69 100644 --- a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/config.yaml +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/main/resources/config.yaml @@ -1,17 +1,27 @@ dataSources: - # TODO The use of `test_hostname` is related to https://github.com/testcontainers/testcontainers-java/issues/4121 + # 数据源配置使用 ShardingSphere YAML 的动态占位符(配合 jdbc:shardingsphere 的 + # `?placeholder-type=system_props`):默认值保持 testcontainers-java 的 `jdbc:tc:` 数据源, + # 无 host 的 URL 在 testcontainers 2.0.0+ 可正确解析库名(testcontainers#4121 已修复), + # GraalVM native image 测试下(见 testcontainers/testcontainers-java#7954)通过系统属性覆盖为 + # 普通 MySQL 驱动与地址。 shardingmaster: dataSourceClassName: com.zaxxer.hikari.HikariDataSource - driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver - jdbcUrl: jdbc:tc:mysql:26.7.0-oraclelinux9://test_hostname/master?TC_INITSCRIPT=db/schema.sql + driverClassName: $${sample.shardingsphere.driver::org.testcontainers.jdbc.ContainerDatabaseDriver} + jdbcUrl: $${sample.shardingsphere.master-url::jdbc:tc:mysql:26.7.0-oraclelinux9:///master?TC_INITSCRIPT=db/schema.sql} + username: $${sample.shardingsphere.username::test} + password: $${sample.shardingsphere.password::test} shardingslave0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource - driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver - jdbcUrl: jdbc:tc:mysql:26.7.0-oraclelinux9://test_hostname/slave1?TC_INITSCRIPT=db/schema.sql + driverClassName: $${sample.shardingsphere.driver::org.testcontainers.jdbc.ContainerDatabaseDriver} + jdbcUrl: $${sample.shardingsphere.slave0-url::jdbc:tc:mysql:26.7.0-oraclelinux9:///slave1?TC_INITSCRIPT=db/schema.sql} + username: $${sample.shardingsphere.username::test} + password: $${sample.shardingsphere.password::test} shardingslave1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource - driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver - jdbcUrl: jdbc:tc:mysql:26.7.0-oraclelinux9://test_hostname/slave2?TC_INITSCRIPT=db/schema.sql + driverClassName: $${sample.shardingsphere.driver::org.testcontainers.jdbc.ContainerDatabaseDriver} + jdbcUrl: $${sample.shardingsphere.slave1-url::jdbc:tc:mysql:26.7.0-oraclelinux9:///slave2?TC_INITSCRIPT=db/schema.sql} + username: $${sample.shardingsphere.username::test} + password: $${sample.shardingsphere.password::test} rules: - !SHARDING tables: diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/AbstractNativeImageSupportTest.java b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/AbstractNativeImageSupportTest.java new file mode 100644 index 00000000..e1e11a40 --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/AbstractNativeImageSupportTest.java @@ -0,0 +1,123 @@ +/* + * Copyright © ${project.inceptionYear} organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.samples.shardingsphere.jdbc.v5.core; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * GraalVM native image 下的测试准备。 + * + *

{@code org.testcontainers:mysql} 的 {@code MySQLContainer} 在 native image 下无法把 classpath 目录 + * {@code mysql-default-conf} 挂载进容器,会把 {@code /etc/mysql/conf.d} 破坏成 普通文件导致 mysqld 启动失败,见 #7954。因此 native 测试改用 + * {@link GenericContainer} 启动普通 MySQL 容器,并在测试类初始化时通过 系统属性把配置文件切换到符合 native 限制的 {@code + * config-native.yaml} 并指向该容器;JVM 测试继续使用 {@code jdbc:tc:} 数据源与 {@code config.yaml},不执行任何额外逻辑。 + */ +abstract class AbstractNativeImageSupportTest { + + private static final String MYSQL_IMAGE = "mysql:26.7.0-oraclelinux9"; + + private static final String MYSQL_ROOT_PASSWORD = "test"; + + private static final String SYSTEM_PROPERTY_PREFIX = "sample.shardingsphere."; + + static { + if ("runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))) { + configureNativeImageDatasources(); + } + } + + private static void configureNativeImageDatasources() { + GenericContainer mysql = + new GenericContainer<>(DockerImageName.parse(MYSQL_IMAGE)) + .withEnv("MYSQL_ROOT_PASSWORD", MYSQL_ROOT_PASSWORD) + .withEnv("MYSQL_DATABASE", "test") + .withExposedPorts(3306); + mysql.start(); + String jdbcUrl = + "jdbc:mysql://" + + mysql.getHost() + + ":" + + mysql.getMappedPort(3306) + + "/test?useSSL=false&allowPublicKeyRetrieval=true"; + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + awaitDatabaseReady(jdbcUrl); + runSchemaScript(jdbcUrl); + } catch (ReflectiveOperationException | SQLException | IOException exception) { + throw new IllegalStateException( + "Failed to prepare MySQL container for native image test", exception); + } + System.setProperty(SYSTEM_PROPERTY_PREFIX + "config", "config-native.yaml"); + System.setProperty(SYSTEM_PROPERTY_PREFIX + "driver", "com.mysql.cj.jdbc.Driver"); + System.setProperty(SYSTEM_PROPERTY_PREFIX + "username", "root"); + System.setProperty(SYSTEM_PROPERTY_PREFIX + "password", MYSQL_ROOT_PASSWORD); + System.setProperty(SYSTEM_PROPERTY_PREFIX + "master-url", jdbcUrl); + System.setProperty(SYSTEM_PROPERTY_PREFIX + "slave0-url", jdbcUrl); + System.setProperty(SYSTEM_PROPERTY_PREFIX + "slave1-url", jdbcUrl); + } + + private static void awaitDatabaseReady(String jdbcUrl) throws SQLException { + long deadline = System.currentTimeMillis() + 60_000L; + while (true) { + try (Connection connection = + DriverManager.getConnection(jdbcUrl, "root", MYSQL_ROOT_PASSWORD)) { + return; + } catch (SQLException exception) { + if (System.currentTimeMillis() > deadline) { + throw exception; + } + try { + Thread.sleep(1_000L); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "Interrupted while waiting for MySQL", interruptedException); + } + } + } + } + + private static void runSchemaScript(String jdbcUrl) throws SQLException, IOException { + String script; + try (InputStream input = + AbstractNativeImageSupportTest.class + .getClassLoader() + .getResourceAsStream("db/schema.sql")) { + if (input == null) { + throw new IllegalStateException("db/schema.sql not found on classpath"); + } + script = new String(input.readAllBytes(), StandardCharsets.UTF_8); + } + try (Connection connection = + DriverManager.getConnection(jdbcUrl, "root", MYSQL_ROOT_PASSWORD); + Statement statement = connection.createStatement()) { + for (String sql : script.split(";")) { + if (!sql.isBlank()) { + statement.execute(sql); + } + } + } + } +} diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/ControllerLayerTests.java b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/ControllerLayerTests.java index 5bb3c888..46f69869 100644 --- a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/ControllerLayerTests.java +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/ControllerLayerTests.java @@ -34,7 +34,7 @@ import org.springframework.web.context.WebApplicationContext; @SpringBootTest -public class ControllerLayerTests { +public class ControllerLayerTests extends AbstractNativeImageSupportTest { MockMvc mockMvc; @Autowired TOrderMapper tOrderMapper; diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/MapperLayerTests.java b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/MapperLayerTests.java index c80116a5..68cc570a 100644 --- a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/MapperLayerTests.java +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/java/com/baomidou/samples/shardingsphere/jdbc/v5/core/MapperLayerTests.java @@ -29,7 +29,7 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -public class MapperLayerTests { +public class MapperLayerTests extends AbstractNativeImageSupportTest { @Autowired TOrderMapper tOrderMapper; @BeforeEach diff --git a/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/resources/config-native.yaml b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/resources/config-native.yaml new file mode 100644 index 00000000..515f0505 --- /dev/null +++ b/spring-boot-4/shardingsphere-jdbc-5.x-core-sample/src/test/resources/config-native.yaml @@ -0,0 +1,62 @@ +# GraalVM native image 测试使用的 ShardingSphere 配置(JVM 测试仍使用 config.yaml)。 +# 依据 ShardingSphere 官方 GraalVM Native Image 文档的使用限制: +# - INLINE 算法依赖 GroovyShell,native 下不可用(oracle/graal#5522),改用等价的 MOD 算法; +# - actualDataNodes、读写分离的分组与库名需要使用 行表达式,以绕开 GroovyShell。 +dataSources: + shardingmaster: + dataSourceClassName: com.zaxxer.hikari.HikariDataSource + driverClassName: $${sample.shardingsphere.driver::org.testcontainers.jdbc.ContainerDatabaseDriver} + jdbcUrl: $${sample.shardingsphere.master-url::jdbc:tc:mysql:26.7.0-oraclelinux9:///master?TC_INITSCRIPT=db/schema.sql} + username: $${sample.shardingsphere.username::test} + password: $${sample.shardingsphere.password::test} + shardingslave0: + dataSourceClassName: com.zaxxer.hikari.HikariDataSource + driverClassName: $${sample.shardingsphere.driver::org.testcontainers.jdbc.ContainerDatabaseDriver} + jdbcUrl: $${sample.shardingsphere.slave0-url::jdbc:tc:mysql:26.7.0-oraclelinux9:///slave1?TC_INITSCRIPT=db/schema.sql} + username: $${sample.shardingsphere.username::test} + password: $${sample.shardingsphere.password::test} + shardingslave1: + dataSourceClassName: com.zaxxer.hikari.HikariDataSource + driverClassName: $${sample.shardingsphere.driver::org.testcontainers.jdbc.ContainerDatabaseDriver} + jdbcUrl: $${sample.shardingsphere.slave1-url::jdbc:tc:mysql:26.7.0-oraclelinux9:///slave2?TC_INITSCRIPT=db/schema.sql} + username: $${sample.shardingsphere.username::test} + password: $${sample.shardingsphere.password::test} +rules: +- !SHARDING + tables: + t_order: + actualDataNodes: shardingmaster.t_order0, shardingmaster.t_order1 + tableStrategy: + standard: + shardingColumn: order_id + shardingAlgorithmName: baomidou_mod + keyGenerateStrategy: + column: order_id + keyGeneratorName: baomidou_snowflake + shardingAlgorithms: + baomidou_mod: + type: CLASS_BASED + props: + strategy: STANDARD + # native 下不能使用 INLINE(依赖 GroovyShell),用等价的取模标准算法替代。 + algorithmClassName: org.apache.shardingsphere.sharding.algorithm.sharding.mod.ModShardingAlgorithm + sharding-count: 2 + keyGenerators: + baomidou_snowflake: + type: SNOWFLAKE +- !READWRITE_SPLITTING + dataSourceGroups: + baomidou_readwrite_data_sources: + writeDataSourceName: shardingmaster + readDataSourceNames: + - shardingslave0 + - shardingslave1 + loadBalancerName: baomidou_load_balance_algorithm + loadBalancers: + baomidou_load_balance_algorithm: + type: ROUND_ROBIN +- !SINGLE + tables: + - *.* +props: + sql-show: true diff --git a/spring-boot-4/spel-sample/pom.xml b/spring-boot-4/spel-sample/pom.xml index 8fabe7a8..18237b69 100644 --- a/spring-boot-4/spel-sample/pom.xml +++ b/spring-boot-4/spel-sample/pom.xml @@ -47,4 +47,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/spel-sample/src/main/java/com/baomidou/samples/spel/SpelApplication.java b/spring-boot-4/spel-sample/src/main/java/com/baomidou/samples/spel/SpelApplication.java index d3d26aa5..e39c39a0 100644 --- a/spring-boot-4/spel-sample/src/main/java/com/baomidou/samples/spel/SpelApplication.java +++ b/spring-boot-4/spel-sample/src/main/java/com/baomidou/samples/spel/SpelApplication.java @@ -19,8 +19,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * SpEL 表达式数据源样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @SpringBootApplication -@MapperScan("com.baomidou.samples.spel.mapper") +@MapperScan(value = "com.baomidou.samples.spel.mapper", sqlSessionFactoryRef = "sqlSessionFactory") public class SpelApplication { public static void main(String[] args) { diff --git a/spring-boot-4/spel-sample/src/main/java/com/baomidou/samples/spel/config/MyBatisNativeConfiguration.java b/spring-boot-4/spel-sample/src/main/java/com/baomidou/samples/spel/config/MyBatisNativeConfiguration.java new file mode 100644 index 00000000..e7322a3a --- /dev/null +++ b/spring-boot-4/spel-sample/src/main/java/com/baomidou/samples/spel/config/MyBatisNativeConfiguration.java @@ -0,0 +1,290 @@ +/* + * Copyright © ${project.inceptionYear} organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.samples.spel.config; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.annotations.DeleteProvider; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.annotations.SelectProvider; +import org.apache.ibatis.annotations.UpdateProvider; +import org.apache.ibatis.reflection.TypeParameterResolver; +import org.mybatis.spring.mapper.MapperFactoryBean; +import org.mybatis.spring.mapper.MapperScannerConfigurer; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * MyBatis 的 Spring AOT 集成。 + * + *

取自 MyBatis 官方的 MyBatisNativeConfiguration 模板;静态的 GraalVM 元数据(日志实现、 缓存装饰器、dtd/xsd 资源等)已按依赖分目录改为 + * {@code META-INF/native-image////reachability-metadata.json}, + * 这里只保留无法用静态元数据表达的部分: + * + *

    + *
  • {@code MyBatisMapperFactoryBeanPostProcessor}:为 MapperFactoryBean 的 BeanDefinition + * 补齐类型信息,避免 AOT 代码生成退回到自动装配 Class; + *
  • {@code MyBatisBeanFactoryInitializationAotProcessor}:处理 mapper 扫描器生成的 MapperFactoryBean + * 定义,并为 mapper 接口及其实体注册反射(由 AOT 扫描结果驱动)。 + *
+ */ +@Configuration(proxyBeanMethods = false) +public class MyBatisNativeConfiguration { + + @Bean + static MyBatisMapperFactoryBeanPostProcessor myBatisMapperFactoryBeanPostProcessor() { + return new MyBatisMapperFactoryBeanPostProcessor(); + } + + @Bean + MyBatisBeanFactoryInitializationAotProcessor myBatisBeanFactoryInitializationAotProcessor() { + return new MyBatisBeanFactoryInitializationAotProcessor(); + } + + static class MyBatisBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter { + + private final Set> excludeClasses = new HashSet<>(); + + MyBatisBeanFactoryInitializationAotProcessor() { + excludeClasses.add(MapperScannerConfigurer.class); + } + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return excludeClasses.contains(registeredBean.getBeanClass()); + } + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + String[] beanNames = beanFactory.getBeanNamesForType(MapperFactoryBean.class); + if (beanNames.length == 0) { + return null; + } + return (context, code) -> { + RuntimeHints hints = context.getRuntimeHints(); + for (String beanName : beanNames) { + BeanDefinition beanDefinition = + beanFactory.getBeanDefinition(beanName.substring(1)); + PropertyValue mapperInterface = + beanDefinition.getPropertyValues().getPropertyValue("mapperInterface"); + if (mapperInterface != null && mapperInterface.getValue() != null) { + Class mapperInterfaceType = (Class) mapperInterface.getValue(); + if (mapperInterfaceType != null) { + registerReflectionTypeIfNecessary(mapperInterfaceType, hints); + hints.proxies().registerJdkProxy(mapperInterfaceType); + hints.resources() + .registerPattern( + mapperInterfaceType + .getName() + .replace('.', '/') + .concat(".xml")); + registerMapperRelationships(mapperInterfaceType, hints); + } + } + } + }; + } + + private void registerMapperRelationships(Class mapperInterfaceType, RuntimeHints hints) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(mapperInterfaceType); + for (Method method : methods) { + if (method.getDeclaringClass() != Object.class) { + ReflectionUtils.makeAccessible(method); + registerSqlProviderTypes( + method, + hints, + SelectProvider.class, + SelectProvider::value, + SelectProvider::type); + registerSqlProviderTypes( + method, + hints, + InsertProvider.class, + InsertProvider::value, + InsertProvider::type); + registerSqlProviderTypes( + method, + hints, + UpdateProvider.class, + UpdateProvider::value, + UpdateProvider::type); + registerSqlProviderTypes( + method, + hints, + DeleteProvider.class, + DeleteProvider::value, + DeleteProvider::type); + Class returnType = + MyBatisMapperTypeUtils.resolveReturnClass(mapperInterfaceType, method); + registerReflectionTypeIfNecessary(returnType, hints); + MyBatisMapperTypeUtils.resolveParameterClasses(mapperInterfaceType, method) + .forEach(x -> registerReflectionTypeIfNecessary(x, hints)); + } + } + } + + @SafeVarargs + private void registerSqlProviderTypes( + Method method, + RuntimeHints hints, + Class annotationType, + Function>... providerTypeResolvers) { + for (T annotation : method.getAnnotationsByType(annotationType)) { + for (Function> providerTypeResolver : providerTypeResolvers) { + registerReflectionTypeIfNecessary( + providerTypeResolver.apply(annotation), hints); + } + } + } + + private void registerReflectionTypeIfNecessary(Class type, RuntimeHints hints) { + if (!type.isPrimitive() && !type.getName().startsWith("java")) { + hints.reflection().registerType(type, MemberCategory.values()); + } + } + } + + static class MyBatisMapperTypeUtils { + /** 工具类,禁止实例化。 */ + private MyBatisMapperTypeUtils() {} + + static Class resolveReturnClass(Class mapperInterface, Method method) { + Type resolvedReturnType = + TypeParameterResolver.resolveReturnType(method, mapperInterface); + return typeToClass(resolvedReturnType, method.getReturnType()); + } + + static Set> resolveParameterClasses(Class mapperInterface, Method method) { + return Stream.of(TypeParameterResolver.resolveParamTypes(method, mapperInterface)) + .map(x -> typeToClass(x, x instanceof Class ? (Class) x : Object.class)) + .collect(Collectors.toSet()); + } + + private static Class typeToClass(Type src, Class fallback) { + Class result = null; + if (src instanceof Class) { + if (((Class) src).isArray()) { + result = ((Class) src).getComponentType(); + } else { + result = (Class) src; + } + } else if (src instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) src; + int index = + (parameterizedType.getRawType() instanceof Class + && Map.class.isAssignableFrom( + (Class) parameterizedType.getRawType()) + && parameterizedType.getActualTypeArguments().length > 1) + ? 1 + : 0; + Type actualType = parameterizedType.getActualTypeArguments()[index]; + result = typeToClass(actualType, fallback); + } + if (result == null) { + result = fallback; + } + return result; + } + } + + static class MyBatisMapperFactoryBeanPostProcessor + implements MergedBeanDefinitionPostProcessor, BeanFactoryAware { + + private static final org.apache.commons.logging.Log LOG = + LogFactory.getLog(MyBatisMapperFactoryBeanPostProcessor.class); + + private static final String MAPPER_FACTORY_BEAN = + "org.mybatis.spring.mapper.MapperFactoryBean"; + + private ConfigurableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + + @Override + public void postProcessMergedBeanDefinition( + RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (ClassUtils.isPresent(MAPPER_FACTORY_BEAN, this.beanFactory.getBeanClassLoader())) { + resolveMapperFactoryBeanTypeIfNecessary(beanDefinition); + } + } + + /** + * 为 {@code MapperFactoryBean} 的 BeanDefinition 补齐类型信息。 + * + *

向上下文暴露泛型类型信息以避免过早初始化;同时把构造参数重新注册为 {@code Class} 字面量——扫描器只会记录 mapper 类名字符串,这会使 AOT + * 代码生成退回到 自动装配 {@code MapperFactoryBean} 构造器,native 测试下会尝试自动装配 {@code Class}。 + */ + private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) { + if (!beanDefinition.hasBeanClass() + || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) { + return; + } + if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) { + Class mapperInterface = getMapperInterface(beanDefinition); + if (mapperInterface != null) { + beanDefinition.setTargetType( + ResolvableType.forClassWithGenerics( + beanDefinition.getBeanClass(), mapperInterface)); + ConstructorArgumentValues constructorArgumentValues = + new ConstructorArgumentValues(); + constructorArgumentValues.addGenericArgumentValue(mapperInterface); + beanDefinition.setConstructorArgumentValues(constructorArgumentValues); + } + } + } + + private Class getMapperInterface(RootBeanDefinition beanDefinition) { + try { + return (Class) beanDefinition.getPropertyValues().get("mapperInterface"); + } catch (Exception e) { + LOG.debug("Fail getting mapper interface type.", e); + return null; + } + } + } +} diff --git a/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/com.baomidou/spel-sample/reachability-metadata.json b/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/com.baomidou/spel-sample/reachability-metadata.json new file mode 100644 index 00000000..2321ae72 --- /dev/null +++ b/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/com.baomidou/spel-sample/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "glob": "db/schema.sql", + "reason": [ + "dynamic-datasource 初始化脚本,建表用" + ] + } + ] +} diff --git a/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json b/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json new file mode 100644 index 00000000..a330c674 --- /dev/null +++ b/spring-boot-4/spel-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis-spring/4.1.0/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration" + }, + "type": "org.mybatis.spring.SqlSessionFactoryBean", + "reason": [ + "mybatis-spring 工厂 Bean,反射调用无参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/spel-sample/src/test/java/com/baomidou/samples/spel/test/SPELTest.java b/spring-boot-4/spel-sample/src/test/java/com/baomidou/samples/spel/test/SPELTest.java index 68228cbd..3e36647d 100644 --- a/spring-boot-4/spel-sample/src/test/java/com/baomidou/samples/spel/test/SPELTest.java +++ b/spring-boot-4/spel-sample/src/test/java/com/baomidou/samples/spel/test/SPELTest.java @@ -17,7 +17,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -39,7 +38,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.web.context.WebApplicationContext; -@SpringBootTest(classes = SpelApplication.class, webEnvironment = RANDOM_PORT) +@SpringBootTest(classes = SpelApplication.class) public class SPELTest { MockMvc mockMvc; diff --git a/spring-boot-4/tx-local-sample/pom.xml b/spring-boot-4/tx-local-sample/pom.xml index 21372aee..1d3db80f 100644 --- a/spring-boot-4/tx-local-sample/pom.xml +++ b/spring-boot-4/tx-local-sample/pom.xml @@ -67,4 +67,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/tx-local-sample/src/main/java/com/baomidou/samples/localtx/LocalTxApplication.java b/spring-boot-4/tx-local-sample/src/main/java/com/baomidou/samples/localtx/LocalTxApplication.java index fc819d8a..05f95fe6 100644 --- a/spring-boot-4/tx-local-sample/src/main/java/com/baomidou/samples/localtx/LocalTxApplication.java +++ b/spring-boot-4/tx-local-sample/src/main/java/com/baomidou/samples/localtx/LocalTxApplication.java @@ -20,9 +20,17 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * 本地事务样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @Slf4j @SpringBootApplication -@MapperScan("com.baomidou.samples.localtx.mapper") +@MapperScan( + value = "com.baomidou.samples.localtx.mapper", + sqlSessionFactoryRef = "sqlSessionFactory") public class LocalTxApplication { public static void main(String[] args) { diff --git a/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json b/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json new file mode 100644 index 00000000..c0453209 --- /dev/null +++ b/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.mybatisplus.core.MybatisConfiguration" + }, + "type": "com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler", + "reason": [ + "MyBatis-Plus 默认枚举处理器,反射调用构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/tx-local-sample/reachability-metadata.json b/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/tx-local-sample/reachability-metadata.json new file mode 100644 index 00000000..8ba1bab4 --- /dev/null +++ b/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/com.baomidou/tx-local-sample/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "org.testcontainers.DockerClientFactory" + }, + "glob": "db/**", + "reason": [ + "dynamic-datasource 初始化脚本目录,建表用" + ] + } + ] +} diff --git a/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json b/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json new file mode 100644 index 00000000..82f48566 --- /dev/null +++ b/spring-boot-4/tx-local-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.apache.ibatis.session.Configuration" + }, + "type": "org.apache.ibatis.type.EnumTypeHandler", + "reason": [ + "含枚举字段的实体,反射调用带参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/tx-seata-sample/pom.xml b/spring-boot-4/tx-seata-sample/pom.xml index 3c427ae6..80cd1cad 100644 --- a/spring-boot-4/tx-seata-sample/pom.xml +++ b/spring-boot-4/tx-seata-sample/pom.xml @@ -71,4 +71,20 @@ test + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/spring-boot-4/tx-seata-sample/src/main/java/com/baomidou/samples/seata/SeataApplication.java b/spring-boot-4/tx-seata-sample/src/main/java/com/baomidou/samples/seata/SeataApplication.java index 6f495d0b..76f15baf 100644 --- a/spring-boot-4/tx-seata-sample/src/main/java/com/baomidou/samples/seata/SeataApplication.java +++ b/spring-boot-4/tx-seata-sample/src/main/java/com/baomidou/samples/seata/SeataApplication.java @@ -20,9 +20,15 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * Seata 分布式事务样例的启动类。 + * + *

{@code @MapperScan} 显式声明 {@code sqlSessionFactoryRef},避免 AOT 下 AUTOWIRE_BY_TYPE 装配丢失(JVM + * 行为不变)。 + */ @Slf4j @SpringBootApplication -@MapperScan("com.baomidou.samples.seata.mapper") +@MapperScan(value = "com.baomidou.samples.seata.mapper", sqlSessionFactoryRef = "sqlSessionFactory") public class SeataApplication { public static void main(String[] args) { diff --git a/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json new file mode 100644 index 00000000..1b199103 --- /dev/null +++ b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/dynamic-datasource-creator/4.6.0/reachability-metadata.json @@ -0,0 +1,15 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.dynamic.datasource.creator.DataSourceProperty" + }, + "type": "com.baomidou.dynamic.datasource.creator.DatasourceInitProperties", + "reason": [ + "dynamic-datasource 嵌套绑定类型,无它则 init.schema 等配置绑定失效,见 baomidou/dynamic-datasource#809" + ], + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } + ] +} diff --git a/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json new file mode 100644 index 00000000..c0453209 --- /dev/null +++ b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/mybatis-plus/3.5.17/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "com.baomidou.mybatisplus.core.MybatisConfiguration" + }, + "type": "com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler", + "reason": [ + "MyBatis-Plus 默认枚举处理器,反射调用构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/tx-seata-sample/reachability-metadata.json b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/tx-seata-sample/reachability-metadata.json new file mode 100644 index 00000000..8ba1bab4 --- /dev/null +++ b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/com.baomidou/tx-seata-sample/reachability-metadata.json @@ -0,0 +1,13 @@ +{ + "resources": [ + { + "condition": { + "typeReached": "org.testcontainers.DockerClientFactory" + }, + "glob": "db/**", + "reason": [ + "dynamic-datasource 初始化脚本目录,建表用" + ] + } + ] +} diff --git a/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/org.apache.seata/seata-all/2.6.0/reachability-metadata.json b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/org.apache.seata/seata-all/2.6.0/reachability-metadata.json new file mode 100644 index 00000000..669d9443 --- /dev/null +++ b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/org.apache.seata/seata-all/2.6.0/reachability-metadata.json @@ -0,0 +1,1140 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "io.seata.integration.tx.api.interceptor.parser.GlobalTransactionalInterceptorParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "io.seata.rm.tcc.TCCResourceManager", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "io.seata.rm.tcc.interceptor.parser.TccActionInterceptorParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "io.seata.rm.tcc.remoting.parser.LocalTCCRemotingParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "io.seata.saga.rm.SagaResourceManager", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.config.file.SimpleFileConfig", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.config.file.YamlFileConfig", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.core.auth.DefaultAuthSigner", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.core.context.FastThreadLocalContextCore", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.core.context.ThreadLocalContextCore", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.core.rpc.hook.StatusRpcHook", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.FileRegistryProvider", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.consul.ConsulRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.custom.CustomRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.etcd3.EtcdRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.eureka.EurekaRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.nacos.NacosRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.namingserver.NamingserverRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.raft.RaftRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.redis.RedisRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.sofa.SofaRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.discovery.registry.zk.ZookeeperRegistryProvider", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.integration.tx.api.interceptor.parser.CombineTransactionalInterceptorParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.integration.tx.api.interceptor.parser.GlobalTransactionalInterceptorParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.integration.tx.api.remoting.parser.DubboRemotingParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.integration.tx.api.remoting.parser.HSFRemotingParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.integration.tx.api.remoting.parser.SofaRpcRemotingParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.RMHandlerAT", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.RMHandlerXA", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.DataSourceManager", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.dm.DmInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.kingbase.KingbaseInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.mariadb.MariadbInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.mysql.MySQLInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.oceanbase.OceanBaseInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.oracle.OracleInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.oscar.OscarInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.polardbx.PolarDBXInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.postgresql.PostgresqlInsertExecutor", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.exec.sqlserver.SqlServerInsertExecutor", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.dm.DmEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.kingbase.KingbaseEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.mariadb.MariadbEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.mysql.MySQLEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.oceanbase.OceanBaseEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.oracle.OracleEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.oscar.OscarEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.polardbx.PolarDBXEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.postgresql.PostgresqlEscapeHandler", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.handler.sqlserver.SqlServerEscapeHandler", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.DmTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.KingbaseTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.MariadbTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.MysqlTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.OceanBaseTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.OracleTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.OscarTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.PolarDBXTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.PostgresqlTableMetaCache", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.sql.struct.cache.SqlServerTableMetaCache", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.dm.DmUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.dm.DmUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.kingbase.KingbaseUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.kingbase.KingbaseUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.mariadb.MariadbUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.mariadb.MariadbUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.mysql.MySQLUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.mysql.MySQLUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.oceanbase.OceanBaseUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.oceanbase.OceanBaseUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.oracle.OracleUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.oracle.OracleUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.oscar.OscarUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.oscar.OscarUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.parser.Fastjson2UndoLogParser", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.parser.FastjsonUndoLogParser", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.parser.ForyUndoLogParser", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.parser.JacksonUndoLogParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.parser.KryoUndoLogParser", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.parser.ProtostuffUndoLogParser", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.polardbx.PolarDBXUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.polardbx.PolarDBXUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.postgresql.PostgresqlUndoExecutorHolder", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.postgresql.PostgresqlUndoLogManager", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.sqlserver.SqlServerUndoExecutorHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.undo.sqlserver.SqlServerUndoLogManager", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.datasource.xa.ResourceManagerXA", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.tcc.RMHandlerTCC", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.tcc.TCCResourceManager", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.tcc.interceptor.parser.TccActionInterceptorParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.rm.tcc.remoting.parser.LocalTCCRemotingParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.saga.rm.RMHandlerSaga", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.saga.rm.RMHandlerSagaAnnotation", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.saga.rm.SagaAnnotationResourceManager", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.saga.rm.SagaResourceManager", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.saga.rm.interceptor.parser.SagaAnnotationActionInterceptorParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.saga.rm.remoting.parser.SagaTransactionalRemotingParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.serializer.fastjson2.Fastjson2Serializer", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.serializer.fory.ForySerializer", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.serializer.hessian.HessianSerializer", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.serializer.kryo.KryoSerializer", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.serializer.seata.SeataSerializer", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.spring.SpringTargetClassParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.spring.annotation.scannercheckers.ConfigBeansScannerChecker", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.spring.annotation.scannercheckers.PackageScannerChecker", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.spring.annotation.scannercheckers.ScopeBeansScannerChecker", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.spring.boot.autoconfigure.provider.SpringBootConfigurationProvider", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.DruidDelegatingDbTypeParser", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.DruidDelegatingSQLRecognizerFactory", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.dm.DmOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.kingbase.KingbaseOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.mariadb.MariadbOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.mysql.MySQLOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.oceanbase.OceanBaseOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.oracle.OracleOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.oscar.OscarOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.polardbx.PolarDBXOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.postgresql.PostgresqlOperateRecognizerHolder", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.sqlparser.druid.sqlserver.SqlServerOperateRecognizerHolder", + "reason": [ + "Seata SPI 候选实现,EnhancedServiceLoader 只按类名加载、不实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ] + }, + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "type": "org.apache.seata.tm.DefaultTransactionManager", + "reason": [ + "Seata SPI 选中的实现,EnhancedServiceLoader 反射实例化(JVM 测试经 agent 实测;native 受 apache/incubator-seata#8303 所限未能全程验证)" + ], + "allDeclaredConstructors": true + } + ], + "resources": [ + { + "condition": { + "typeReached": "org.apache.seata.spring.annotation.GlobalTransactionScanner" + }, + "glob": "META-INF/services/**", + "reason": [ + "SPI 描述文件,ServiceLoader 读取" + ] + } + ] +} diff --git a/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json new file mode 100644 index 00000000..82f48566 --- /dev/null +++ b/spring-boot-4/tx-seata-sample/src/main/resources/META-INF/native-image/org.mybatis/mybatis/3.5.19/reachability-metadata.json @@ -0,0 +1,14 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.apache.ibatis.session.Configuration" + }, + "type": "org.apache.ibatis.type.EnumTypeHandler", + "reason": [ + "含枚举字段的实体,反射调用带参构造器" + ], + "allDeclaredConstructors": true + } + ] +} diff --git a/spring-boot-4/tx-seata-sample/src/main/resources/application.yml b/spring-boot-4/tx-seata-sample/src/main/resources/application.yml index 33499660..658e7bd5 100644 --- a/spring-boot-4/tx-seata-sample/src/main/resources/application.yml +++ b/spring-boot-4/tx-seata-sample/src/main/resources/application.yml @@ -9,11 +9,14 @@ seata: enable-auto-data-source-proxy: false service: vgroup-mapping: - #key与上面的tx-service-group的值对应 + #key与上面的tx-service-group的值对应;default_tx_group 是 Seata 客户端回退的默认分组, + #AOT 处理测试上下文时分组解析可能取到该默认值,同样映射到 file 注册中心的 default 集群。 my_test_tx_group: default + default_tx_group: default grouplist: - #seata-server地址仅file注册中心需要 - default: 110.40.253.205:8091 + #seata-server地址仅file注册中心需要;构建期(AOT)与测试运行期分别由本地 server 与测试容器提供, + #默认值指向构建本机(CI 用 services 容器占用同样端口),测试运行时会被下方测试容器的真实地址覆盖。 + default: localhost:8091 config: type: file registry: diff --git a/spring-boot-4/tx-seata-sample/src/test/java/com/baomidou/samples/seata/test/SeataSampleTest.java b/spring-boot-4/tx-seata-sample/src/test/java/com/baomidou/samples/seata/test/SeataSampleTest.java index 6d1b201c..a7ed407d 100644 --- a/spring-boot-4/tx-seata-sample/src/test/java/com/baomidou/samples/seata/test/SeataSampleTest.java +++ b/spring-boot-4/tx-seata-sample/src/test/java/com/baomidou/samples/seata/test/SeataSampleTest.java @@ -74,7 +74,10 @@ static void datasourceProperties(DynamicPropertyRegistry registry) { "spring.datasource.dynamic.datasource.product.password", postgres::getPassword); registry.add( "seata.service.grouplist.default", - () -> seataServer.getHost() + ":" + seataServer.getMappedPort(8091)); + () -> + seataServer.isRunning() + ? seataServer.getHost() + ":" + seataServer.getMappedPort(8091) + : "localhost:8091"); } @Autowired private OrderService orderService;