Mysql配置

mysql使用DynamicDataSource做到主从复制和动态设置mysql配置,在初始化时默认初始化化从库,从库初始化失败不影响程序正常启动,并且可以在配置页面修改数据库地址,账号密码。

这一个类的主要作用是更新数据库连接,在项目初始化的时候用这个类去校验数据库连接和更新数据库连接

package top.csmcool.generalconfiguration.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.csmcool.common.enums.DataSourceType;
import top.csmcool.generalconfiguration.Exception.AppException;
import top.csmcool.generalconfiguration.config.properties.DruidProperties;
import top.csmcool.generalconfiguration.model.database.DbStatusSingleton;
import top.csmcool.generalconfiguration.model.database.InitDatabaseModel;
import top.csmcool.generalconfiguration.utils.DataSourceUtil;
import top.csmcool.generalconfiguration.utils.YamlUtil;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @Description
 * @Author shizhe
 * @Date 2025/1/15
 */

@Component
public class DynamicDatasourceConfig {

    @Autowired(required = false)
    private DataSource dataSource;

    @Autowired(required = false)
    private DruidProperties druidProperties;


    public static final String configPrefix = "spring.datasource.druid.";
    public static final String master = "spring.datasource.druid.master";
    public static final String slave = "spring.datasource.druid.slave";
    public static final String defaultConnectArg = "useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8"; // 默认链接参数
    public static final String jdbcPrefix = "jdbc:mysql://";

    /**
     * 是否获取链接成功
     * @return 是否获取链接成功
     */
    public Boolean checkDataSource() {
        try {
            Connection connection = dataSource.getConnection();
            connection.close();
            DbStatusSingleton.getInstance().setDbStatus(true);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public Boolean checkConnect(InitDatabaseModel jdbcInfo) {
        // jdbc:mysql://192.168.1.2:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        String url = jdbcPrefix
                + jdbcInfo.getDatabaseHost()
                + ":"
                + jdbcInfo.getDatabasePort()
                + "/"
                + jdbcInfo.getDatabaseName()
                + "?"
                + (StringUtils.isEmpty(jdbcInfo.getDataConnectArg()) ? defaultConnectArg : jdbcInfo.getDataConnectArg());
        String user = jdbcInfo.getUser();
        String password = jdbcInfo.getPassword();

        // 加载驱动程序
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            System.out.println("找不到数据库驱动程序!");
            return false;
        }

        // 建立数据库连接
        try (Connection connection = DriverManager.getConnection(url, user, password)) {
            System.out.println("数据库连接成功!");
        } catch (SQLException e) {
            System.out.println("数据库连接失败!");
            e.printStackTrace();
            return false;
        }

        return true;
    }

    public String checkConnectWithException(InitDatabaseModel jdbcInfo) {
        String url = jdbcPrefix
                + jdbcInfo.getDatabaseHost()
                + ":"
                + jdbcInfo.getDatabasePort()
                + "/"
                + jdbcInfo.getDatabaseName()
                + "?"
                + (StringUtils.isEmpty(jdbcInfo.getDataConnectArg()) ? defaultConnectArg : jdbcInfo.getDataConnectArg());
        String user = jdbcInfo.getUser();
        String password = jdbcInfo.getPassword();

        // 加载驱动程序
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            return "找不到数据库驱动程序!";
        }

        // 建立数据库连接
        try (Connection connection = DriverManager.getConnection(url, user, password)) {
            System.out.println("数据库连接成功!");
        } catch (SQLException e) {
            e.printStackTrace();
            return convertConnectionException(e);
        }

        return "";
    }

