瀏覽代碼

新增《数据升级》模块

deason 3 月之前
父節點
當前提交
614efbf8cf

+ 25 - 0
data-upgrade/pom.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>data-upgrade</artifactId>
+    <packaging>jar</packaging>
+
+    <parent>
+        <artifactId>qmth-boot</artifactId>
+        <groupId>com.qmth.boot</groupId>
+        <version>1.0.5</version>
+    </parent>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>data-mybatis-plus</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 26 - 0
data-upgrade/src/main/java/com/qmth/boot/data/upgrade/annotation/DataUpgradeVersion.java

@@ -0,0 +1,26 @@
+package com.qmth.boot.data.upgrade.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * 数据升级版本注解类
+ *
+ * @author: Deason
+ * @since: 2025/3/3
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface DataUpgradeVersion {
+
+    /**
+     * 匹配版本
+     */
+    String value();
+
+}

+ 10 - 0
data-upgrade/src/main/java/com/qmth/boot/data/upgrade/config/DataUpgradeAutoConfiguration.java

@@ -0,0 +1,10 @@
+package com.qmth.boot.data.upgrade.config;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan("com.qmth.boot.data.upgrade")
+public class DataUpgradeAutoConfiguration {
+
+}

+ 209 - 0
data-upgrade/src/main/java/com/qmth/boot/data/upgrade/config/DataUpgradeListener.java

@@ -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;
+    }
+
+}

+ 21 - 0
data-upgrade/src/main/java/com/qmth/boot/data/upgrade/service/DataUpgradeService.java

@@ -0,0 +1,21 @@
+package com.qmth.boot.data.upgrade.service;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.transaction.support.TransactionTemplate;
+
+/**
+ * 数据升级服务类
+ *
+ * @author: Deason
+ * @since: 2025/3/3
+ */
+public interface DataUpgradeService {
+
+    /**
+     * 数据升级处理
+     *
+     * @throws Exception 处理失败时抛出异常
+     */
+    void process(JdbcTemplate jdbcTemplate, TransactionTemplate transactionTemplate) throws Exception;
+
+}

+ 43 - 0
data-upgrade/src/main/java/com/qmth/boot/data/upgrade/utils/ResourceFileHelper.java

@@ -0,0 +1,43 @@
+package com.qmth.boot.data.upgrade.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+public class ResourceFileHelper {
+
+    private static final Logger log = LoggerFactory.getLogger(ResourceFileHelper.class);
+
+    public static String readContent(String resourcePath) {
+        try {
+            InputStream is = ResourceFileHelper.class.getClassLoader().getResourceAsStream(resourcePath);
+            if (is == null) {
+                throw new RuntimeException(resourcePath + " file is not exist!");
+            }
+
+            return ResourceFileHelper.readContent(is);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static String readContent(InputStream is) {
+        try (InputStreamReader isReader = new InputStreamReader(is, StandardCharsets.UTF_8);
+             BufferedReader bfReader = new BufferedReader(isReader);) {
+            StringBuilder result = new StringBuilder();
+
+            String line;
+            while ((line = bfReader.readLine()) != null) {
+                result.append(line).append("\n");
+            }
+            return result.toString();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 28 - 0
data-upgrade/src/main/java/com/qmth/boot/data/upgrade/utils/VersionComparator.java

@@ -0,0 +1,28 @@
+package com.qmth.boot.data.upgrade.utils;
+
+import java.util.Comparator;
+
+public class VersionComparator implements Comparator<String> {
+
+    @Override
+    public int compare(String v1, String v2) {
+        // 分割版本号为数字段
+        String[] parts1 = v1.split("\\.");
+        String[] parts2 = v2.split("\\.");
+
+        // 获取最大段数
+        int maxLength = Math.max(parts1.length, parts2.length);
+
+        for (int i = 0; i < maxLength; i++) {
+            // 处理长度不一致的情况,缺失段视为0
+            int num1 = (i < parts1.length) ? Integer.parseInt(parts1[i]) : 0;
+            int num2 = (i < parts2.length) ? Integer.parseInt(parts2[i]) : 0;
+
+            if (num1 != num2) {
+                return Integer.compare(num1, num2);
+            }
+        }
+        return 0;
+    }
+
+}

+ 4 - 0
data-upgrade/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,4 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  com.qmth.boot.data.upgrade.config.DataUpgradeAutoConfiguration
+org.springframework.context.ApplicationListener=\
+  com.qmth.boot.data.upgrade.config.DataUpgradeListener