|
|
@@ -3,14 +3,17 @@ package com.fs.framework.liquibase;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.factory.InitializingBean;
|
|
|
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
|
|
+import org.springframework.context.ApplicationContext;
|
|
|
import org.springframework.core.env.Environment;
|
|
|
import org.springframework.util.ClassUtils;
|
|
|
import org.springframework.util.StringUtils;
|
|
|
|
|
|
import javax.sql.DataSource;
|
|
|
+import java.util.List;
|
|
|
|
|
|
/**
|
|
|
- * 应用启动门禁:校验 master 数据源、执行 pending migration、校验 changeset 执行记录、同步 bundle MD5。
|
|
|
+ * 应用启动门禁:按目标检查数据源 → 检查 changelog 文件 → 执行 pending migration → 同步 bundle MD5。
|
|
|
*/
|
|
|
public class LiquibaseStartupGate implements InitializingBean {
|
|
|
|
|
|
@@ -18,14 +21,14 @@ public class LiquibaseStartupGate implements InitializingBean {
|
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(LiquibaseStartupGate.class);
|
|
|
|
|
|
- private final DataSource masterDataSource;
|
|
|
+ private final ApplicationContext applicationContext;
|
|
|
private final LiquibaseProperties properties;
|
|
|
private final Environment environment;
|
|
|
|
|
|
- public LiquibaseStartupGate(DataSource masterDataSource,
|
|
|
+ public LiquibaseStartupGate(ApplicationContext applicationContext,
|
|
|
LiquibaseProperties properties,
|
|
|
Environment environment) {
|
|
|
- this.masterDataSource = masterDataSource;
|
|
|
+ this.applicationContext = applicationContext;
|
|
|
this.properties = properties;
|
|
|
this.environment = environment;
|
|
|
}
|
|
|
@@ -43,89 +46,186 @@ public class LiquibaseStartupGate implements InitializingBean {
|
|
|
|
|
|
assertLiquibaseOnClasspath();
|
|
|
|
|
|
- MasterDataSourceValidator masterDataSourceValidator = new MasterDataSourceValidator();
|
|
|
+ DataSourceAvailabilityValidator dataSourceValidator = new DataSourceAvailabilityValidator();
|
|
|
ChangelogMd5Service changelogMd5Service = new ChangelogMd5Service(properties);
|
|
|
LiquibaseMetaRepository liquibaseMetaRepository = new LiquibaseMetaRepository(properties);
|
|
|
LiquibaseMigrationRunner liquibaseMigrationRunner = new LiquibaseMigrationRunner(properties);
|
|
|
String moduleName = environment.getProperty("spring.application.name", "unknown-module");
|
|
|
- log.info("模块 {} 开始 Liquibase 启动门禁校验(优先于业务 Bean 初始化)", moduleName);
|
|
|
|
|
|
- masterDataSourceValidator.assertMasterExists(masterDataSource);
|
|
|
+ List<LiquibaseProperties.LiquibaseTarget> targets = properties.resolveTargets();
|
|
|
+ log.info("模块 {} 开始 Liquibase 启动门禁,共 {} 个目标: {}",
|
|
|
+ moduleName, targets.size(), summarizeTargets(targets));
|
|
|
|
|
|
- ChangelogMd5Service.BundleMd5Result bundleMd5Result = changelogMd5Service.computeBundleMd5();
|
|
|
- String fileMd5 = bundleMd5Result.getMd5();
|
|
|
-
|
|
|
- boolean metaTableExists = liquibaseMetaRepository.metaTableExists(masterDataSource);
|
|
|
- String dbMd5 = liquibaseMetaRepository.loadBundleMd5(masterDataSource);
|
|
|
- LiquibaseMigrationRunner.MigrationAudit audit = liquibaseMigrationRunner.audit(masterDataSource);
|
|
|
- boolean md5Mismatch = StringUtils.hasText(dbMd5) && !fileMd5.equals(dbMd5);
|
|
|
+ for (LiquibaseProperties.LiquibaseTarget target : targets) {
|
|
|
+ runTarget(target, moduleName, dataSourceValidator, changelogMd5Service,
|
|
|
+ liquibaseMetaRepository, liquibaseMigrationRunner);
|
|
|
+ }
|
|
|
|
|
|
- assertChangeSetsParsable(audit);
|
|
|
+ log.info("模块 {} Liquibase 全部目标门禁完成", moduleName);
|
|
|
+ }
|
|
|
|
|
|
- if (audit.getPendingTotal() > 0) {
|
|
|
- log.info("检测到 {} 条未执行 Liquibase changeset", audit.getPendingTotal());
|
|
|
- }
|
|
|
- if (!audit.getMissingInDatabase().isEmpty()) {
|
|
|
- log.warn("检测到 {} 条 changeset 未写入 DATABASECHANGELOG", audit.getMissingInDatabase().size());
|
|
|
+ private void runTarget(LiquibaseProperties.LiquibaseTarget target,
|
|
|
+ String moduleName,
|
|
|
+ DataSourceAvailabilityValidator dataSourceValidator,
|
|
|
+ ChangelogMd5Service changelogMd5Service,
|
|
|
+ LiquibaseMetaRepository liquibaseMetaRepository,
|
|
|
+ LiquibaseMigrationRunner liquibaseMigrationRunner) {
|
|
|
+ log.info("Liquibase 目标 [{}] 开始: dataSource={}, changelog={}",
|
|
|
+ target.getName(), target.getDataSource(), target.getChangelog());
|
|
|
+
|
|
|
+ DataSource dataSource = resolveDataSource(target);
|
|
|
+ if (dataSource == null) {
|
|
|
+ return;
|
|
|
}
|
|
|
- if (md5Mismatch) {
|
|
|
- log.info("changelog bundle MD5 已变化: dbMd5={}, fileMd5={}", dbMd5, fileMd5);
|
|
|
+
|
|
|
+ try {
|
|
|
+ dataSourceValidator.assertAvailable(dataSource, target.getName(), target.getDataSource());
|
|
|
+ } catch (IllegalStateException ex) {
|
|
|
+ handleTargetFailure(target, ex.getMessage(), ex);
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- boolean needUpdate = audit.getPendingTotal() > 0
|
|
|
- || !audit.getMissingInDatabase().isEmpty()
|
|
|
- || !metaTableExists
|
|
|
- || !StringUtils.hasText(dbMd5)
|
|
|
- || md5Mismatch;
|
|
|
-
|
|
|
- if (needUpdate) {
|
|
|
- liquibaseMigrationRunner.update(masterDataSource);
|
|
|
- bootstrapMetaIfNeeded(liquibaseMetaRepository);
|
|
|
- audit = liquibaseMigrationRunner.audit(masterDataSource);
|
|
|
- assertMigrationCompleted(audit);
|
|
|
- } else {
|
|
|
- assertMigrationCompleted(audit);
|
|
|
+ if (!changelogFileExists(target.getChangelog())) {
|
|
|
+ handleTargetFailure(target, "未找到 changelog 文件: " + target.getChangelog(), null);
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- ChangelogMd5Service.BundleMd5Result latestMd5 = changelogMd5Service.computeBundleMd5();
|
|
|
- liquibaseMetaRepository.saveBundleMd5(
|
|
|
- masterDataSource, latestMd5.getMd5(), latestMd5.getFileCount(), moduleName);
|
|
|
+ try {
|
|
|
+ ChangelogMd5Service.BundleMd5Result bundleMd5Result = changelogMd5Service.computeBundleMd5(target);
|
|
|
+ String fileMd5 = bundleMd5Result.getMd5();
|
|
|
+
|
|
|
+ boolean metaTableExists = liquibaseMetaRepository.metaTableExists(dataSource);
|
|
|
+ String dbMd5 = liquibaseMetaRepository.loadBundleMd5(dataSource);
|
|
|
+ LiquibaseMigrationRunner.MigrationAudit audit =
|
|
|
+ liquibaseMigrationRunner.audit(dataSource, target.getChangelog());
|
|
|
+ boolean md5Mismatch = StringUtils.hasText(dbMd5) && !fileMd5.equals(dbMd5);
|
|
|
+
|
|
|
+ assertChangeSetsParsable(target, audit);
|
|
|
+
|
|
|
+ if (audit.getPendingTotal() > 0) {
|
|
|
+ log.info("目标 [{}] 检测到 {} 条未执行 Liquibase changeset",
|
|
|
+ target.getName(), audit.getPendingTotal());
|
|
|
+ }
|
|
|
+ if (!audit.getMissingInDatabase().isEmpty()) {
|
|
|
+ log.warn("目标 [{}] 检测到 {} 条 changeset 未写入 DATABASECHANGELOG",
|
|
|
+ target.getName(), audit.getMissingInDatabase().size());
|
|
|
+ }
|
|
|
+ if (md5Mismatch) {
|
|
|
+ log.info("目标 [{}] changelog bundle MD5 已变化: dbMd5={}, fileMd5={}",
|
|
|
+ target.getName(), dbMd5, fileMd5);
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean needUpdate = audit.getPendingTotal() > 0
|
|
|
+ || !audit.getMissingInDatabase().isEmpty()
|
|
|
+ || !metaTableExists
|
|
|
+ || !StringUtils.hasText(dbMd5)
|
|
|
+ || md5Mismatch;
|
|
|
+
|
|
|
+ if (needUpdate) {
|
|
|
+ liquibaseMigrationRunner.update(dataSource, target.getChangelog());
|
|
|
+ bootstrapMetaIfNeeded(liquibaseMetaRepository, dataSource);
|
|
|
+ audit = liquibaseMigrationRunner.audit(dataSource, target.getChangelog());
|
|
|
+ assertMigrationCompleted(target, audit);
|
|
|
+ } else {
|
|
|
+ assertMigrationCompleted(target, audit);
|
|
|
+ }
|
|
|
+
|
|
|
+ ChangelogMd5Service.BundleMd5Result latestMd5 = changelogMd5Service.computeBundleMd5(target);
|
|
|
+ liquibaseMetaRepository.saveBundleMd5(
|
|
|
+ dataSource, latestMd5.getMd5(), latestMd5.getFileCount(),
|
|
|
+ moduleName + "#" + target.getName());
|
|
|
+
|
|
|
+ if (md5Mismatch) {
|
|
|
+ log.info("目标 [{}] changelog 文件变更且 migration 校验通过,bundle MD5 已同步: {} -> {}",
|
|
|
+ target.getName(), dbMd5, latestMd5.getMd5());
|
|
|
+ } else {
|
|
|
+ log.info("目标 [{}] Liquibase 启动门禁通过,bundle MD5={}",
|
|
|
+ target.getName(), latestMd5.getMd5());
|
|
|
+ }
|
|
|
+ } catch (RuntimeException ex) {
|
|
|
+ handleTargetFailure(target, ex.getMessage(), ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (md5Mismatch) {
|
|
|
- log.info("changelog 文件变更且 migration 校验通过,bundle MD5 已同步: {} -> {}",
|
|
|
- dbMd5, latestMd5.getMd5());
|
|
|
- } else {
|
|
|
- log.info("模块 {} Liquibase 启动门禁通过,bundle MD5={}", moduleName, latestMd5.getMd5());
|
|
|
+ private DataSource resolveDataSource(LiquibaseProperties.LiquibaseTarget target) {
|
|
|
+ try {
|
|
|
+ return applicationContext.getBean(target.getDataSource(), DataSource.class);
|
|
|
+ } catch (NoSuchBeanDefinitionException ex) {
|
|
|
+ String message = "数据源 Bean 不存在: " + target.getDataSource();
|
|
|
+ if (!target.isRequired()) {
|
|
|
+ log.info("目标 [{}] {},required=false,跳过", target.getName(), message);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ handleTargetFailure(target, message, ex);
|
|
|
+ return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void assertChangeSetsParsable(LiquibaseMigrationRunner.MigrationAudit audit) {
|
|
|
+ private boolean changelogFileExists(String changelog) {
|
|
|
+ ClassLoader classLoader = getClass().getClassLoader();
|
|
|
+ return classLoader != null && classLoader.getResource(changelog) != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assertChangeSetsParsable(LiquibaseProperties.LiquibaseTarget target,
|
|
|
+ LiquibaseMigrationRunner.MigrationAudit audit) {
|
|
|
if (audit.getParsedTotal() == 0) {
|
|
|
- abort("Liquibase 未解析到任何 changeset,请确认 master changelog 使用 XML include 且已正确引用 SQL 文件");
|
|
|
+ abort(target, "Liquibase 未解析到任何 changeset,请确认 master changelog 使用 XML include 且已正确引用 SQL 文件");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void assertMigrationCompleted(LiquibaseMigrationRunner.MigrationAudit audit) {
|
|
|
+ private void assertMigrationCompleted(LiquibaseProperties.LiquibaseTarget target,
|
|
|
+ LiquibaseMigrationRunner.MigrationAudit audit) {
|
|
|
if (audit.getPendingTotal() > 0) {
|
|
|
- abort("Liquibase update 后仍有 " + audit.getPendingTotal() + " 条未执行 changeset: "
|
|
|
+ abort(target, "Liquibase update 后仍有 " + audit.getPendingTotal() + " 条未执行 changeset: "
|
|
|
+ audit.getPendingKeys());
|
|
|
}
|
|
|
if (!audit.getMissingInDatabase().isEmpty()) {
|
|
|
- abort("以下 changeset 未写入 DATABASECHANGELOG: " + audit.getMissingInDatabase());
|
|
|
+ abort(target, "以下 changeset 未写入 DATABASECHANGELOG: " + audit.getMissingInDatabase());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleTargetFailure(LiquibaseProperties.LiquibaseTarget target,
|
|
|
+ String message,
|
|
|
+ Throwable cause) {
|
|
|
+ String full = "Liquibase 目标 [" + target.getName() + "] 失败: " + message;
|
|
|
+ if (target.isRequired() && properties.isFailFast()) {
|
|
|
+ if (cause instanceof IllegalStateException) {
|
|
|
+ throw (IllegalStateException) cause;
|
|
|
+ }
|
|
|
+ throw new IllegalStateException(full, cause);
|
|
|
+ }
|
|
|
+ if (target.isRequired() && !properties.isFailFast()) {
|
|
|
+ log.error("Liquibase 启动门禁校验失败(fail-fast=false): {}", full, cause);
|
|
|
+ return;
|
|
|
}
|
|
|
+ log.error("Liquibase 目标 [{}] 失败(required=false,跳过): {}", target.getName(), message, cause);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void abort(LiquibaseProperties.LiquibaseTarget target, String message) {
|
|
|
+ throw new IllegalStateException("Liquibase 目标 [" + target.getName() + "] " + message);
|
|
|
}
|
|
|
|
|
|
- private void abort(String message) {
|
|
|
- if (properties.isFailFast()) {
|
|
|
- throw new IllegalStateException(message);
|
|
|
+ private void bootstrapMetaIfNeeded(LiquibaseMetaRepository liquibaseMetaRepository,
|
|
|
+ DataSource dataSource) {
|
|
|
+ if (!liquibaseMetaRepository.metaTableExists(dataSource)) {
|
|
|
+ liquibaseMetaRepository.ensureMetaTable(dataSource);
|
|
|
}
|
|
|
- log.error("Liquibase 启动门禁校验失败(fail-fast=false): {}", message);
|
|
|
}
|
|
|
|
|
|
- private void bootstrapMetaIfNeeded(LiquibaseMetaRepository liquibaseMetaRepository) {
|
|
|
- if (!liquibaseMetaRepository.metaTableExists(masterDataSource)) {
|
|
|
- liquibaseMetaRepository.ensureMetaTable(masterDataSource);
|
|
|
+ private String summarizeTargets(List<LiquibaseProperties.LiquibaseTarget> targets) {
|
|
|
+ StringBuilder builder = new StringBuilder();
|
|
|
+ for (int i = 0; i < targets.size(); i++) {
|
|
|
+ if (i > 0) {
|
|
|
+ builder.append(", ");
|
|
|
+ }
|
|
|
+ LiquibaseProperties.LiquibaseTarget target = targets.get(i);
|
|
|
+ builder.append(target.getName())
|
|
|
+ .append('(')
|
|
|
+ .append(target.getDataSource())
|
|
|
+ .append(target.isRequired() ? ",required" : ",optional")
|
|
|
+ .append(')');
|
|
|
}
|
|
|
+ return builder.toString();
|
|
|
}
|
|
|
|
|
|
private void assertLiquibaseOnClasspath() {
|