    /**
     * 将 MySQL 连接异常转换为用户友好的错误信息
     * @param ex SQL 异常对象
     * @return 格式化后的错误信息字符串
     */
    public static String convertConnectionException(SQLException ex) {
        StringBuilder errorMsg = new StringBuilder();

        // 1. 通信异常处理
        if (ex.getMessage().contains("Communications link failure")) {
            errorMsg.append("🚨 网络连接失败:\n")
                    .append("   • MySQL服务可能未运行\n")
                    .append("   • 检查主机地址和端口号是否正确\n")
                    .append("   • 验证防火墙设置是否开放了3306端口");
        }

        // 2. 认证失败处理 (错误代码1045)
        else if (ex.getErrorCode() == 1045) {
            errorMsg.append("🔑 认证失败:\n")
                    .append("   • 用户名或密码错误\n")
                    .append("   • 用户没有访问数据库的权限\n")
                    .append("   • 检查连接字符串中的用户名和密码");
        }

        // 3. 数据库不存在 (错误代码1049)
        else if (ex.getErrorCode() == 1049) {
            errorMsg.append("🗃️ 数据库不存在:\n")
                    .append("   • 指定的数据库名称不正确\n")
                    .append("   • 数据库尚未创建\n")
                    .append("   • 检查连接URL中的数据库名称");
        }

        // 4. SSL握手异常
        else if (ex.getCause() != null && ex.getCause().toString().contains("SSLHandshakeException")) {
            errorMsg.append("🔒 SSL配置错误:\n")
                    .append("   • 服务器要求SSL连接但客户端未配置\n")
                    .append("   • 证书验证失败\n")
                    .append("   • 尝试在连接字符串添加:?useSSL=false");
        }

        // 5. 连接超时处理
        else if (ex.getMessage().contains("Connection timed out")) {
            errorMsg.append("⏱️ 连接超时:\n")
                    .append("   • 网络延迟过高\n")
                    .append("   • MySQL服务器过载\n")
                    .append("   • 尝试增加超时时间:&connectTimeout=5000");
        }

        // 6. 连接拒绝 (通常为端口错误)
        else if (ex.getMessage().contains("Connection refused")) {
            errorMsg.append("🚫 连接被拒绝:\n")
                    .append("   • MySQL未在指定端口运行\n")
                    .append("   • 检查MySQL服务端口号\n")
                    .append("   • 确认防火墙允许访问该端口");
        }

        // 7. 通用错误处理
        else {
            errorMsg.append("❌ 数据库连接错误 [")
                    .append(ex.getErrorCode()).append("]:\n")
                    .append("   错误信息: ").append(ex.getMessage()).append("\n")
                    .append("   SQL状态码: ").append(ex.getSQLState());
        }

        // 添加通用建议
        errorMsg.append("\n\n🔧 通用解决方案:")
                .append("\n   • 检查MySQL服务状态")
                .append("\n   • 验证连接参数(主机、端口、用户名、密码、数据库名)")
                .append("\n   • 尝试使用命令行工具连接:mysql -u[用户] -p[密码] -h[主机]");

        return errorMsg.toString();
    }

    public List<InitDatabaseModel> getAllConfig() {
        List<InitDatabaseModel> list = new ArrayList<>();
        Map<String, Object> yamlMap = YamlUtil.getYamlMap(master);
        InitDatabaseModel masterJdbcInfo = getJdbcInfo(yamlMap, true);
        masterJdbcInfo.setType("master");
        list.add(masterJdbcInfo);

        // 从库信息
        Map<String, Object> slaveyamlMap = YamlUtil.getYamlMap(slave);
        InitDatabaseModel slaveJdbcInfo = getJdbcInfo(slaveyamlMap, false);
        slaveJdbcInfo.setType("slave");
        list.add(slaveJdbcInfo);

        return list;
    }

    public boolean updateJdbcInfo(InitDatabaseModel jdbcInfo) {
        // jdbc:mysql://192.168.1.2:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        String url = jdbcPrefix
                + jdbcInfo.getDatabaseHost()
                + ":"
                + jdbcInfo.getDatabasePort()
                + "/"
                + jdbcInfo.getDatabaseName()
                + "?"
                + (StringUtils.isEmpty(jdbcInfo.getDataConnectArg()) ? defaultConnectArg : jdbcInfo.getDataConnectArg());

        String type = jdbcInfo.getType();
        String user = jdbcInfo.getUser();
        String password = jdbcInfo.getPassword();

        if(!DataSourceType.MASTER.toString().equalsIgnoreCase(type) && !DataSourceType.SLAVE.toString().equalsIgnoreCase(type)) {
            throw new AppException("数据源类型错误!");
        }

        String key = configPrefix + type.toLowerCase() + ".";
        YamlUtil.updateValueByKey(key + "url", url);
        YamlUtil.updateValueByKey(key + "user", user);
        YamlUtil.updateValueByKey(key + "password", password);
        YamlUtil.updateValueByKey(key + "enabled", jdbcInfo.isEnabled());

        // 更新数据库链接
        return updateDataSource();
    }

