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消息