|
@@ -0,0 +1,209 @@
|
|
|
+package com.qmth.boot.data.upgrade.config;
|
|
|
+
|
|
|
+import com.qmth.boot.data.upgrade.annotation.DataUpgradeVersion;
|
|
|
+import com.qmth.boot.data.upgrade.service.DataUpgradeService;
|
|
|
+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.JdbcTemplate;
|
|
|
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
|
|
+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;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 数据升级监听类
|
|
|
+ *
|
|
|
+ * @author: Deason
|
|
|
+ * @since: 2025/3/3
|
|
|
+ */
|
|
|
+public class DataUpgradeListener implements ApplicationListener<ApplicationPreparedEvent> {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(DataUpgradeListener.class);
|
|
|
+
|
|
|
+ private static final String DB_URL = "com.qmth.datasource.url";
|
|
|
+
|
|
|
+ private static final String DB_USERNAME = "com.qmth.datasource.username";
|
|
|
+
|
|
|
+ private static final String DB_PASSWORD = "com.qmth.datasource.password";
|
|
|
+
|
|
|
+ private static final String APP_VERSION = "com.qmth.solar.app-version";
|
|
|
+
|
|
|
+ private static final String BASE_PACKAGE = "com.qmth.data.upgrade.base-package";
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onApplicationEvent(ApplicationPreparedEvent event) {
|
|
|
+ ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
|
|
|
+ try (HikariDataSource dataSource = this.initDataSource(environment)) {
|
|
|
+ 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 + " 配置值不能为空!");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查当前版本是否需要数据升级
|
|
|
+ String currentVersion = this.queryCurrentVersion(jdbcTemplate);
|
|
|
+ if (new VersionComparator().compare(currentVersion, appVersion) >= 0) {
|
|
|
+ log.warn("跳过数据升级!currentVersion:{} appVersion:{}", currentVersion, appVersion);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String basePackage = environment.getProperty(BASE_PACKAGE, String.class);
|
|
|
+ if (StringUtils.isBlank(basePackage)) {
|
|
|
+ basePackage = "com.qmth";// 未配置扫描包名,赋默认值
|
|
|
+ }
|
|
|
+
|
|
|
+ List<DataUpgradeService> services = this.matchServices(basePackage, currentVersion, appVersion);
|
|
|
+ if (!services.isEmpty()) {
|
|
|
+ log.warn("*************** data upgrade start ***************");
|
|
|
+ for (DataUpgradeService service : services) {
|
|
|
+ service.process(jdbcTemplate, transactionTemplate);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.warn("未找到数据升级的具体实现!{}:{}", BASE_PACKAGE, basePackage);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 数据升级完成后修改为当前版本号
|
|
|
+ updateCurrentVersion(appVersion, jdbcTemplate);
|
|
|
+ log.warn("*************** data upgrade finish ***************");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("数据升级异常,终止运行。{}", e.getMessage(), e);
|
|
|
+ System.exit(1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取数据升级的具体实现(按版本号由小到大排序、支持跨版本)
|
|
|
+ * 1、出现同个版本的重复实现,则异常终止
|
|
|
+ * 2、未扫描到任何实现 或 所有实现都与待升级目标版本的版本区间不匹配,则忽略
|
|
|
+ */
|
|
|
+ private List<DataUpgradeService> matchServices(String basePackage, String currentVersion, String appVersion) {
|
|
|
+ List<Class<?>> implClasses = this.findInterfaceImpls(DataUpgradeService.class, basePackage);
|
|
|
+ if (implClasses.isEmpty()) {
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ // 跳过未声明“数据升级版本”注解的实现
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ Constructor<?> constructor = ClassUtils.getConstructorIfAvailable(clazz);
|
|
|
+ if (constructor != null) {
|
|
|
+ serviceMaps.put(value, (DataUpgradeService) constructor.newInstance());
|
|
|
+ keys.add(value);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ List<DataUpgradeService> result = new ArrayList<>();
|
|
|
+ if (!keys.isEmpty()) {
|
|
|
+ // 按版本号由小到大排序
|
|
|
+ keys.sort(vc);
|
|
|
+ for (String key : keys) {
|
|
|
+ result.add(serviceMaps.get(key));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String queryCurrentVersion(JdbcTemplate jdbcTemplate) {
|
|
|
+ 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";
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateCurrentVersion(String appVersion, JdbcTemplate jdbcTemplate) {
|
|
|
+ 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);
|
|
|
+ } 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 + " 配置值不能为空!");
|
|
|
+ }
|
|
|
+ String driverClassName = DatabaseDriver.fromJdbcUrl(dbUrl).getDriverClassName();
|
|
|
+
|
|
|
+ HikariDataSource dataSource = new HikariDataSource();
|
|
|
+ dataSource.setPoolName("dataUpgradeDataSource");
|
|
|
+ dataSource.setDriverClassName(driverClassName);
|
|
|
+ dataSource.setJdbcUrl(dbUrl);
|
|
|
+ dataSource.setUsername(dbUsername);
|
|
|
+ dataSource.setPassword(dbPassword);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|