    private InitDatabaseModel getJdbcInfo(Map<String, Object> yamlMap, boolean master) {
        String url =  String.valueOf(yamlMap.get("url"));
        String username =  String.valueOf(yamlMap.get("username"));
        String password =  String.valueOf(yamlMap.get("password"));
        boolean enabled = (boolean) yamlMap.getOrDefault("enabled", true);

        InitDatabaseModel initModel = DataSourceUtil.parseJdbcUrl(url);
        initModel.setUser(username);
        initModel.setPassword(password);
        initModel.setEnabled(enabled);
        if(master) {
            initModel.setEnabled(true);
        }

        return initModel;
    }

    public Boolean updateDataSource(){
        DynamicDataSource ds = (DynamicDataSource) dataSource;
        try {
            // 主数据库连接一定要有
            Map<Object, Object> targetDataSources = DataSourceUtil.getTargetDataSources();
            DruidDataSource masterDataSource = (DruidDataSource) targetDataSources.get(DataSourceType.MASTER.name());
            ds.setDefaultTargetDataSource(masterDataSource);
            ds.setTargetDataSources(targetDataSources);
            ds.afterPropertiesSet();
            Connection connection = masterDataSource.getConnection();
            connection.close();
            DbStatusSingleton.getInstance().setDbStatus(true);
        } catch (Exception e) {
            return false;
        }
        return true;
    }
}

Redis配置

在DynamicRedisConfig动态更新redis配置,需要注入sa-token的redis类,不然不会更新到sa-token的reids配置,导致redis使用失败,可以系统在初始化的设置redis等配置,用户可以不去操作配置文件,简化了上手难度

package top.csmcool.generalconfiguration.datasource;

import cn.dev33.satoken.dao.SaTokenDaoRedisJackson;
import io.lettuce.core.RedisCommandTimeoutException;
import io.lettuce.core.RedisConnectionException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import top.csmcool.generalconfiguration.Exception.AppException;
import top.csmcool.generalconfiguration.config.properties.RedisProperties;
import top.csmcool.generalconfiguration.model.database.InitRedisModel;
import top.csmcool.generalconfiguration.utils.SetSystemProperty;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

@Component
@RequiredArgsConstructor
public class DynamicRedisConfig {

    private final RedisTemplate<String, Object> redisTemplate;
    private final SaTokenDaoRedisJackson saTokenDaoRedisJackson;
    private final RedisProperties redisProperties;
    private final AtomicBoolean updating = new AtomicBoolean(false);

    private static final String HOST_KEY = "spring.data.redis.host";
    private static final String PORT_KEY = "spring.data.redis.port";
    private static final String DATABASE_KEY = "spring.data.redis.database";
    private static final String PASSWORD_KEY = "spring.data.redis.password";

    public boolean checkRedis() {
        try {
            return checkConnectAndClose((LettuceConnectionFactory) Objects.requireNonNull(redisTemplate.getConnectionFactory()));
        } catch (Exception e) {
            return false;
        }
    }

    public boolean checkConnection(InitRedisModel initRedisModel) {
        try {
            LettuceConnectionFactory factory = createLettuceConnectionFactory(initRedisModel);
            factory.afterPropertiesSet();
            return checkConnectAndClose(factory);
        } catch (Exception e) {
            return false;
        }
    }

    public String checkConnectionWithException(InitRedisModel initRedisModel) {
        try {
            LettuceConnectionFactory factory = createLettuceConnectionFactory(initRedisModel);
            factory.afterPropertiesSet();
            boolean b = checkConnectAndClose(factory);
            return b ? "" : "连接失败,请检测地址或者密码";
        } catch (Exception e) {
            return parseConnectionError(e, initRedisModel);
        }
    }

