diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java index 87b65396..573af505 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java @@ -28,9 +28,10 @@ public class OrgDataSourceFactory { dataSource.setTestWhileIdle(poolProperties.isTestWhileIdle()); dataSource.setTestOnBorrow(poolProperties.isTestOnBorrow()); dataSource.setTestOnReturn(poolProperties.isTestOnReturn()); - dataSource.setConnectionErrorRetryAttempts(3); - dataSource.setBreakAfterAcquireFailure(false); - dataSource.setFailFast(false); + // 连接失败快速失败,不做重试:重试没有意义,等下次请求再触发建连 + dataSource.setConnectionErrorRetryAttempts(0); + dataSource.setBreakAfterAcquireFailure(true); + dataSource.setFailFast(true); // 定期 ping 空闲连接,对抗云 NAT/防火墙回收空闲 TCP 会话(默认 120s) dataSource.setKeepAlive(true); dataSource.setKeepAliveBetweenTimeMillis(120_000); diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java index f75308e0..ccb04053 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java @@ -1,19 +1,38 @@ package cn.g3soft.fmsapi.database; +import cn.g3soft.fmsapi.exception.AuthenticationException; import com.alibaba.druid.pool.DruidDataSource; import jakarta.annotation.PreDestroy; import jakarta.annotation.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.sql.Connection; import java.sql.SQLException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +/** + * 机构连接池注册表:按机构码懒加载并缓存连接池,定期检测配置文件变更。 + * 配置(url/密码等)变更后无需重启,到期自动重建连接池并延迟关闭旧池, + * 保证在途请求有足够时间归还旧连接。 + */ @Component public class OrgDataSourceManager { - private final ConcurrentMap dataSources = new ConcurrentHashMap<>(); + private static final Logger log = LoggerFactory.getLogger(OrgDataSourceManager.class); + + private final ConcurrentMap dataSources = new ConcurrentHashMap<>(); + private final ScheduledExecutorService retiredPoolCloser = Executors.newSingleThreadScheduledExecutor(task -> { + Thread thread = new Thread(task, "org-datasource-retired-close"); + thread.setDaemon(true); + return thread; + }); @Resource private OrgDatabaseConfigLoader configLoader; @@ -21,9 +40,16 @@ public class OrgDataSourceManager { @Resource private OrgDataSourceFactory dataSourceFactory; + @Resource + private OrgDatabaseProperties properties; + public DruidDataSource getDataSource(String orgId) { String normalizedOrgId = configLoader.normalizeOrgId(orgId); - return dataSources.computeIfAbsent(normalizedOrgId, this::createDataSource); + PoolEntry entry = dataSources.computeIfAbsent(normalizedOrgId, this::createEntry); + if (tryClaimRefreshCheck(entry)) { + return refreshIfConfigChanged(normalizedOrgId, entry).dataSource(); + } + return entry.dataSource(); } public Connection getConnection(String orgId) throws SQLException { @@ -36,11 +62,11 @@ public class OrgDataSourceManager { public boolean closeDataSource(String orgId) { String normalizedOrgId = configLoader.normalizeOrgId(orgId); - DruidDataSource dataSource = dataSources.remove(normalizedOrgId); - if (dataSource == null) { + PoolEntry entry = dataSources.remove(normalizedOrgId); + if (entry == null) { return false; } - dataSource.close(); + entry.dataSource().close(); return true; } @@ -50,12 +76,123 @@ public class OrgDataSourceManager { @PreDestroy public void closeAll() { - dataSources.values().forEach(DruidDataSource::close); + retiredPoolCloser.shutdownNow(); + dataSources.values().forEach(entry -> entry.dataSource().close()); dataSources.clear(); } - private DruidDataSource createDataSource(String orgId) { + /** + * 配置文件变更事件回调:立即核对并切换连接池(仅对已缓存的机构生效)。 + * 由配置监听线程调用,配置读取与建池的 I/O 不占用请求线程。 + */ + public void onConfigChanged(String orgId) { + String normalizedOrgId = configLoader.normalizeOrgId(orgId); + PoolEntry entry = dataSources.get(normalizedOrgId); + if (entry == null) { + return; + } + try { + refreshIfConfigChanged(normalizedOrgId, entry); + } catch (AuthenticationException exception) { + // 配置文件已删除,旧池已在 refreshIfConfigChanged 中淘汰 + } + } + + /** + * TTL 到期后由一个线程认领检测任务(CAS 防止并发重复 stat 文件)。 + */ + private boolean tryClaimRefreshCheck(PoolEntry entry) { + long intervalNanos = properties.getRefreshCheckInterval().toNanos(); + if (intervalNanos <= 0) { + return false; + } + long last = entry.checkedAtNanos.get(); + long now = System.nanoTime(); + if (now - last < intervalNanos) { + return false; + } + return entry.checkedAtNanos.compareAndSet(last, now); + } + + private PoolEntry refreshIfConfigChanged(String orgId, PoolEntry entry) { + long lastModified = configLoader.lastModified(orgId); + if (lastModified == entry.configLastModified()) { + return entry; + } + PoolEntry fresh; + try { + fresh = createEntry(orgId); + } catch (AuthenticationException exception) { + // 配置文件已不存在:机构已下线,淘汰旧池,按"机构码错误"处理 + if (dataSources.remove(orgId, entry)) { + scheduleClose(entry.dataSource()); + } + throw exception; + } catch (RuntimeException exception) { + // 配置读取或建池瞬时失败:保留旧池,下个检测周期重试 + log.warn("刷新机构数据库连接池失败,继续使用旧配置: {}", orgId, exception); + return entry; + } + if (fresh.config().equals(entry.config())) { + // 内容未变(仅时间戳被 touch):避免无谓重建 + entry.configLastModified(fresh.configLastModified()); + fresh.dataSource().close(); + return entry; + } + if (dataSources.replace(orgId, entry, fresh)) { + scheduleClose(entry.dataSource()); + log.info("机构数据库配置已变更,已切换到新连接池: {}", orgId); + return fresh; + } + // 并发刷新:其他线程已完成切换或已淘汰旧条目,丢弃本次多余的池 + fresh.dataSource().close(); + PoolEntry current = dataSources.get(orgId); + if (current != null) { + return current; + } + // 旧条目被并发淘汰且无新条目(如配置文件删除后又被重建):退回懒加载语义 + return dataSources.computeIfAbsent(orgId, this::createEntry); + } + + private PoolEntry createEntry(String orgId) { + // 先取时间戳再加载:若加载期间文件又被修改,记录的是旧时间戳,下个周期能检测到 + long lastModified = configLoader.lastModified(orgId); OrgDatabaseConfig config = configLoader.load(orgId); - return dataSourceFactory.create(orgId, config); + return new PoolEntry(dataSourceFactory.create(orgId, config), config, lastModified); + } + + private void scheduleClose(DruidDataSource dataSource) { + long delayMillis = properties.getRetiredPoolCloseDelay().toMillis(); + retiredPoolCloser.schedule(dataSource::close, delayMillis, TimeUnit.MILLISECONDS); + } + + private static final class PoolEntry { + + private final DruidDataSource dataSource; + private final OrgDatabaseConfig config; + private volatile long configLastModified; + private final AtomicLong checkedAtNanos = new AtomicLong(System.nanoTime()); + + private PoolEntry(DruidDataSource dataSource, OrgDatabaseConfig config, long configLastModified) { + this.dataSource = dataSource; + this.config = config; + this.configLastModified = configLastModified; + } + + private DruidDataSource dataSource() { + return dataSource; + } + + private OrgDatabaseConfig config() { + return config; + } + + private long configLastModified() { + return configLastModified; + } + + private void configLastModified(long configLastModified) { + this.configLastModified = configLastModified; + } } } diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java index 82cafdf7..6aa81d2c 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java @@ -35,10 +35,7 @@ public class OrgDatabaseConfigLoader { public OrgDatabaseConfig load(String orgId) { String normalizedOrgId = normalizeOrgId(orgId); - Path configFile = configDir.resolve(normalizedOrgId + ".properties").normalize(); - if (!configFile.startsWith(configDir)) { - throw new IllegalArgumentException("机构配置文件路径不合法"); - } + Path configFile = resolveConfigFile(normalizedOrgId); if (!Files.isRegularFile(configFile)) { throw new AuthenticationException("机构码错误"); } @@ -80,6 +77,26 @@ public class OrgDatabaseConfigLoader { return configDir; } + /** + * 配置文件修改时间戳,用于检测配置变更。文件不存在时返回 -1。 + */ + public long lastModified(String orgId) { + Path configFile = resolveConfigFile(normalizeOrgId(orgId)); + try { + return Files.getLastModifiedTime(configFile).toMillis(); + } catch (IOException exception) { + return -1L; + } + } + + private Path resolveConfigFile(String normalizedOrgId) { + Path configFile = configDir.resolve(normalizedOrgId + ".properties").normalize(); + if (!configFile.startsWith(configDir)) { + throw new IllegalArgumentException("机构配置文件路径不合法"); + } + return configFile; + } + private String required(Properties properties, String key, Path configFile) { String value = properties.getProperty(key); if (value == null || value.isBlank()) { diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.java new file mode 100644 index 00000000..2b60bd31 --- /dev/null +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.java @@ -0,0 +1,141 @@ +package cn.g3soft.fmsapi.database; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.file.ClosedWatchServiceException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 监听机构配置目录,配置文件变更时立即通知 {@link OrgDataSourceManager} 重建连接池, + * 使配置变更毫秒级生效,配置读取与建池的 I/O 全部发生在监听线程,不占用请求线程。 + * 监听注册失败、事件溢出或目录失效时自动降级为定期轮询兜底,不影响服务。 + */ +@Component +public class OrgDatabaseConfigWatcher { + + private static final Logger log = LoggerFactory.getLogger(OrgDatabaseConfigWatcher.class); + private static final Pattern CONFIG_FILE_PATTERN = + Pattern.compile("([A-Za-z0-9_-]{1,32})\\.properties"); + private static final long DEBOUNCE_MILLIS = 500; + + @Resource + private OrgDatabaseConfigLoader configLoader; + + @Resource + private OrgDataSourceManager dataSourceManager; + + private WatchService watchService; + private volatile Thread watcherThread; + + @PostConstruct + public void start() { + Path configDir = configLoader.getConfigDir(); + try { + watchService = FileSystems.getDefault().newWatchService(); + configDir.register( + watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE + ); + } catch (IOException exception) { + log.warn("注册机构配置目录监听失败,配置变更检测降级为定期轮询: {}", configDir, exception); + closeWatchService(); + return; + } + Thread thread = new Thread(this::watch, "org-dbconfig-watcher"); + thread.setDaemon(true); + this.watcherThread = thread; + thread.start(); + log.info("已启动机构配置目录监听: {}", configDir); + } + + @PreDestroy + public void stop() { + Thread thread = watcherThread; + if (thread != null) { + thread.interrupt(); + } + closeWatchService(); + } + + private void watch() { + try { + while (true) { + WatchKey key = watchService.take(); + Set orgIds = new HashSet<>(); + collect(key, orgIds); + // 合并安静期内的后续事件(编辑器保存常触发多次写入),避免重复重建连接池 + long deadline = System.currentTimeMillis() + DEBOUNCE_MILLIS; + while (System.currentTimeMillis() < deadline) { + WatchKey more = watchService.poll( + deadline - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + if (more == null) { + break; + } + collect(more, orgIds); + } + orgIds.forEach(this::notifyChanged); + } + } catch (ClosedWatchServiceException | InterruptedException exception) { + if (exception instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + // 正常退出:服务关闭或线程中断,轮询兜底仍然生效 + log.debug("机构配置目录监听线程退出"); + } + } + + private void collect(WatchKey key, Set orgIds) { + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + continue; + } + if (event.context() instanceof Path path) { + Matcher matcher = CONFIG_FILE_PATTERN.matcher(path.getFileName().toString()); + if (matcher.matches()) { + orgIds.add(matcher.group(1).toUpperCase(Locale.ROOT)); + } + } + } + if (!key.reset()) { + log.warn("机构配置目录已不可用,文件监听停止,配置变更检测降级为定期轮询"); + closeWatchService(); + } + } + + private void notifyChanged(String orgId) { + try { + dataSourceManager.onConfigChanged(orgId); + } catch (RuntimeException exception) { + log.warn("处理机构配置变更事件失败: {}", orgId, exception); + } + } + + private void closeWatchService() { + if (watchService != null) { + try { + watchService.close(); + } catch (IOException ignored) { + // 关闭失败无需处理 + } + } + } +} diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseProperties.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseProperties.java index 2be6561e..e1fb43a6 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseProperties.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseProperties.java @@ -9,6 +9,8 @@ import java.time.Duration; public class OrgDatabaseProperties { private Path configDir = Path.of("./config/dbconfigs"); + private Duration refreshCheckInterval = Duration.ofSeconds(30); + private Duration retiredPoolCloseDelay = Duration.ofSeconds(60); private final Pool pool = new Pool(); public Path getConfigDir() { @@ -19,6 +21,22 @@ public class OrgDatabaseProperties { this.configDir = configDir; } + public Duration getRefreshCheckInterval() { + return refreshCheckInterval; + } + + public void setRefreshCheckInterval(Duration refreshCheckInterval) { + this.refreshCheckInterval = refreshCheckInterval; + } + + public Duration getRetiredPoolCloseDelay() { + return retiredPoolCloseDelay; + } + + public void setRetiredPoolCloseDelay(Duration retiredPoolCloseDelay) { + this.retiredPoolCloseDelay = retiredPoolCloseDelay; + } + public Pool getPool() { return pool; } diff --git a/code/fms/fms-api/src/main/resources/application.yaml b/code/fms/fms-api/src/main/resources/application.yaml index 91ab78f5..2759dfc7 100644 --- a/code/fms/fms-api/src/main/resources/application.yaml +++ b/code/fms/fms-api/src/main/resources/application.yaml @@ -13,8 +13,13 @@ fms: jwt-expiration: 24h database: config-dir: ./config/dbconfigs + # 机构配置文件变更检测周期(文件监听的兜底,正常由监听毫秒级触发;设为 0s 关闭轮询) + refresh-check-interval: 30s + # 配置变更切换后,旧连接池延迟关闭时间,等待在途请求归还连接 + retired-pool-close-delay: 60s pool: - initial-size: 0 + # 建池时先建 1 条物理连接:配置指向不可达的库时快速失败,保留旧池不切换 + initial-size: 1 min-idle: 0 max-active: 10 max-wait: 5000 diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests.java new file mode 100644 index 00000000..ea609c00 --- /dev/null +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests.java @@ -0,0 +1,175 @@ +package cn.g3soft.fmsapi.database; + +import cn.g3soft.fmsapi.exception.AuthenticationException; +import com.alibaba.druid.pool.DruidDataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.sql.SQLException; +import java.time.Duration; +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OrgDataSourceManagerTests { + + @TempDir + Path configDir; + + private String failPassword; + + @Test + void unreachableNewConfigKeepsOldPool() throws Exception { + OrgDataSourceManager manager = newManager(Duration.ofSeconds(30), Duration.ofMillis(200)); + try { + writeConfig("old-password"); + DruidDataSource first = manager.getDataSource("TEST"); + + failPassword = "unreachable-password"; + writeConfig("unreachable-password"); + bumpMtime(); + manager.onConfigChanged("TEST"); + + // 新配置建池失败(fail-fast):保留旧池继续服务,等下次再试 + assertThat(manager.getDataSource("TEST")).isSameAs(first); + assertThat(first.isClosed()).isFalse(); + } finally { + manager.closeAll(); + } + } + + @Test + void configChangeSwitchesToNewDataSource() throws Exception { + OrgDataSourceManager manager = newManager(Duration.ofMillis(50), Duration.ofMillis(200)); + try { + writeConfig("old-password"); + DruidDataSource first = manager.getDataSource("TEST"); + assertThat(manager.getDataSource("TEST")).isSameAs(first); + + writeConfig("new-password"); + bumpMtime(); + + Thread.sleep(150); + DruidDataSource second = manager.getDataSource("TEST"); + assertThat(second).isNotSameAs(first); + assertThat(second.getUsername()).isEqualTo("test"); + + // 旧池延迟关闭 + Thread.sleep(400); + assertThat(first.isClosed()).isTrue(); + assertThat(second.isClosed()).isFalse(); + } finally { + manager.closeAll(); + } + } + + @Test + void untouchedMtimeReusesPool() throws Exception { + OrgDataSourceManager manager = newManager(Duration.ofMillis(50), Duration.ofMillis(200)); + try { + writeConfig("password"); + DruidDataSource first = manager.getDataSource("TEST"); + + Thread.sleep(150); + assertThat(manager.getDataSource("TEST")).isSameAs(first); + assertThat(first.isClosed()).isFalse(); + } finally { + manager.closeAll(); + } + } + + @Test + void missingConfigFileEvictsPool() throws Exception { + OrgDataSourceManager manager = newManager(Duration.ofMillis(50), Duration.ofMillis(200)); + try { + writeConfig("password"); + DruidDataSource first = manager.getDataSource("TEST"); + + Files.delete(configDir.resolve("TEST.properties")); + Thread.sleep(150); + + assertThatThrownBy(() -> manager.getDataSource("TEST")) + .isInstanceOf(AuthenticationException.class); + assertThat(manager.getDataSourceCount()).isZero(); + } finally { + manager.closeAll(); + } + } + + @Test + void configChangeEventSwitchesPoolImmediately() throws Exception { + OrgDataSourceManager manager = newManager(Duration.ofSeconds(30), Duration.ofMillis(200)); + try { + writeConfig("old-password"); + DruidDataSource first = manager.getDataSource("TEST"); + + writeConfig("new-password"); + bumpMtime(); + manager.onConfigChanged("test"); // 模拟监听线程的事件回调(小写验证归一化) + + DruidDataSource second = manager.getDataSource("TEST"); + assertThat(second).isNotSameAs(first); + assertThat(first.isClosed()).isFalse(); // 延迟关闭,在途请求仍可用 + } finally { + manager.closeAll(); + } + } + + private OrgDataSourceManager newManager(Duration refreshInterval, Duration closeDelay) { + OrgDatabaseProperties properties = new OrgDatabaseProperties(); + properties.setConfigDir(configDir); + properties.setRefreshCheckInterval(refreshInterval); + properties.setRetiredPoolCloseDelay(closeDelay); + + OrgDatabaseConfigLoader loader = new OrgDatabaseConfigLoader(); + ReflectionTestUtils.setField(loader, "properties", properties); + loader.init(); + + OrgDataSourceFactory factory = new OrgDataSourceFactory() { + @Override + public DruidDataSource create(String orgId, OrgDatabaseConfig config) { + if (config.password().equals(failPassword)) { + throw new IllegalStateException("模拟新库不可达"); + } + // 测试不真实连接数据库(initialSize=0 时 init 不建物理连接) + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setName(orgId); + dataSource.setUrl(config.url()); + dataSource.setUsername(config.username()); + dataSource.setDriverClassName(config.driver()); + try { + dataSource.init(); + } catch (SQLException exception) { + throw new IllegalStateException(exception); + } + return dataSource; + } + }; + + OrgDataSourceManager manager = new OrgDataSourceManager(); + ReflectionTestUtils.setField(manager, "configLoader", loader); + ReflectionTestUtils.setField(manager, "dataSourceFactory", factory); + ReflectionTestUtils.setField(manager, "properties", properties); + return manager; + } + + private void writeConfig(String password) throws Exception { + Files.writeString(configDir.resolve("TEST.properties"), """ + url=jdbc:sqlserver://127.0.0.1:1433;databaseName=test + username=test + password=%s + """.formatted(password)); + } + + private void bumpMtime() throws Exception { + Files.setLastModifiedTime( + configDir.resolve("TEST.properties"), + FileTime.from(Instant.now().plusSeconds(60)) + ); + } +} diff --git a/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests.java b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests.java new file mode 100644 index 00000000..f304e960 --- /dev/null +++ b/code/fms/fms-api/src/test/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests.java @@ -0,0 +1,138 @@ +package cn.g3soft.fmsapi.database; + +import cn.g3soft.fmsapi.exception.AuthenticationException; +import com.alibaba.druid.pool.DruidDataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.sql.SQLException; +import java.time.Duration; +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OrgDatabaseConfigWatcherTests { + + private static final long AWAIT_MILLIS = 5000; + + @TempDir + Path configDir; + + private OrgDataSourceManager manager; + private OrgDatabaseConfigWatcher watcher; + + @Test + void fileChangeTriggersPoolSwitchWithoutPolling() throws Exception { + start(Duration.ofMinutes(10)); + try { + writeConfig("TEST", "old-password"); + DruidDataSource first = manager.getDataSource("TEST"); + + writeConfig("TEST", "new-password"); + bumpMtime("TEST"); + + DruidDataSource current = first; + long deadline = System.currentTimeMillis() + AWAIT_MILLIS; + while (current == first && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + current = manager.getDataSource("TEST"); + } + assertThat(current).isNotSameAs(first); + assertThat(current.getUsername()).isEqualTo("test"); + } finally { + stop(); + } + } + + @Test + void renamingOrgCodeEvictsOldOrgAndServesNewOne() throws Exception { + start(Duration.ofMinutes(10)); + try { + writeConfig("G3HD", "old-password"); + manager.getDataSource("G3HD"); + + Files.move(configDir.resolve("G3HD.properties"), configDir.resolve("NEWORG.properties")); + + // 删除事件应淘汰旧机构码的池(而不是像改造前一样悄悄继续用旧库) + long deadline = System.currentTimeMillis() + AWAIT_MILLIS; + while (manager.getDataSourceCount() > 0 && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + } + assertThat(manager.getDataSourceCount()).isZero(); + assertThatThrownBy(() -> manager.getDataSource("G3HD")) + .isInstanceOf(AuthenticationException.class); + + // 新机构码走懒加载,正常建池 + assertThat(manager.getDataSource("NEWORG")).isNotNull(); + } finally { + stop(); + } + } + + private void start(Duration refreshInterval) { + OrgDatabaseProperties properties = new OrgDatabaseProperties(); + properties.setConfigDir(configDir); + properties.setRefreshCheckInterval(refreshInterval); + properties.setRetiredPoolCloseDelay(Duration.ofMillis(200)); + + OrgDatabaseConfigLoader loader = new OrgDatabaseConfigLoader(); + ReflectionTestUtils.setField(loader, "properties", properties); + loader.init(); + + OrgDataSourceFactory factory = new OrgDataSourceFactory() { + @Override + public DruidDataSource create(String orgId, OrgDatabaseConfig config) { + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setName(orgId); + dataSource.setUrl(config.url()); + dataSource.setUsername(config.username()); + dataSource.setDriverClassName(config.driver()); + try { + dataSource.init(); + } catch (SQLException exception) { + throw new IllegalStateException(exception); + } + return dataSource; + } + }; + + manager = new OrgDataSourceManager(); + ReflectionTestUtils.setField(manager, "configLoader", loader); + ReflectionTestUtils.setField(manager, "dataSourceFactory", factory); + ReflectionTestUtils.setField(manager, "properties", properties); + + watcher = new OrgDatabaseConfigWatcher(); + ReflectionTestUtils.setField(watcher, "configLoader", loader); + ReflectionTestUtils.setField(watcher, "dataSourceManager", manager); + watcher.start(); + } + + private void stop() { + if (watcher != null) { + watcher.stop(); + } + if (manager != null) { + manager.closeAll(); + } + } + + private void writeConfig(String orgId, String password) throws Exception { + Files.writeString(configDir.resolve(orgId + ".properties"), """ + url=jdbc:sqlserver://127.0.0.1:1433;databaseName=test + username=test + password=%s + """.formatted(password)); + } + + private void bumpMtime(String orgId) throws Exception { + Files.setLastModifiedTime( + configDir.resolve(orgId + ".properties"), + FileTime.from(Instant.now().plusSeconds(60)) + ); + } +} diff --git a/code/fms/fms-api/target/classes/application.yaml b/code/fms/fms-api/target/classes/application.yaml index 91ab78f5..2759dfc7 100644 --- a/code/fms/fms-api/target/classes/application.yaml +++ b/code/fms/fms-api/target/classes/application.yaml @@ -13,8 +13,13 @@ fms: jwt-expiration: 24h database: config-dir: ./config/dbconfigs + # 机构配置文件变更检测周期(文件监听的兜底,正常由监听毫秒级触发;设为 0s 关闭轮询) + refresh-check-interval: 30s + # 配置变更切换后,旧连接池延迟关闭时间,等待在途请求归还连接 + retired-pool-close-delay: 60s pool: - initial-size: 0 + # 建池时先建 1 条物理连接:配置指向不可达的库时快速失败,保留旧池不切换 + initial-size: 1 min-idle: 0 max-active: 10 max-wait: 5000 diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceFactory.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceFactory.class index f9db64e8..b8506ba5 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceFactory.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceFactory.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager$PoolEntry.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager$PoolEntry.class new file mode 100644 index 00000000..a7514698 Binary files /dev/null and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager$PoolEntry.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager.class index d8f8f871..c3c80f8d 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.class index dacb3502..9120d513 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.class new file mode 100644 index 00000000..9c18dc14 Binary files /dev/null and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties$Pool.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties$Pool.class index 04075c46..4caaae3e 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties$Pool.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties$Pool.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties.class index 27a08168..b88b12ec 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties.class differ diff --git a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 702d8937..3248525f 100644 --- a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -2,6 +2,7 @@ cn\g3soft\fmsapi\service\LoginLogService.class cn\g3soft\fmsapi\controller\FileController.class cn\g3soft\fmsapi\utils\ParamUtils.class cn\g3soft\fmsapi\utils\JwtUtils.class +cn\g3soft\fmsapi\database\OrgDataSourceManager$PoolEntry.class cn\g3soft\fmsapi\FmsApiApplication.class cn\g3soft\fmsapi\utils\DbUtils$TableMetadata.class cn\g3soft\fmsapi\utils\JwtUtils$TokenClaims.class @@ -40,6 +41,7 @@ cn\g3soft\fmsapi\utils\DbUtils.class cn\g3soft\fmsapi\exception\SaveObjectException.class cn\g3soft\fmsapi\service\AuthService.class cn\g3soft\fmsapi\config\AuthProperties.class +cn\g3soft\fmsapi\database\OrgDatabaseConfigWatcher.class cn\g3soft\fmsapi\database\OrgRoutingDataSource.class cn\g3soft\fmsapi\utils\ApiResponse.class cn\g3soft\fmsapi\utils\snowflake\core\SnowWorkerM2.class diff --git a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index 38bed198..ff26c2f6 100644 --- a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -8,6 +8,7 @@ D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\controller\FileCont D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\database\OrgContext.java D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\database\OrgDatabaseConfig.java D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\database\OrgDatabaseConfigLoader.java +D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\database\OrgDatabaseConfigWatcher.java D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\database\OrgDatabaseProperties.java D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\database\OrgDataSourceFactory.java D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\database\OrgDataSourceManager.java diff --git a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst index 567edb45..544190ff 100644 --- a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst +++ b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst @@ -1,10 +1,14 @@ -cn\g3soft\fmsapi\database\OrgDatabaseConfigLoaderTests.class -cn\g3soft\fmsapi\utils\snowflake\DefaultIdGeneratorTests.class -cn\g3soft\fmsapi\database\OrgDataSourceFactoryTests.class cn\g3soft\fmsapi\service\AuthServiceTests.class cn\g3soft\fmsapi\utils\JwtUtilsTests.class +cn\g3soft\fmsapi\database\OrgDataSourceManagerTests$1.class +cn\g3soft\fmsapi\service\DataSaveServiceTests$JdbcFixture.class +cn\g3soft\fmsapi\service\DataSaveServiceTests.class +cn\g3soft\fmsapi\database\OrgDatabaseConfigLoaderTests.class +cn\g3soft\fmsapi\utils\snowflake\DefaultIdGeneratorTests.class +cn\g3soft\fmsapi\database\OrgDatabaseConfigWatcherTests$1.class +cn\g3soft\fmsapi\database\OrgDataSourceFactoryTests.class +cn\g3soft\fmsapi\database\OrgDatabaseConfigWatcherTests.class +cn\g3soft\fmsapi\database\OrgDataSourceManagerTests.class cn\g3soft\fmsapi\exception\GlobalExceptionHandlerTests.class cn\g3soft\fmsapi\FmsApiApplicationTests.class -cn\g3soft\fmsapi\service\DataSaveServiceTests$JdbcFixture.class cn\g3soft\fmsapi\service\ActiveSessionRegistryTests.class -cn\g3soft\fmsapi\service\DataSaveServiceTests.class diff --git a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst index 8fe569d9..4ffc47d6 100644 --- a/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst +++ b/code/fms/fms-api/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst @@ -1,5 +1,7 @@ D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\database\OrgDatabaseConfigLoaderTests.java +D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\database\OrgDatabaseConfigWatcherTests.java D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\database\OrgDataSourceFactoryTests.java +D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\database\OrgDataSourceManagerTests.java D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\exception\GlobalExceptionHandlerTests.java D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\FmsApiApplicationTests.java D:\workspace\code\fms\fms-api\src\test\java\cn\g3soft\fmsapi\service\ActiveSessionRegistryTests.java diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T14-29-00_642.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-29-00_642.dumpstream new file mode 100644 index 00000000..585140ba --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-29-00_642.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T14:29:04.429 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T14-30-18_735.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-30-18_735.dumpstream new file mode 100644 index 00000000..613ae82b --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-30-18_735.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T14:30:19.263 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T14-30-47_680.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-30-47_680.dumpstream new file mode 100644 index 00000000..a59c75ac --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-30-47_680.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T14:30:48.153 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T14-31-11_898.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-31-11_898.dumpstream new file mode 100644 index 00000000..a0df49e2 --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-31-11_898.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T14:31:12.371 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T14-53-51_760.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-53-51_760.dumpstream new file mode 100644 index 00000000..f64d613f --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-53-51_760.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T14:53:52.332 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T14-54-41_379.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-54-41_379.dumpstream new file mode 100644 index 00000000..b358d67b --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-54-41_379.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T14:54:41.751 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T14-54-59_867.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-54-59_867.dumpstream new file mode 100644 index 00000000..33eddefb --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T14-54-59_867.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T14:55:00.351 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T15-10-34_347.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T15-10-34_347.dumpstream new file mode 100644 index 00000000..a1a5c89d --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T15-10-34_347.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T15:10:35.538 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T15-14-07_984.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T15-14-07_984.dumpstream new file mode 100644 index 00000000..020367fc --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T15-14-07_984.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T15:14:08.649 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T15-18-38_687.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T15-18-38_687.dumpstream new file mode 100644 index 00000000..34c67e2c --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T15-18-38_687.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T15:18:39.321 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/2026-08-28T15-28-11_237.dumpstream b/code/fms/fms-api/target/surefire-reports/2026-08-28T15-28-11_237.dumpstream new file mode 100644 index 00000000..4cd6e1c3 --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/2026-08-28T15-28-11_237.dumpstream @@ -0,0 +1,5 @@ +# Created at 2026-08-28T15:28:12.719 +Boot Manifest-JAR contains absolute paths in classpath 'D:\workspace\code\fms\fms-api\target\test-classes' +Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true +'other' has different root + diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.FmsApiApplicationTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.FmsApiApplicationTests.xml index 505bb764..79bc274f 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.FmsApiApplicationTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.FmsApiApplicationTests.xml @@ -1,30 +1,30 @@ - + - - + + - + - + - - + + - + - + - - + + @@ -33,44 +33,43 @@ - + - - + + - - - - - + + + + - + - + - - + + - - + - + - - + + - + - + - - + + - + - + - - + + - + - - + + - - - - - + + + + - + - - + + - - - + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDataSourceManagerTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDataSourceManagerTests.xml new file mode 100644 index 00000000..31a1afd5 --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDataSourceManagerTests.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests.xml index 258069e8..38b3719c 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests.xml @@ -1,63 +1,62 @@ - + - - + + - + - + - - + + - + - + - - + + - + - - + + - - - - - + + + + - + - - + + - + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDatabaseConfigWatcherTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDatabaseConfigWatcherTests.xml new file mode 100644 index 00000000..377ee16b --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.database.OrgDatabaseConfigWatcherTests.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests.xml index 2f6072d1..ed3d225c 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests.xml @@ -1,66 +1,65 @@ - + - - + + - + - + - - + + - + - + - - + + - + - - + + - - - - - + + + + - + - - + + - - + - - + - + - - + + - + - + - - + + - + - + - - + + @@ -33,38 +33,37 @@ - + - - + + - - - - - + + + + - + - + - - + + - - + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.AuthServiceTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.AuthServiceTests.xml index 1ff8e3d6..8263ee21 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.AuthServiceTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.AuthServiceTests.xml @@ -1,30 +1,30 @@ - + - - + + - + - + - - + + - + - + - - + + @@ -33,37 +33,36 @@ - + - - + + - - - - - + + + + - + - + - - + + - + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.DataSaveServiceTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.DataSaveServiceTests.xml index 327ee952..581cde61 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.DataSaveServiceTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.service.DataSaveServiceTests.xml @@ -1,30 +1,30 @@ - + - - + + - + - + - - + + - + - + - - + + @@ -33,41 +33,40 @@ - + - - + + - - - - - + + + + - + - + - - + + - - - - - + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml index cc6c9c6f..d455d8dd 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.JwtUtilsTests.xml @@ -1,30 +1,30 @@ - + - - + + - + - + - - + + - + - + - - + + @@ -33,37 +33,36 @@ - + - - + + - - - - - + + + + - + - + - - + + - + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests.xml b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests.xml index f786d7db..fa912327 100644 --- a/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests.xml +++ b/code/fms/fms-api/target/surefire-reports/TEST-cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests.xml @@ -1,30 +1,30 @@ - + - - + + - + - + - - + + - + - + - - + + @@ -33,42 +33,41 @@ - + - - + + - - - - - + + + + - + - + - - + + - - - - - - + + + + + + \ No newline at end of file diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.FmsApiApplicationTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.FmsApiApplicationTests.txt index 4530b8a7..7a2d0b43 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.FmsApiApplicationTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.FmsApiApplicationTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.FmsApiApplicationTests ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.644 s -- in cn.g3soft.fmsapi.FmsApiApplicationTests +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 33.85 s -- in cn.g3soft.fmsapi.FmsApiApplicationTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests.txt index 54dd7516..79efa0bb 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests.txt @@ -1,11 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests ------------------------------------------------------------------------------- -Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.145 s <<< FAILURE! -- in cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests -cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests.databaseConnectionFailureDoesNotRetry -- Time elapsed: 0.144 s <<< FAILURE! -org.opentest4j.AssertionFailedError: - -expected: 0 - but was: 3 - at cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests.databaseConnectionFailureDoesNotRetry(OrgDataSourceFactoryTests.java:25) - +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.050 s -- in cn.g3soft.fmsapi.database.OrgDataSourceFactoryTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDataSourceManagerTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDataSourceManagerTests.txt new file mode 100644 index 00000000..d3783dec --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDataSourceManagerTests.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: cn.g3soft.fmsapi.database.OrgDataSourceManagerTests +------------------------------------------------------------------------------- +Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.608 s -- in cn.g3soft.fmsapi.database.OrgDataSourceManagerTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests.txt index d03a4e93..5154356d 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.139 s -- in cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.646 s -- in cn.g3soft.fmsapi.database.OrgDatabaseConfigLoaderTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDatabaseConfigWatcherTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDatabaseConfigWatcherTests.txt new file mode 100644 index 00000000..c706cc83 --- /dev/null +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.database.OrgDatabaseConfigWatcherTests.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: cn.g3soft.fmsapi.database.OrgDatabaseConfigWatcherTests +------------------------------------------------------------------------------- +Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.088 s -- in cn.g3soft.fmsapi.database.OrgDatabaseConfigWatcherTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests.txt index 2d971bcc..fb2119ff 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests ------------------------------------------------------------------------------- -Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.014 s -- in cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests +Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.251 s -- in cn.g3soft.fmsapi.exception.GlobalExceptionHandlerTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.txt index a8084c22..f35ef334 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.ActiveSessionRegistryTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.service.ActiveSessionRegistryTests ------------------------------------------------------------------------------- -Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.003 s -- in cn.g3soft.fmsapi.service.ActiveSessionRegistryTests +Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.017 s -- in cn.g3soft.fmsapi.service.ActiveSessionRegistryTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.AuthServiceTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.AuthServiceTests.txt index d7686c52..0be27a90 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.AuthServiceTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.AuthServiceTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.service.AuthServiceTests ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.201 s -- in cn.g3soft.fmsapi.service.AuthServiceTests +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.118 s -- in cn.g3soft.fmsapi.service.AuthServiceTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.DataSaveServiceTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.DataSaveServiceTests.txt index 55cee72a..4b7c089b 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.DataSaveServiceTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.service.DataSaveServiceTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.service.DataSaveServiceTests ------------------------------------------------------------------------------- -Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.434 s -- in cn.g3soft.fmsapi.service.DataSaveServiceTests +Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.593 s -- in cn.g3soft.fmsapi.service.DataSaveServiceTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.JwtUtilsTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.JwtUtilsTests.txt index 42f67e43..d27834dc 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.JwtUtilsTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.JwtUtilsTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.utils.JwtUtilsTests ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.114 s -- in cn.g3soft.fmsapi.utils.JwtUtilsTests +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.991 s -- in cn.g3soft.fmsapi.utils.JwtUtilsTests diff --git a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests.txt b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests.txt index 45dec5f6..e13571f4 100644 --- a/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests.txt +++ b/code/fms/fms-api/target/surefire-reports/cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests ------------------------------------------------------------------------------- -Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.633 s -- in cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests +Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.258 s -- in cn.g3soft.fmsapi.utils.snowflake.DefaultIdGeneratorTests diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests$1.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests$1.class new file mode 100644 index 00000000..87d0c86e Binary files /dev/null and b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests$1.class differ diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests.class new file mode 100644 index 00000000..cae9c258 Binary files /dev/null and b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDataSourceManagerTests.class differ diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests$1.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests$1.class new file mode 100644 index 00000000..0c37f74f Binary files /dev/null and b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests$1.class differ diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests.class new file mode 100644 index 00000000..f6a9a417 Binary files /dev/null and b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcherTests.class differ diff --git a/code/fms/fms-vue/src/layouts/DefaultLayout.vue b/code/fms/fms-vue/src/layouts/DefaultLayout.vue index e72ba990..abf1a042 100644 --- a/code/fms/fms-vue/src/layouts/DefaultLayout.vue +++ b/code/fms/fms-vue/src/layouts/DefaultLayout.vue @@ -1,5 +1,5 @@