roleIds) {
try (Session session = sessionFactory.createSession()) {
return session.dsl()
.selectFrom(Role.class)
- .where(field(Role::getId).in(roleIds))
+ .where(role.id.in(roleIds))
.execute();
}
}
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/auth/repository/UserFmRepository.java b/flexmodel-server/src/main/java/dev/flexmodel/auth/repository/UserFmRepository.java
index 7a11212..1063549 100644
--- a/flexmodel-server/src/main/java/dev/flexmodel/auth/repository/UserFmRepository.java
+++ b/flexmodel-server/src/main/java/dev/flexmodel/auth/repository/UserFmRepository.java
@@ -1,14 +1,14 @@
package dev.flexmodel.auth.repository;
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
import dev.flexmodel.codegen.entity.User;
import dev.flexmodel.session.Session;
import dev.flexmodel.session.SessionFactory;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
import java.util.List;
-import static dev.flexmodel.query.Expressions.field;
+import static dev.flexmodel.codegen.System.user;
/**
* @author cjbi
@@ -24,8 +24,8 @@ public User findByUsername(String username) {
try (Session session = sessionFactory.createSession()) {
return session.dsl()
.selectFrom(User.class)
- .where(field(User::getId).eq(username)
- .or(field(User::getEmail).eq(username)))
+ .where(user.id.eq(username)
+ .or(user.email.eq(username)))
.executeOne();
}
}
@@ -35,7 +35,7 @@ public User findById(String userId) {
try (Session session = sessionFactory.createSession()) {
return session.dsl()
.selectFrom(User.class)
- .where(field(User::getId).eq(userId))
+ .where(user.id.eq(userId))
.executeOne();
}
}
@@ -65,7 +65,7 @@ public void delete(String userId) {
try (Session session = sessionFactory.createSession()) {
session.dsl()
.deleteFrom(User.class)
- .where(field(User::getId).eq(userId))
+ .where(user.id.eq(userId))
.execute();
}
}
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/auth/service/ApiKeyGenerator.java b/flexmodel-server/src/main/java/dev/flexmodel/auth/service/ApiKeyGenerator.java
index bd92be0..9095459 100644
--- a/flexmodel-server/src/main/java/dev/flexmodel/auth/service/ApiKeyGenerator.java
+++ b/flexmodel-server/src/main/java/dev/flexmodel/auth/service/ApiKeyGenerator.java
@@ -10,10 +10,12 @@
* API Key 生成工具。
* 生成格式:fm_ak_{type}_{random40chars}
* 存储 SHA-256 哈希,不存原文。
+ *
+ * {@link SecureRandom} 实例为方法内局部变量,
+ * 避免 static 字段在 Native Image 构建期进入 image heap 的问题。
*/
public class ApiKeyGenerator {
- private static final SecureRandom RANDOM = new SecureRandom();
private static final String CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
public record GeneratedKey(String plainText, String hash, String prefix) {
@@ -26,9 +28,10 @@ public record GeneratedKey(String plainText, String hash, String prefix) {
* @return 包含明文、SHA-256 哈希和前缀的 GeneratedKey
*/
public static GeneratedKey generate(String keyType) {
+ SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder(40);
for (int i = 0; i < 40; i++) {
- sb.append(CHARS.charAt(RANDOM.nextInt(CHARS.length())));
+ sb.append(CHARS.charAt(random.nextInt(CHARS.length())));
}
String randomPart = sb.toString();
String plainText = "fm_ak_" + keyType + "_" + randomPart;
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/AbstractRepository.java b/flexmodel-server/src/main/java/dev/flexmodel/common/AbstractRepository.java
index 3aea2b2..047b3b7 100644
--- a/flexmodel-server/src/main/java/dev/flexmodel/common/AbstractRepository.java
+++ b/flexmodel-server/src/main/java/dev/flexmodel/common/AbstractRepository.java
@@ -1,14 +1,11 @@
package dev.flexmodel.common;
-import dev.flexmodel.codegen.entity.Branch;
+import dev.flexmodel.codegen.System;
import dev.flexmodel.codegen.entity.Project;
import dev.flexmodel.session.Session;
import dev.flexmodel.session.SessionFactory;
import jakarta.inject.Inject;
-import dev.flexmodel.common.utils.StringUtils;
-
-import static dev.flexmodel.query.Expressions.field;
/**
* @author chengjinbao
@@ -31,7 +28,7 @@ protected Session getProjectSession(String projectId) {
// 查询项目对应的 databaseName 并更新缓存
try (Session session = sessionFactory.createSession()) {
Project project = session.dsl().selectFrom(Project.class)
- .where(field(Project::getId).eq(projectId))
+ .where(System.project.id.eq(projectId))
.executeOne();
if (project == null || project.getDatabaseName() == null) {
throw new IllegalArgumentException("项目不存在或 databaseName 为空: " + projectId);
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/FlexmodelConfig.java b/flexmodel-server/src/main/java/dev/flexmodel/common/FlexmodelConfig.java
index 29a0ba6..71e2328 100644
--- a/flexmodel-server/src/main/java/dev/flexmodel/common/FlexmodelConfig.java
+++ b/flexmodel-server/src/main/java/dev/flexmodel/common/FlexmodelConfig.java
@@ -15,11 +15,13 @@
@ConfigMapping(prefix = "flexmodel")
public interface FlexmodelConfig extends Serializable {
+ String DEFAULT_SCHEMA_NAME = "system";
+
@WithName("project-url-template")
String projectUrlTemplate();
@WithName("datasource")
- @WithUnnamedKey("system")
+ @WithUnnamedKey(DEFAULT_SCHEMA_NAME)
Map datasources();
@WithDefault("${quarkus.http.root-path}")
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/SchemaRegistry.java b/flexmodel-server/src/main/java/dev/flexmodel/common/SchemaRegistry.java
index d1fb123..80892d8 100644
--- a/flexmodel-server/src/main/java/dev/flexmodel/common/SchemaRegistry.java
+++ b/flexmodel-server/src/main/java/dev/flexmodel/common/SchemaRegistry.java
@@ -1,14 +1,15 @@
package dev.flexmodel.common;
-import com.zaxxer.hikari.HikariDataSource;
import dev.flexmodel.SchemaProvider;
import dev.flexmodel.codegen.entity.Project;
+import dev.flexmodel.common.config.AgroalDataSourceFactory;
import dev.flexmodel.common.config.EngineConfig;
import dev.flexmodel.common.utils.StringUtils;
+import dev.flexmodel.project.ProjectService;
import dev.flexmodel.session.Session;
import dev.flexmodel.session.SessionFactory;
import dev.flexmodel.sql.JdbcSchemaProvider;
-import dev.flexmodel.project.ProjectService;
+import io.agroal.api.AgroalDataSource;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
@@ -91,7 +92,7 @@ public void registerSchema(String schemaName) {
return;
}
// 配置存在但SchemaProvider未注册,使用配置URL创建
- HikariDataSource ds = buildOptimizedDataSource(configDs.url(), configDs.username().orElse(null), configDs.password().orElse(null));
+ AgroalDataSource ds = AgroalDataSourceFactory.createDataSource(configDs.url(), configDs.username().orElse(null), configDs.password().orElse(null));
SchemaProvider schemaProvider = new JdbcSchemaProvider(actualSchemaName, ds);
sessionFactory.registerSchemaProvider(schemaProvider);
log.info("Registered SchemaProvider '{}' from config URL: {}", actualSchemaName, configDs.url());
@@ -112,14 +113,14 @@ public void registerSchema(String schemaName) {
* @param schemaName Schema 名称
*/
public void unregisterSchema(String schemaName) {
- // 取消注册前关闭对应的 HikariDataSource
+ // 取消注册前关闭对应的 AgroalDataSource
try {
dev.flexmodel.SchemaProvider sp = sessionFactory.getSchemaProvider(schemaName);
- if (sp instanceof JdbcSchemaProvider jsp) {
- jsp.dataSource().unwrap(HikariDataSource.class).close();
+ if (sp instanceof JdbcSchemaProvider jsp && jsp.dataSource() instanceof AgroalDataSource ads) {
+ ads.close();
}
} catch (Exception e) {
- log.warn("Failed to close HikariDataSource for schema '{}'", schemaName, e);
+ log.warn("Failed to close AgroalDataSource for schema '{}'", schemaName, e);
}
sessionFactory.unregisterSchemaProvider(schemaName);
log.info("Unregistered SchemaProvider '{}'", schemaName);
@@ -147,25 +148,7 @@ public DataSource buildJdbcDataSource(String databaseName) {
String jdbcUrl = flexmodelConfig.projectUrlTemplate().replace("{{databaseName}}", databaseName);
String username = datasource.username().orElse(null);
String password = datasource.password().orElse(null);
- return buildOptimizedDataSource(getContent(jdbcUrl), getContent(username), getContent(password));
- }
-
- /**
- * 创建优化配置的 HikariDataSource,统一连接池参数。
- */
- private HikariDataSource buildOptimizedDataSource(String jdbcUrl, String username, String password) {
- dev.flexmodel.common.config.EngineConfig.ensureSqliteParentDir(jdbcUrl);
- HikariDataSource ds = new HikariDataSource();
- ds.setJdbcUrl(jdbcUrl);
- if (username != null) ds.setUsername(username);
- if (password != null) ds.setPassword(password);
- ds.setMaximumPoolSize(10);
- ds.setConnectionTimeout(10000);
- ds.setMaxLifetime(600000);
- ds.setIdleTimeout(300000);
- ds.setLeakDetectionThreshold(60000);
- ds.setValidationTimeout(3000);
- return ds;
+ return AgroalDataSourceFactory.createDataSource(getContent(jdbcUrl), getContent(username), getContent(password));
}
public void delete(Project project) {
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/config/AgroalDataSourceFactory.java b/flexmodel-server/src/main/java/dev/flexmodel/common/config/AgroalDataSourceFactory.java
new file mode 100644
index 0000000..2d56db0
--- /dev/null
+++ b/flexmodel-server/src/main/java/dev/flexmodel/common/config/AgroalDataSourceFactory.java
@@ -0,0 +1,108 @@
+package dev.flexmodel.common.config;
+
+import com.mysql.cj.jdbc.MysqlDataSource;
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.configuration.supplier.AgroalConnectionFactoryConfigurationSupplier;
+import io.agroal.api.configuration.supplier.AgroalConnectionPoolConfigurationSupplier;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.api.security.NamePrincipal;
+import io.agroal.api.security.SimplePassword;
+import org.sqlite.SQLiteDataSource;
+
+import java.sql.SQLException;
+import java.time.Duration;
+
+/**
+ * Agroal 连接池工厂,统一管理 DataSource 的创建与池化参数配置。
+ *
+ * 在 native image 中,通过 {@link AgroalConnectionFactoryConfigurationSupplier#connectionProviderClass(Class)}
+ * 直接指定厂商 DataSource 类,由 Agroal 内部实例化并调用其 getConnection(),
+ * 完全绕过 DriverManager SPI,避免 GraalVM 反射缺失问题。
+ *
+ * @author cjbi
+ */
+public final class AgroalDataSourceFactory {
+
+ private AgroalDataSourceFactory() {
+ }
+
+ /**
+ * 创建标准配置的 AgroalDataSource,用于应用正常运行时数据库访问。
+ */
+ public static AgroalDataSource createDataSource(String url, String username, String password) {
+ EngineConfig.ensureSqliteParentDir(url);
+ return buildDataSource(url, username, password,
+ Duration.ofMinutes(10), // maxLifetime
+ Duration.ofMinutes(5) // reapTimeout (idle)
+ );
+ }
+
+ /**
+ * 创建系统管理用的短存活 AgroalDataSource,用于 Schema DDL 操作。
+ */
+ public static AgroalDataSource createSystemDataSource(String url, String username, String password) {
+ EngineConfig.ensureSqliteParentDir(url);
+ return buildDataSource(url, username, password,
+ Duration.ofSeconds(30), // maxLifetime (短存活)
+ Duration.ofMinutes(5) // reapTimeout
+ );
+ }
+
+ private static AgroalDataSource buildDataSource(String url, String username, String password,
+ Duration maxLifetime, Duration reapTimeout) {
+ Class> providerClass = resolveProviderClass(url);
+
+ AgroalDataSourceConfigurationSupplier config = new AgroalDataSourceConfigurationSupplier();
+ AgroalConnectionPoolConfigurationSupplier poolConfig = config.connectionPoolConfiguration();
+ AgroalConnectionFactoryConfigurationSupplier factoryConfig = poolConfig.connectionFactoryConfiguration();
+
+ factoryConfig.jdbcUrl(url);
+ if (providerClass != null) {
+ factoryConfig.connectionProviderClass(providerClass);
+ }
+ if (username != null && !username.isEmpty()) {
+ factoryConfig.principal(new NamePrincipal(username));
+ }
+ if (password != null && !password.isEmpty()) {
+ factoryConfig.credential(new SimplePassword(password));
+ }
+
+ // 连接池参数
+ poolConfig.maxSize(10);
+ poolConfig.minSize(0);
+ poolConfig.initialSize(0);
+ poolConfig.acquisitionTimeout(Duration.ofSeconds(10)); // connectionTimeout
+ poolConfig.maxLifetime(maxLifetime);
+ poolConfig.reapTimeout(reapTimeout); // idleTimeout
+ poolConfig.leakTimeout(Duration.ofSeconds(60)); // leakDetectionThreshold
+ poolConfig.validationTimeout(Duration.ofSeconds(3)); // validationTimeout
+
+ try {
+ return AgroalDataSource.from(config);
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to create AgroalDataSource: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 根据 JDBC URL 返回对应的厂商 DataSource 类。
+ *
+ * 在 native image 中,通过 connectionProviderClass 直接提供 DataSource 类,
+ * Agroal 会使用 DataSource 模式(而非 Driver 模式),由 PropertyInjector
+ * 反射注入 url/user/password 等属性,完全绕过 DriverManager SPI。
+ *
+ * @return DataSource 类,或 null(回退到 Agroal 默认 DriverManager 方式)
+ */
+ static Class> resolveProviderClass(String url) {
+ if (url == null) {
+ return null;
+ }
+ if (url.startsWith("jdbc:sqlite:")) {
+ return SQLiteDataSource.class;
+ }
+ if (url.startsWith("jdbc:mysql:") || url.startsWith("jdbc:mariadb:")) {
+ return MysqlDataSource.class;
+ }
+ return null;
+ }
+}
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java b/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java
index cc9c676..0d4d0dc 100644
--- a/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java
+++ b/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java
@@ -1,22 +1,23 @@
package dev.flexmodel.common.config;
-import com.zaxxer.hikari.HikariDataSource;
-import dev.flexmodel.common.SchemaRegistry;
-import io.quarkus.runtime.StartupEvent;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.enterprise.event.Observes;
-import jakarta.enterprise.inject.Produces;
-import lombok.extern.slf4j.Slf4j;
-import dev.flexmodel.common.AuditDataEventListener;
-import dev.flexmodel.realtime.RealtimeEventListener;
-import dev.flexmodel.scheduling.TriggerDataChangedEventListener;
import dev.flexmodel.codegen.entity.Branch;
import dev.flexmodel.codegen.entity.Project;
+import dev.flexmodel.common.AuditDataEventListener;
+import dev.flexmodel.common.FlexmodelConfig;
+import dev.flexmodel.common.SchemaRegistry;
import dev.flexmodel.project.BranchRepository;
import dev.flexmodel.project.ProjectService;
+import dev.flexmodel.realtime.RealtimeEventListener;
+import dev.flexmodel.scheduling.TriggerDataChangedEventListener;
import dev.flexmodel.session.SessionFactory;
-import dev.flexmodel.common.FlexmodelConfig;
import dev.flexmodel.sql.JdbcSchemaProvider;
+import dev.flexmodel.test.codegen.DevTest;
+import io.agroal.api.AgroalDataSource;
+import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import jakarta.enterprise.inject.Produces;
+import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.List;
@@ -36,7 +37,6 @@ public void installDatasource(@Observes StartupEvent startupEvent,
long beginTime = System.currentTimeMillis();
List projects = projectService.findProjects();
for (Project project : projects) {
- schemaRegistry.add(project);
// 注册非 main 分支的数据库 SchemaProvider
List branches = branchRepository.findByProjectId(project.getId());
for (Branch branch : branches) {
@@ -53,7 +53,10 @@ public SessionFactory sessionFactory(FlexmodelConfig flexmodelConfig,
AuditDataEventListener auditDataEventListener,
RealtimeEventListener realtimeEventListener) {
FlexmodelConfig.DatasourceConfig datasourceConfig = flexmodelConfig.datasources().get(SYSTEM_DS_KEY);
- HikariDataSource defaultDs = createDataSource(datasourceConfig);
+ AgroalDataSource defaultDs = AgroalDataSourceFactory.createDataSource(
+ datasourceConfig.url(),
+ datasourceConfig.username().orElse(null),
+ datasourceConfig.password().orElse(null));
SessionFactory.Builder builder = SessionFactory.builder()
.setDefaultSchemaProvider(new JdbcSchemaProvider(SYSTEM_DS_KEY, defaultDs))
.setFailsafe(true);
@@ -61,40 +64,25 @@ public SessionFactory sessionFactory(FlexmodelConfig flexmodelConfig,
if (key.equals(SYSTEM_DS_KEY)) {
return;
}
- HikariDataSource ds = createDataSource(value);
+ log.info("Registering datasource: key={}, url={}", key, value.url());
+ AgroalDataSource ds = AgroalDataSourceFactory.createDataSource(
+ value.url(),
+ value.username().orElse(null),
+ value.password().orElse(null));
builder.registerSchemaProvider(new JdbcSchemaProvider(key, ds));
});
+ log.info("Total registered datasources: {}", flexmodelConfig.datasources().keySet());
SessionFactory sf = builder.build();
+ // 直接注册 BuildItem 实例,绕过 SPI ServiceLoader 机制
+ // 在 GraalVM 原生镜像中 ServiceLoader.load() 无法正确发现 SPI 实现类
+ sf.registerBuildItem(new dev.flexmodel.codegen.System());
+ sf.registerBuildItem(new DevTest());
sf.getEventPublisher().addListener(triggerDataChangedEventListener);
sf.getEventPublisher().addListener(auditDataEventListener);
sf.getEventPublisher().addListener(realtimeEventListener);
return sf;
}
- /**
- * 创建优化配置的 HikariDataSource,包含连接泄漏检测和合理的超时设置。
- */
- static HikariDataSource createDataSource(FlexmodelConfig.DatasourceConfig config) {
- ensureSqliteParentDir(config.url());
- HikariDataSource ds = new HikariDataSource();
- ds.setJdbcUrl(config.url());
- ds.setUsername(config.username().orElse(null));
- ds.setPassword(config.password().orElse(null));
- // 连接池最大连接数
- ds.setMaximumPoolSize(10);
- // 获取连接超时:10 秒(默认 30 秒),快速失败而非长时间阻塞
- ds.setConnectionTimeout(10000);
- // 连接最大存活时间:10 分钟(默认 30 分钟),防止数据库侧断开后连接池未感知
- ds.setMaxLifetime(600000);
- // 空闲超时:5 分钟,及时回收空闲连接
- ds.setIdleTimeout(300000);
- // 连接泄漏检测:60 秒,超过此时间的活动连接会在日志中输出警告
- ds.setLeakDetectionThreshold(60000);
- // 连接验证超时:3 秒
- ds.setValidationTimeout(3000);
- return ds;
- }
-
/**
* 如果是 SQLite 文件数据库,确保父目录存在。
*/
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/config/nativeimage/FlexmodelNativeProcessor.java b/flexmodel-server/src/main/java/dev/flexmodel/common/config/nativeimage/FlexmodelNativeProcessor.java
new file mode 100644
index 0000000..0d049e6
--- /dev/null
+++ b/flexmodel-server/src/main/java/dev/flexmodel/common/config/nativeimage/FlexmodelNativeProcessor.java
@@ -0,0 +1,295 @@
+package dev.flexmodel.common.config.nativeimage;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
+
+/**
+ * Flexmodel 原生镜像反射配置处理器。
+ *
+ * 替代传统的 reachability-metadata.json,通过 Quarkus BuildStep
+ * 在构建时自动注册所需类的反射信息,无需维护庞大的 JSON 文件。
+ *
+ * 分类策略:
+ *
+ * - Codegen 实体和枚举 — 运行时数据映射需要全部方法和字段
+ * - DTO 和模型对象 — Jackson 序列化需要全部方法和字段
+ * - Jackson MixIn 类 — 多态反序列化需要构造函数
+ * - 第三方库 — 最小化反射配置
+ *
+ *
+ * @author cjbi
+ */
+public class FlexmodelNativeProcessor {
+
+ /**
+ * 注册 Codegen 生成的实体和枚举。
+ * 这些类在运行时被 Flexmodel 数据引擎通过反射进行属性映射。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerCodegenEntities() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.codegen.entity.**",
+ "dev.flexmodel.codegen.enumeration.**",
+ "dev.flexmodel.test.codegen.entity.**",
+ "dev.flexmodel.test.codegen.enumeration.**"
+ )
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册 BuildItem SPI 实现类。
+ * 这些类通过 ServiceLoader 加载,在原生镜像中需要显式注册
+ * 以支持反射实例化(含无参构造函数)和 Java 序列化(ObjectUtils.deserialize)。
+ *
+ * 注意:必须显式调用 {@code .constructors()},因为 {@code .methods()} 仅注册
+ * 已声明方法(通过 {@link Class#getDeclaredMethods()}),不包含构造函数。
+ * ServiceLoader 在原生镜像中需要无参构造函数的反射访问权限才能实例化提供者类。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerBuildItemImplementations() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.codegen.System",
+ "dev.flexmodel.test.codegen.DevTest",
+ "dev.flexmodel.codegen.ObjectUtils"
+ )
+ .constructors()
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册 BuildItem SPI 服务,使得 ServiceLoader.load(BuildItem.class)
+ * 在原生镜像中能正确发现并实例化所有实现类。
+ *
+ * ServiceProviderBuildItem 同时处理两件事:
+ *
+ * - 将 META-INF/services/dev.flexmodel.BuildItem 描述符嵌入原生镜像
+ * - 将提供者类注册为反射可实例化
+ *
+ *
+ * 注意:使用构造函数 {@code new ServiceProviderBuildItem(serviceInterface, providerClass...)}
+ * 显式指定提供者类名,而非依赖 {@code allProvidersFromClassPath} 的类路径扫描。
+ * 因为 SPI 描述符文件由 codegen 动态生成到 {@code target/classes/} 目录下,
+ * 在 Quarkus 构建阶段的类加载器中可能无法被扫描到。
+ */
+ @BuildStep
+ ServiceProviderBuildItem registerBuildItemServiceProvider() {
+ return new ServiceProviderBuildItem("dev.flexmodel.BuildItem",
+ "dev.flexmodel.codegen.System",
+ "dev.flexmodel.test.codegen.DevTest");
+ }
+
+ /**
+ * 注册所有 DTO 类。
+ * DTO 用于 REST 端点,Jackson 序列化/反序列化需要反射访问。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerDtos() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.auth.dto.**",
+ "dev.flexmodel.project.dto.**",
+ "dev.flexmodel.scheduling.dto.**",
+ "dev.flexmodel.functions.dto.**",
+ "dev.flexmodel.api.dto.**",
+ "dev.flexmodel.flow.dto.**",
+ "dev.flexmodel.common.dto.**",
+ "dev.flexmodel.metrics.dto.**",
+ "dev.flexmodel.storage.dto.**"
+ )
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册引擎模块中的模型定义类。
+ * 这些类使用 Jackson 多态反序列化(@JsonSubTypes / @JsonTypeInfo),
+ * 并通过 ObjectUtils.deserialize() 在运行时从 JSON 反序列化。
+ *
+ * 注意:必须包含 {@code .constructors()},因为 Jackson 在原生镜像中
+ * 需要通过反射调用无参构造函数来实例化模型对象。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerModelDefinitions() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.model.**",
+ "dev.flexmodel.model.field.**",
+ "dev.flexmodel.condition.**",
+ "dev.flexmodel.event.**",
+ "dev.flexmodel.event.impl.**",
+ "dev.flexmodel.ModelImportBundle",
+ "dev.flexmodel.ModelImportBundle$ImportData"
+ )
+ .constructors()
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册 Jackson MixIn 和序列化支持类。
+ * 包含 TypedFieldMixIn、ModelMixIn、IndexMixIn 等类型映射配置。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerJacksonSupport() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.supports.jackson.**",
+ "dev.flexmodel.common.config.web.json.jackson.**",
+ "dev.flexmodel.common.config.web.json.jackson.mixin.**"
+ )
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册流程引擎核心类。
+ * 包含执行器、验证器、服务类等运行时通过 CDI/反射实例化的组件。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerFlowEngineClasses() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.flow.executor.**",
+ "dev.flexmodel.flow.validator.**",
+ "dev.flexmodel.flow.service.**",
+ "dev.flexmodel.flow.common.**",
+ "dev.flexmodel.flow.config.**",
+ "dev.flexmodel.flow.plugin.**",
+ "dev.flexmodel.flow.processor.**"
+ )
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册业务组件类。
+ * 包含调度配置、认证提供者等含 @JsonSubTypes 多态类型的类。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerBusinessComponents() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.scheduling.config.**",
+ "dev.flexmodel.scheduling.job.**",
+ "dev.flexmodel.projectauth.provider.**",
+ "dev.flexmodel.storage.config.**",
+ "dev.flexmodel.sql.dialect.**"
+ )
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册数据层和服务层类。
+ * Repository、Service、Resource 等运行时组件。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerDataAndServiceClasses() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.auth.repository.**",
+ "dev.flexmodel.auth.service.**",
+ "dev.flexmodel.flow.repository.**",
+ "dev.flexmodel.data.**",
+ "dev.flexmodel.scheduling.**",
+ "dev.flexmodel.functions.**",
+ "dev.flexmodel.settings.**",
+ "dev.flexmodel.storage.**",
+ "dev.flexmodel.modeling.**",
+ "dev.flexmodel.api.**",
+ "dev.flexmodel.project.**",
+ "dev.flexmodel.projectauth.**",
+ "dev.flexmodel.mcp.**",
+ "dev.flexmodel.metrics.**",
+ "dev.flexmodel.realtime.**",
+ "dev.flexmodel.audit.**"
+ )
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册第三方库中不含 Quarkus 扩展的类。
+ * 这些类在原生镜像中需要显式声明反射访问。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerThirdPartyClasses() {
+ return ReflectiveClassBuildItem.builder(
+ // Auth0 JWT — 无 Quarkus 扩展
+ "com.auth0.jwt.exceptions.JWTVerificationException",
+ // MySQL JDBC 反射由 quarkus-jdbc-mysql 扩展自动处理
+ // Agroal 连接池反射由 quarkus-agroal 扩展自动处理
+ // ConnectionHandler[] 数组类型见 reachability-metadata.json(BuildStep 不支持数组语法)
+ // SQLite JDBC — 原生驱动需要手动注册 DataSource 反射
+ "org.sqlite.SQLiteDataSource",
+ // Caffeine 缓存 — 内部类需反射
+ "com.github.benmanes.caffeine.cache.UnboundedLocalCache",
+ // GraphQL — 执行结果类
+ "graphql.ExecutionResult",
+ "graphql.ExecutionResultImpl",
+ // Import map
+ "io.mvnpm.importmap.model.Imports"
+ )
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册核心基础设施类。
+ * 包含缓存、通用工具、配置、过滤器、Session、SQL、MongoDB、查询、解析器等。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerCoreInfrastructure() {
+ return ReflectiveClassBuildItem.builder(
+ "dev.flexmodel.cache.**",
+ "dev.flexmodel.common.**",
+ "dev.flexmodel.mongodb.**",
+ "dev.flexmodel.parser.**",
+ "dev.flexmodel.query.**",
+ "dev.flexmodel.session.**",
+ "dev.flexmodel.sql.**",
+ "dev.flexmodel.ModelImportBundle",
+ "dev.flexmodel.ModelImportBundle$ImportData",
+ "dev.flexmodel.ModelRegistry",
+ "dev.flexmodel.SchemaProvider"
+ )
+ .constructors()
+ .methods()
+ .fields()
+ .build();
+ }
+
+ /**
+ * 注册无 Quarkus 扩展覆盖的第三方库类。
+ * 包括 Gson、Guice、JZlib、MongoDB Driver、GraphQL 内部类等。
+ * 已有 Quarkus 扩展处理的库(MySQL JDBC、Agroal、crypto)由扩展自行注册。
+ */
+ @BuildStep
+ ReflectiveClassBuildItem registerAdditionalThirdParty() {
+ return ReflectiveClassBuildItem.builder(
+ // Gson
+ "com.google.gson.Gson",
+ // Guice
+ "com.google.inject.AbstractModule",
+ "com.google.inject.Module",
+ // JZlib 压缩
+ "com.jcraft.jzlib.JZlib",
+ // MongoDB Driver
+ "com.mongodb.client.MongoDatabase",
+ // GraphQL Java 内部类(不可变集合)
+ "graphql.com.google.common.collect.ImmutableCollection",
+ "graphql.com.google.common.collect.ImmutableList",
+ "graphql.com.google.common.collect.RegularImmutableList"
+ )
+ .methods()
+ .fields()
+ .build();
+ }
+
+}
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/config/web/exception/BusinessExceptionMapper.java b/flexmodel-server/src/main/java/dev/flexmodel/common/config/web/exception/BusinessExceptionMapper.java
index 37bee67..4e65bda 100644
--- a/flexmodel-server/src/main/java/dev/flexmodel/common/config/web/exception/BusinessExceptionMapper.java
+++ b/flexmodel-server/src/main/java/dev/flexmodel/common/config/web/exception/BusinessExceptionMapper.java
@@ -1,11 +1,11 @@
package dev.flexmodel.common.config.web.exception;
import dev.flexmodel.auth.exception.AuthException;
+import dev.flexmodel.common.BusinessException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
-import dev.flexmodel.common.BusinessException;
import java.util.HashMap;
import java.util.Map;
@@ -19,7 +19,11 @@ public class BusinessExceptionMapper implements ExceptionMapper body = new HashMap<>();
body.put("code", 401);
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/common/util/JavaScriptUtil.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/common/util/JavaScriptUtil.java
index 9d3e815..af7cab7 100644
--- a/flexmodel-server/src/main/java/dev/flexmodel/flow/common/util/JavaScriptUtil.java
+++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/common/util/JavaScriptUtil.java
@@ -10,7 +10,7 @@
import java.util.Map;
/**
- * JavaScript执行工具类,使用GraalVM JavaScript ScriptEngine
+ * JavaScript执行工具类,使用 quickjs4j (QuickJS) ScriptEngine
*
* @author cjbi
*/
@@ -21,32 +21,15 @@ public class JavaScriptUtil {
private static final ScriptEngineManager SCRIPT_ENGINE_MANAGER = new ScriptEngineManager();
private static final ThreadLocal ENGINE_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
try {
- // 尝试获取GraalVM JavaScript引擎
- ScriptEngine engine = SCRIPT_ENGINE_MANAGER.getEngineByName("graal.js");
+ ScriptEngine engine = SCRIPT_ENGINE_MANAGER.getEngineByName("quickjs4j");
if (engine == null) {
- LOGGER.warn("GraalVM JavaScript engine not found, trying JavaScript engine");
- engine = SCRIPT_ENGINE_MANAGER.getEngineByName("JavaScript");
- }
- if (engine == null) {
- throw new RuntimeException("No JavaScript engine available");
- }
-
- // 配置引擎选项
- Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
- if (bindings != null) {
- // ECMAScript版本:使用最新标准
- bindings.put("polyglot.js.ecmascript-version", "2023");
- // 允许主机访问
- bindings.put("polyglot.js.allowHostAccess", true);
- bindings.put("polyglot.js.allowHostClassLookup", false);
- // 兼容性设置
- bindings.put("polyglot.js.nashorn-compat", false);
+ throw new RuntimeException("quickjs4j JavaScript engine not found on classpath");
}
- LOGGER.info("GraalVM JavaScript ScriptEngine initialized successfully");
+ LOGGER.info("quickjs4j JavaScript ScriptEngine initialized successfully");
return engine;
} catch (Exception e) {
- LOGGER.error("Failed to initialize GraalVM JavaScript ScriptEngine", e);
+ LOGGER.error("Failed to initialize quickjs4j JavaScript ScriptEngine", e);
throw new RuntimeException("Failed to initialize JavaScript engine", e);
}
});
diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/common/util/RequestScriptContext.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/common/util/RequestScriptContext.java
deleted file mode 100644
index 8b37ba3..0000000
--- a/flexmodel-server/src/main/java/dev/flexmodel/flow/common/util/RequestScriptContext.java
+++ /dev/null
@@ -1,194 +0,0 @@
-package dev.flexmodel.flow.common.util;
-
-import dev.flexmodel.JsonUtils;
-import dev.flexmodel.query.Query;
-import dev.flexmodel.session.Session;
-import dev.flexmodel.session.SessionFactory;
-import lombok.Getter;
-import lombok.Setter;
-import lombok.ToString;
-import org.slf4j.LoggerFactory;
-import dev.flexmodel.common.Constants;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-/**
- * 脚本执行上下文:封装请求、响应、环境变量等信息
- * context = {
- * request: {
- * method,
- * url,
- * headers,
- * body,
- * query
- * },
- * response: {
- * status,
- * headers,
- * body
- * },
- * log(msg){},
- * utils:{
- * md5(str){}
- * }
- * }
- *
- */
-@Getter
-@Setter
-@ToString
-public class RequestScriptContext {
-
- public static final String SCRIPT_CONTEXT_KEY = "context";
-
- private final String projectId;
-
- /** 原始请求参数(用户输入) */
- private Request request;
-
- /** 最终响应数据(后置脚本可修改) */
- private Response response;
-
- private SessionFactory sessionFactory;
-
- public RequestScriptContext(String projectId) {
- this.projectId = projectId;
- }
-
- public record Request(String method,
- String url,
- Map headers,
- Map body,
- Map query
- ) {
- }
-
- public record Response(int status,
- String message,
- Map headers,
- Map body
- ) {
- }
-
-
- @SuppressWarnings("all")
- public Map buildContextMap() {
- Map context = new HashMap<>();
-
- // 将 Request record 转换为 Map
- if (request != null) {
- Map requestMap = new HashMap<>();
- requestMap.put("method", request.method());
- requestMap.put("url", request.url());
- requestMap.put("headers", request.headers());
- requestMap.put("body", request.body());
- requestMap.put("query", request.query());
- context.put("request", requestMap);
- }
-
- // 将 Response record 转换为 Map,使用可变的 HashMap 以便 JavaScript 修改后能同步
- if (response != null) {
- Map responseMap = new HashMap<>();
- responseMap.put("status", response.status());
- responseMap.put("message", response.message());
- responseMap.put("headers", response.headers());
- // 直接使用原始 body Map 的引用,这样修改会反映回去
- responseMap.put("body", response.body());
- context.put("response", responseMap);
- }
-
- // 工具类
- context.put("utils", new ScriptUtils());
- // 日志
- context.put("log", new Logger(projectId));
-
- if(sessionFactory!=null) {
- context.put("db", new ScriptExecutionDB(projectId, sessionFactory));
- }
-
- return context;
- }
-
- /**
- * 从 Map 中同步回 Response 对象
- * 用于在 JavaScript 执行后,将修改后的值同步回原始的 Response
- *
- * @param contextMap JavaScript 执行后的 context Map
- */
- @SuppressWarnings("all")
- public void syncFromMap(Map contextMap) {
- JsonUtils.updateValue(this, contextMap);
- }
-
-
- public static class ScriptUtils {
- public String uuid() {
- return UUID.randomUUID().toString();
- }
-
- }
-
- public static class Logger {
-
- private final String projectId;
-
- public Logger(String projectId) {
- this.projectId = projectId;
- }
-
- public void info(String msg, Object... args) {
- LoggerFactory.getLogger(Constants.APP_LOG_CATEGORY_NAME + "_" + projectId).info(msg, args);
- }
-
- public void error(String msg, Object... args) {
- LoggerFactory.getLogger(Constants.APP_LOG_CATEGORY_NAME + "_" + projectId).error(msg, args);
- }
-
- public void debug(String msg, Object... args) {
- LoggerFactory.getLogger(Constants.APP_LOG_CATEGORY_NAME + "_" + projectId).debug(msg, args);
- }
-
- public void warn(String msg, Object... args) {
- LoggerFactory.getLogger(Constants.APP_LOG_CATEGORY_NAME + "_" + projectId).warn(msg, args);
- }
-
- }
-
- public static class ScriptExecutionDB {
-
- private final String datasourceName;
- private final SessionFactory sessionFactory;
-
- public ScriptExecutionDB(String datasourceName, SessionFactory sessionFactory) {
- this.datasourceName = datasourceName;
- this.sessionFactory = sessionFactory;
- }
-
- public Map findById(String modelName, String id) {
- try (Session session = sessionFactory.createSession(datasourceName)) {
- return session.data().findById(modelName, id).orElse(null);
- }
- }
-
- public List