    /**
     * 解析连接异常并生成用户友好的错误消息
     */
    private String parseConnectionError(Exception ex, InitRedisModel model) {
        String hostPort = model.getHost() + ":" + model.getPort();
        Throwable rootCause = getRootCause(ex);
        String errorDetail = rootCause.getMessage();


        // 2. 连接超时
        if (rootCause instanceof RedisCommandTimeoutException) {
            return "⏱️ Redis 连接超时\n" +
                    "   • 目标地址: " + hostPort + "\n" +
                    "   • 错误详情: " + errorDetail + "\n" +
                    "   • 解决方案: \n" +
                    "       1. 检查网络连接\n" +
                    "       2. 增加超时时间\n" +
                    "       3. 确认服务器防火墙开放";
        }

        // 3. 拒绝连接
        if (rootCause instanceof RedisConnectionException) {
            if (errorDetail.contains("Connection refused")) {
                return "🚫 Redis 连接被拒绝\n" +
                        "   • 可能原因:\n" +
                        "       1. Redis 服务未启动\n" +
                        "       2. 端口号错误: " + model.getPort() + "\n" +
                        "       3. 防火墙阻止访问\n" +
                        "   • 诊断命令: redis-cli -h " + model.getHost() + " -p " + model.getPort() + " ping";
            }
            return "🚫 Redis 连接错误\n" +
                    "   • 主机: " + hostPort + "\n" +
                    "   • 错误详情: " + errorDetail;
        }

        // 4. 主机无法解析
        if (errorDetail.contains("Unable to connect") ||
                errorDetail.contains("unknown host")) {
            return "🌐 无法解析主机地址\n" +
                    "   • 主机: " + model.getHost() + "\n" +
                    "   • 错误详情: " + errorDetail + "\n" +
                    "   • 解决方案: \n" +
                    "       1. 检查主机名拼写\n" +
                    "       2. 检查DNS解析";
        }

        // 5. 数据库索引无效
        if (errorDetail.contains("invalid database index")) {
            return "📁 无效的数据库索引\n" +
                    "   • 指定索引: " + model.getDataindex() + "\n" +
                    "   • 有效范围: 0-15 (默认配置)\n" +
                    "   • 解决方案: 使用有效的数据库索引";
        }

        // 6. SSL/TLS 错误
        if (errorDetail.contains("SSL") || errorDetail.contains("TLS")) {
            return "🔒 SSL/TLS 连接错误\n" +
                    "   • 错误详情: " + errorDetail + "\n" +
                    "   • 解决方案: \n" +
                    "       1. 检查证书配置\n" +
                    "       2. 使用非加密连接 (不推荐)\n" +
                    "       3. 添加参数: verifyPeer=false";
        }

        // 7. 通用错误处理
        return "❌ Redis 连接失败\n" +
                "   • 主机: " + hostPort + "\n" +
                "   • 错误类型: " + rootCause.getClass().getSimpleName() + "\n" +
                "   • 错误详情: " + errorDetail + "\n" +
                "   • 排查步骤: \n" +
                "       1. 检查Redis服务状态\n" +
                "       2. 使用命令行测试: telnet " + model.getHost() + " " + model.getPort() + "\n" +
                "       3. 检查认证信息";
    }

    /**
     * 获取异常的根本原因
     */
    private Throwable getRootCause(Throwable throwable) {
        Throwable rootCause = throwable;
        while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
            rootCause = rootCause.getCause();
        }
        return rootCause;
    }

    private boolean checkConnectAndClose(LettuceConnectionFactory factory) {
        try (RedisConnection connection = factory.getConnection()) {
            return "PONG".equals(connection.ping());
        } catch (Exception e) {
            throw e;
        }
    }

    public InitRedisModel getRedisConfig() {
        return InitRedisModel.builder()
                .host(SetSystemProperty.getKeyValue(HOST_KEY))
                .port(Integer.parseInt(SetSystemProperty.getKeyValue(PORT_KEY)))
                .dataindex(Integer.parseInt(SetSystemProperty.getKeyValue(DATABASE_KEY)))
                .password(SetSystemProperty.getKeyValue(PASSWORD_KEY))
                .build();
    }

    public boolean updateConfig(InitRedisModel initRedisModel) {
        if (!updating.compareAndSet(false, true)) {
            throw new AppException("已有更新操作在进行中");
        }

        try {
            // 1. 验证新配置
            if (!checkConnection(initRedisModel)) {
                return false;
            }

            // 2. 写入新配置
            writeConfigToProperties(initRedisModel);

            // 3. 更新运行时配置
            return updateRedisConnection(initRedisModel);
        } finally {
            updating.set(false);
        }
    }

    private void writeConfigToProperties(InitRedisModel model) {
        SetSystemProperty.writeProperties(HOST_KEY, model.getHost());
        SetSystemProperty.writeProperties(PORT_KEY, String.valueOf(model.getPort()));
        SetSystemProperty.writeProperties(DATABASE_KEY, String.valueOf(model.getDataindex()));
        SetSystemProperty.writeProperties(PASSWORD_KEY, model.getPassword());
    }

    private synchronized boolean updateRedisConnection(InitRedisModel model) {
        LettuceConnectionFactory newFactory = null;
        LettuceConnectionFactory oldFactory = null;

        try {
            // 创建新连接工厂
            newFactory = createLettuceConnectionFactory(model);
            newFactory.afterPropertiesSet();

            // 替换连接工厂
            oldFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();

            redisTemplate.setConnectionFactory(newFactory);
            saTokenDaoRedisJackson.stringRedisTemplate = new StringRedisTemplate(newFactory);
            saTokenDaoRedisJackson.objectRedisTemplate.setConnectionFactory(newFactory);
            // 销毁旧连接池(避免泄漏)
            if (oldFactory != null) {
                oldFactory.destroy();
            }

            return true;
        } catch (Exception e) {
            // 回滚:恢复旧连接工厂
            if (oldFactory != null) {
                redisTemplate.setConnectionFactory(oldFactory);
            }
            // 清理失败的新工厂
            if (newFactory != null) {
                newFactory.destroy();
            }
            throw new AppException(e,"Redis配置更新失败: " + e.getMessage());
        }
    }

    private LettuceConnectionFactory createLettuceConnectionFactory(InitRedisModel model) {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(model.getHost());
        config.setPort(model.getPort());
        config.setPassword(model.getPassword());
        config.setDatabase(model.getDataindex());
        return redisProperties.setProperties(config);
    }
}

