|
@@ -1,25 +1,26 @@
|
|
|
package com.qmth.boot.data.upgrade.config;
|
|
|
|
|
|
import com.qmth.boot.data.upgrade.annotation.DataUpgradeVersion;
|
|
|
+import com.qmth.boot.data.upgrade.model.BootAppInfo;
|
|
|
+import com.qmth.boot.data.upgrade.service.DataInitService;
|
|
|
import com.qmth.boot.data.upgrade.service.DataUpgradeService;
|
|
|
+import com.qmth.boot.data.upgrade.utils.ClassHelper;
|
|
|
import com.qmth.boot.data.upgrade.utils.VersionComparator;
|
|
|
import com.zaxxer.hikari.HikariDataSource;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.boot.context.event.ApplicationPreparedEvent;
|
|
|
-import org.springframework.boot.jdbc.DatabaseDriver;
|
|
|
import org.springframework.context.ApplicationListener;
|
|
|
-import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
|
|
-import org.springframework.core.env.ConfigurableEnvironment;
|
|
|
-import org.springframework.core.type.filter.AssignableTypeFilter;
|
|
|
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
|
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
|
|
+import org.springframework.transaction.TransactionStatus;
|
|
|
+import org.springframework.transaction.support.TransactionCallbackWithoutResult;
|
|
|
import org.springframework.transaction.support.TransactionTemplate;
|
|
|
import org.springframework.util.ClassUtils;
|
|
|
|
|
|
import java.lang.reflect.Constructor;
|
|
|
-import java.lang.reflect.Modifier;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
@@ -31,78 +32,140 @@ import java.util.Map;
|
|
|
* @author: Deason
|
|
|
* @since: 2025/3/3
|
|
|
*/
|
|
|
-public class DataUpgradeListener implements ApplicationListener<ApplicationPreparedEvent> {
|
|
|
+public class DataUpgradeListener implements ApplicationListener<ApplicationPreparedEvent>, DataUpgradeConstants {
|
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(DataUpgradeListener.class);
|
|
|
|
|
|
- private static final String DB_URL = "com.qmth.datasource.url";
|
|
|
+ private DataUpgradeProperties properties;
|
|
|
|
|
|
- private static final String DB_USERNAME = "com.qmth.datasource.username";
|
|
|
+ @Override
|
|
|
+ public void onApplicationEvent(ApplicationPreparedEvent event) {
|
|
|
+ try {
|
|
|
+ properties = new DataUpgradeProperties(event.getApplicationContext().getEnvironment());
|
|
|
+ if (!properties.getEnable()) {
|
|
|
+ // 未启用时,不再进行后续处理
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- private static final String DB_PASSWORD = "com.qmth.datasource.password";
|
|
|
+ if (properties.getFirstInstall()) {
|
|
|
+ // 首次安装时,仅执行数据初始化
|
|
|
+ this.handlerDataInit();
|
|
|
+ } else {
|
|
|
+ // 否则执行数据升级
|
|
|
+ this.handlerDataUpgrade();
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("数据操作异常终止!原因:{}", e.getMessage(), e);
|
|
|
+ System.exit(1);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- private static final String APP_VERSION = "com.qmth.solar.app-version";
|
|
|
+ public void handlerDataInit() {
|
|
|
+ try (HikariDataSource dataSource = this.initDataSource()) {
|
|
|
+ JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
|
|
|
|
|
|
- private static final String BASE_PACKAGE = "com.qmth.data.upgrade.base-package";
|
|
|
+ log.warn("待初始化应用码:{} 待初始化应用版本号:{}", properties.getAppCode(), properties.getAppVersion());
|
|
|
+ if (this.existBootAppInfo(jdbcTemplate)) {
|
|
|
+ throw new RuntimeException("【应用信息表】已存在,不允许重复数据初始化!");
|
|
|
+ }
|
|
|
|
|
|
- @Override
|
|
|
- public void onApplicationEvent(ApplicationPreparedEvent event) {
|
|
|
- ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
|
|
|
- try (HikariDataSource dataSource = this.initDataSource(environment)) {
|
|
|
+ List<DataInitService> services = ClassHelper.findInterfaceInstances(DataInitService.class, properties.getBasePackage());
|
|
|
+ if (services.isEmpty()) {
|
|
|
+ log.warn("未找到数据初始化的具体实现!{}:{}", BASE_PACKAGE, properties.getBasePackage());
|
|
|
+ throw new RuntimeException("未找到数据初始化的具体实现!");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (services.size() > 1) {
|
|
|
+ throw new RuntimeException("不允许存在多个数据初始化的具体实现!");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.warn("*************** data init start ***************");
|
|
|
+ try {
|
|
|
+ services.get(0).process(jdbcTemplate);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("SQL语句执行异常!", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 数据初始化完成后,初始应用信息表
|
|
|
+ this.initBootAppInfo(jdbcTemplate, properties.getAppCode(), properties.getAppVersion());
|
|
|
+ log.warn("*************** data init finish ***************");
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void handlerDataUpgrade() {
|
|
|
+ try (HikariDataSource dataSource = this.initDataSource()) {
|
|
|
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
|
|
|
TransactionTemplate transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
|
|
|
|
|
|
- String appVersion = environment.getProperty(APP_VERSION, String.class);
|
|
|
- if (StringUtils.isBlank(appVersion)) {
|
|
|
- throw new RuntimeException(APP_VERSION + " 配置值不能为空!");
|
|
|
+ if (!this.existBootAppInfo(jdbcTemplate)) {
|
|
|
+ throw new RuntimeException("【应用信息表】不存在!");
|
|
|
+ }
|
|
|
+
|
|
|
+ BootAppInfo appInfo = this.queryBootAppInfo(jdbcTemplate, properties.getAppCode());
|
|
|
+ if (appInfo == null) {
|
|
|
+ throw new RuntimeException("【应用信息表】中未找到匹配的数据!appCode:" + properties.getAppCode());
|
|
|
}
|
|
|
|
|
|
- // 检查当前版本是否需要数据升级
|
|
|
- String currentVersion = this.queryCurrentVersion(jdbcTemplate);
|
|
|
- if (new VersionComparator().compare(currentVersion, appVersion) >= 0) {
|
|
|
- log.warn("跳过数据升级!currentVersion:{} appVersion:{}", currentVersion, appVersion);
|
|
|
+ log.warn("待升级应用码:{} 待升级应用版本号:{} 当前应用码:{} 当前应用版本号:{}", properties.getAppCode(),
|
|
|
+ properties.getAppVersion(), appInfo.getAppCode(), appInfo.getAppVersion());
|
|
|
+
|
|
|
+ int compareValue = new VersionComparator().compare(appInfo.getAppVersion(), properties.getAppVersion());
|
|
|
+ if (compareValue > 0) {
|
|
|
+ throw new RuntimeException("待升级的版本号不能低于当前版本号!");
|
|
|
+ }
|
|
|
+ if (compareValue == 0) {
|
|
|
+ log.warn("跳过数据升级,版本号一致!");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- String basePackage = environment.getProperty(BASE_PACKAGE, String.class);
|
|
|
- if (StringUtils.isBlank(basePackage)) {
|
|
|
- basePackage = "com.qmth";// 未配置扫描包名,赋默认值
|
|
|
+ Map<String, DataUpgradeService> serviceMaps = this.matchServices(appInfo.getAppVersion(), properties.getAppVersion());
|
|
|
+ if (serviceMaps.isEmpty()) {
|
|
|
+ log.warn("未找到数据升级的具体实现!{}:{}", BASE_PACKAGE, properties.getBasePackage());
|
|
|
+ throw new RuntimeException("未找到数据升级的具体实现!");
|
|
|
}
|
|
|
|
|
|
- List<DataUpgradeService> services = this.matchServices(basePackage, currentVersion, appVersion);
|
|
|
- if (!services.isEmpty()) {
|
|
|
- log.warn("*************** data upgrade start ***************");
|
|
|
- for (DataUpgradeService service : services) {
|
|
|
- service.process(jdbcTemplate, transactionTemplate);
|
|
|
+ // 按版本号由小到大排序
|
|
|
+ List<String> versionKeys = new ArrayList<>(serviceMaps.keySet());
|
|
|
+ versionKeys.sort(new VersionComparator());
|
|
|
+
|
|
|
+ transactionTemplate.execute(new TransactionCallbackWithoutResult() {
|
|
|
+ @Override
|
|
|
+ protected void doInTransactionWithoutResult(TransactionStatus status) {
|
|
|
+ try {
|
|
|
+ for (String version : versionKeys) {
|
|
|
+ log.warn("*************** data upgrade version:{} start ***************", version);
|
|
|
+ serviceMaps.get(version).process(jdbcTemplate);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ // DML-SQL语句支持事务,执行失败时触发自动回滚。
|
|
|
+ status.setRollbackOnly();
|
|
|
+ // DDL-SQL语句不支持事务,执行失败时请采用其它补偿机制回滚!!!
|
|
|
+ throw new RuntimeException("SQL语句执行异常!", e);
|
|
|
+ }
|
|
|
}
|
|
|
- } else {
|
|
|
- log.warn("未找到数据升级的具体实现!{}:{}", BASE_PACKAGE, basePackage);
|
|
|
- }
|
|
|
+ });
|
|
|
|
|
|
- // 数据升级完成后修改为当前版本号
|
|
|
- updateCurrentVersion(appVersion, jdbcTemplate);
|
|
|
+ // 数据升级完成后,更新应用信息表
|
|
|
+ this.updateBootAppInfo(jdbcTemplate, properties.getAppCode(), properties.getAppVersion());
|
|
|
log.warn("*************** data upgrade finish ***************");
|
|
|
} catch (Exception e) {
|
|
|
- log.error("数据升级异常,终止运行。{}", e.getMessage(), e);
|
|
|
- System.exit(1);
|
|
|
+ throw new RuntimeException(e.getMessage(), e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取数据升级的具体实现(按版本号由小到大排序、支持跨版本)
|
|
|
- * 1、出现同个版本的重复实现,则异常终止
|
|
|
- * 2、未扫描到任何实现 或 所有实现都与待升级目标版本的版本区间不匹配,则忽略
|
|
|
+ * 获取数据升级的具体实现集合
|
|
|
*/
|
|
|
- private List<DataUpgradeService> matchServices(String basePackage, String currentVersion, String appVersion) {
|
|
|
- List<Class<?>> implClasses = this.findInterfaceImpls(DataUpgradeService.class, basePackage);
|
|
|
+ private Map<String, DataUpgradeService> matchServices(String currentVersion, String appVersion) {
|
|
|
+ List<Class<?>> implClasses = ClassHelper.findInterfaceImpls(DataUpgradeService.class, properties.getBasePackage());
|
|
|
if (implClasses.isEmpty()) {
|
|
|
- return new ArrayList<>();
|
|
|
+ return new HashMap<>();
|
|
|
}
|
|
|
|
|
|
VersionComparator vc = new VersionComparator();
|
|
|
- List<String> keys = new ArrayList<>();
|
|
|
Map<String, DataUpgradeService> serviceMaps = new HashMap<>();
|
|
|
-
|
|
|
for (Class<?> clazz : implClasses) {
|
|
|
DataUpgradeVersion annotation = clazz.getAnnotation(DataUpgradeVersion.class);
|
|
|
if (annotation == null) {
|
|
@@ -110,100 +173,91 @@ public class DataUpgradeListener implements ApplicationListener<ApplicationPrepa
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- String value = annotation.value();
|
|
|
- if (vc.compare(value, currentVersion) > 0 && vc.compare(value, appVersion) <= 0) {
|
|
|
- // log.info("{} {}", clazz.getSimpleName(), value);
|
|
|
- if (serviceMaps.containsKey(value)) {
|
|
|
- throw new RuntimeException("同个版本数据升级的实现出现重复!@DataUpgradeVersion:" + value);
|
|
|
+ String implVersion = annotation.value();
|
|
|
+ // 校验规则:只允许x.x.x格式的版本号 todo
|
|
|
+
|
|
|
+ if (vc.compare(implVersion, currentVersion) > 0 && vc.compare(implVersion, appVersion) <= 0) {
|
|
|
+ // log.info("{} {}", clazz.getSimpleName(), implVersion);
|
|
|
+ if (serviceMaps.containsKey(implVersion)) {
|
|
|
+ throw new RuntimeException("同个版本数据升级的实现出现重复!@DataUpgradeVersion:" + implVersion);
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
Constructor<?> constructor = ClassUtils.getConstructorIfAvailable(clazz);
|
|
|
if (constructor != null) {
|
|
|
- serviceMaps.put(value, (DataUpgradeService) constructor.newInstance());
|
|
|
- keys.add(value);
|
|
|
+ serviceMaps.put(implVersion, (DataUpgradeService) constructor.newInstance());
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
throw new RuntimeException(e);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ return serviceMaps;
|
|
|
+ }
|
|
|
|
|
|
- List<DataUpgradeService> result = new ArrayList<>();
|
|
|
- if (!keys.isEmpty()) {
|
|
|
- // 按版本号由小到大排序
|
|
|
- keys.sort(vc);
|
|
|
- for (String key : keys) {
|
|
|
- result.add(serviceMaps.get(key));
|
|
|
- }
|
|
|
+ private void updateBootAppInfo(JdbcTemplate jdbcTemplate, String appCode, String appVersion) {
|
|
|
+ if (StringUtils.isBlank(appCode)) {
|
|
|
+ throw new RuntimeException("appCode的值不能为空!");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(appVersion)) {
|
|
|
+ throw new RuntimeException("appVersion的值不能为空!");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ jdbcTemplate.update("update boot_app_info set app_version = ? where app_code = ?", appVersion, appCode);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
}
|
|
|
- return result;
|
|
|
}
|
|
|
|
|
|
- private String queryCurrentVersion(JdbcTemplate jdbcTemplate) {
|
|
|
+ private BootAppInfo queryBootAppInfo(JdbcTemplate jdbcTemplate, String appCode) {
|
|
|
try {
|
|
|
- jdbcTemplate.execute("create table if not exists app_version (current_version varchar(10) not null, unique key(current_version))");
|
|
|
- String currentVersion = jdbcTemplate.queryForObject("select max(current_version) from app_version", String.class);
|
|
|
- // 未定义时,默认值为0,即最小版本号从0开始
|
|
|
- return StringUtils.isNotBlank(currentVersion) ? currentVersion : "0";
|
|
|
+ List<BootAppInfo> list = jdbcTemplate.query("select app_code,app_version from boot_app_info where app_code = ?",
|
|
|
+ new String[]{appCode}, new BeanPropertyRowMapper<>(BootAppInfo.class));
|
|
|
+ return list.isEmpty() ? null : list.get(0);
|
|
|
} catch (Exception e) {
|
|
|
throw new RuntimeException(e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void updateCurrentVersion(String appVersion, JdbcTemplate jdbcTemplate) {
|
|
|
+ private void initBootAppInfo(JdbcTemplate jdbcTemplate, String appCode, String appVersion) {
|
|
|
+ if (StringUtils.isBlank(appCode)) {
|
|
|
+ throw new RuntimeException("appCode的值不能为空!");
|
|
|
+ }
|
|
|
if (StringUtils.isBlank(appVersion)) {
|
|
|
throw new RuntimeException("appVersion的值不能为空!");
|
|
|
}
|
|
|
-
|
|
|
try {
|
|
|
- jdbcTemplate.update("replace into app_version (current_version) values (?)", appVersion);
|
|
|
- jdbcTemplate.update("delete from app_version where current_version != ?", appVersion);
|
|
|
+ String createTableSql = "create table if not exists boot_app_info (" +
|
|
|
+ " app_code varchar(20) not null," +
|
|
|
+ " app_version varchar(10) not null," +
|
|
|
+ " unique key(app_code)" +
|
|
|
+ ")";
|
|
|
+ jdbcTemplate.execute(createTableSql);
|
|
|
+
|
|
|
+ jdbcTemplate.update("insert into boot_app_info (app_code,app_version) values(?,?)", appCode, appVersion);
|
|
|
} catch (Exception e) {
|
|
|
throw new RuntimeException(e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private HikariDataSource initDataSource(ConfigurableEnvironment environment) {
|
|
|
- String dbUrl = environment.getProperty(DB_URL, String.class);
|
|
|
- if (StringUtils.isBlank(dbUrl)) {
|
|
|
- throw new RuntimeException(DB_URL + " 配置值不能为空!");
|
|
|
- }
|
|
|
- String dbUsername = environment.getProperty(DB_USERNAME, String.class);
|
|
|
- if (StringUtils.isBlank(dbUsername)) {
|
|
|
- throw new RuntimeException(DB_USERNAME + " 配置值不能为空!");
|
|
|
- }
|
|
|
- String dbPassword = environment.getProperty(DB_PASSWORD, String.class);
|
|
|
- if (StringUtils.isBlank(dbPassword)) {
|
|
|
- throw new RuntimeException(DB_PASSWORD + " 配置值不能为空!");
|
|
|
+ private boolean existBootAppInfo(JdbcTemplate jdbcTemplate) {
|
|
|
+ try {
|
|
|
+ // 是否存在应用信息表:boot_app_info
|
|
|
+ List<String> exists = jdbcTemplate.queryForList("SHOW TABLES LIKE 'boot_app_info'", String.class);
|
|
|
+ return !exists.isEmpty();
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
}
|
|
|
- String driverClassName = DatabaseDriver.fromJdbcUrl(dbUrl).getDriverClassName();
|
|
|
+ }
|
|
|
|
|
|
+ private HikariDataSource initDataSource() {
|
|
|
HikariDataSource dataSource = new HikariDataSource();
|
|
|
dataSource.setPoolName("dataUpgradeDataSource");
|
|
|
- dataSource.setDriverClassName(driverClassName);
|
|
|
- dataSource.setJdbcUrl(dbUrl);
|
|
|
- dataSource.setUsername(dbUsername);
|
|
|
- dataSource.setPassword(dbPassword);
|
|
|
+ dataSource.setDriverClassName(properties.getDriverClassName());
|
|
|
+ dataSource.setJdbcUrl(properties.getDbUrl());
|
|
|
+ dataSource.setUsername(properties.getDbUsername());
|
|
|
+ dataSource.setPassword(properties.getDbPassword());
|
|
|
return dataSource;
|
|
|
}
|
|
|
|
|
|
- private List<Class<?>> findInterfaceImpls(Class<?> interfaceClass, String basePackage) {
|
|
|
- ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
|
|
|
- scanner.addIncludeFilter(new AssignableTypeFilter(interfaceClass));
|
|
|
- List<Class<?>> impls = new ArrayList<>();
|
|
|
- scanner.findCandidateComponents(basePackage).forEach(bean -> {
|
|
|
- try {
|
|
|
- Class<?> clazz = ClassUtils.forName(bean.getBeanClassName(), ClassUtils.getDefaultClassLoader());
|
|
|
- // 排除接口和抽象类
|
|
|
- if (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) {
|
|
|
- impls.add(clazz);
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("{} class load fail. {}", bean.getBeanClassName(), e.getMessage());
|
|
|
- }
|
|
|
- });
|
|
|
- return impls;
|
|
|
- }
|
|
|
-
|
|
|
}
|