配置文件加载

在系统中配置文件都默认在resource中配置,如果部署在docker中就不好修改配置文件内容,所以需要把配置文件放在外层,这样就方便用户把配置文件挂载出来可以很方便修改配置文件,通过实现EnvironmentPostProcessor来加载配置文件,优先使用系统变量config.path来指定配置文件地址


package top.csmcool.generalconfiguration.config;

import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.FileSystemResource;
import top.csmcool.common.utils.ConfigUtil;
import top.csmcool.common.utils.SetSystemProperty;
import top.csmcool.common.utils.YamlUtil;


import java.io.FileInputStream;
import java.io.InputStream;
import java.util.*;

public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        String[] activeProfiles = environment.getActiveProfiles();
        if(activeProfiles.length > 0) {
            for (String activeProfile : activeProfiles) {
                loadConfigFile(environment,activeProfile);
            }
        } else {
            String active = System.getProperty("active") == null ? "dev" : System.getProperty("active");
            loadConfigFile(environment, active);
        }
    }

    private static void loadConfigFile(ConfigurableEnvironment environment, String active) {
        //tomcat路径
        String path = ConfigUtil.getConfigPath();
        List<String> configFiles = System.getProperty("configFiles") == null ? new ArrayList<String>() : Arrays.asList(System.getProperty("configFiles").split(","));
        String dbConfigPath = path + String.format("application-%s-druid.yml", active);
        YamlUtil.setConfigPath(dbConfigPath);
        configFiles.add(dbConfigPath);
        configFiles.add(path + String.format("application-%s.yml", active));
        String profilePath = path + String.format("application-%s.properties", active);
        SetSystemProperty.setProfilePath(profilePath);
        configFiles.add(profilePath);
        Properties properties = new Properties();
        for (String configFile : configFiles) {
            //Springboot读取yml配置
            try {
                if(configFile.endsWith(".yml")) {
                    YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
                    yaml.setResources(new FileSystemResource(configFile));
                    Properties object = Objects.requireNonNull(yaml.getObject());
                    properties.putAll(object);
                }

                if(configFile.endsWith(".properties")) {
                    try(InputStream in = new FileInputStream(configFile)) {
                        properties.load(in);
                    }
                }
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
        MutablePropertySources propertySources = environment.getPropertySources();
        propertySources.addFirst(new PropertiesPropertySource("Config", properties));
    }
}



package top.csmcool.common.utils;

public class ConfigUtil {

    public static String getConfigPath() {
        // 优先检查是否通过系统属性指定了路径
        String customPath = System.getenv("config.path");
        if (customPath != null) {
            return customPath.endsWith("/") ? customPath : customPath + "/";
        }


        String projectPath = System.getProperty("user.dir");
        String path;
        if(System.getProperty("os.name").toLowerCase().contains("windows")) {
            path = projectPath + "/conf/";
        } else {
            path = "/data/";
        }
        return path;
    }

    public static String getEnvProperties(String key) {
        return System.getProperty(key);
    }

    public static String getEnvProperties(String key, String defaultValue) {
        String envProperties = getEnvProperties(key);
        return envProperties == null ? defaultValue : envProperties;
    }
}

mq配置

通过工厂类配置,生成不同mq的util,然后再mqutil推送中根据key来匹配util来发送mq消息