前言
客户那边的服务器不靠谱,多个基地多个数据源,经常有数据源连不上,他们又不想影响访问,
故此有了此次尝试
可以搭配注解@DS使用,检查数据源连接状态,选择跳过或者报异常;也可以将本次示例作为一次自定义注解+AOP的实践
yml中配置的多数据源示例如下

1.定时任务获取各个数据源的健康状态
如果用户调用接口时再去检查健康状态会十分影响返回速度,故此提前将各个数据源的健康状态存储在redis中,这里使用了一个自定义的redisUtil工具类,读者可使用RedisTemplate进行功能替换
@Resource
private DynamicRoutingDataSource dynamicDataSource;
/**
* 每分钟检查全厂区数据库的连接状态
*/
@Scheduled(cron = "0 0/1 * * * ?")
public void checkDBConnect() {
Map<String, DataSource> dataSources = dynamicDataSource.getDataSources();
//遍历测试连接
for (String dsName : dataSources.keySet()) {
boolean isConnected = Boolean.FALSE;
String dbUrl = "";
try{
//获取url
ItemDataSource currentDataSource = (ItemDataSource)dataSources.get(dsName);
HikariDataSource hikariDataSource = (HikariDataSource)currentDataSource.getRealDataSource();
dbUrl = hikariDataSource.getJdbcUrl();
if (hikariDataSource.getDriverClassName().contains("sqlserver")){
dbUrl = dbUrl.substring(0, dbUrl.indexOf(";",dbUrl.indexOf(";")+1));
}else{
dbUrl = dbUrl.substring(0, dbUrl.indexOf("?"));
}
//检查数据库连接
hikariDataSource.setLoginTimeout(5);
isConnected = checkConnection(hikariDataSource);
}catch (Exception e){
e.printStackTrace();
}
redisUtil.set(SystemConstants.DATASOURCE_STATUS + dbUrl, isConnected);
}
}
private boolean checkConnection(DataSource dataSource) {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// 根据数据库类型执行不同的健康检查语句
String productName = conn.getMetaData().getDatabaseProductName();
String testQuery = productName.contains("MySQL") ? "SELECT 1" : "SELECT 1";
return stmt.execute(testQuery);
} catch (Exception e) {
return false;
}
}
定时任务运行之后,可以在redis中查看到各个数据源的健康状态,如下图所示
true标识存活,false则是无法连接上

2.自定义注解
允许在方法或者类上进行注解,都有注解时,优先方法上的注解(这个逻辑在后续切面中)
默认抛出异常,而是依据原方法的返回值,返回空对象
/**
* 数据源连接检查注解
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckDSConnect {
/**
* 连接失败时抛出的异常,默认不抛出异常
*/
boolean throwException() default false;
/**
* 可选:抛出的异常信息(若指定异常信息,则无论throwException为何值都抛出异常)
*/
String throwExceptionMsg() default "";
/**
* 可选:抛出的异常code(若指定异常code,则无论throwException为何值都抛出异常)
*/
String throwExceptionCode() default "";
}
3.切面实现
实现逻辑较为简单,在执行实际的方法前,检查redis中当前数据源的存活状态,然后依据注解中的参数处理返回或者异常抛出
异常类型可以修改为自己所需的类型,对整体逻辑无影响
目前数据源仅适配了sql server和mysql,如有其他数据源,可依据需要修改获取url的substring(redis存储时也要同步修改)
@Aspect
@Component
@Slf4j
public class CheckDSConnectAspect {
@Autowired
RedisUtil redisUtil;
// 注入Spring管理的数据源(自动适配HikariCP、Druid等连接池)
@Resource
private DynamicRoutingDataSource dataSource;
// 切入点:拦截BaseMapper的list方法
@Pointcut("@annotation(com.tot.common.annotation.CheckDSConnect) || @within(com.tot.common.annotation.CheckDSConnect)")
public void needCheck() {}
@Around("needCheck()")
public Object aroundListMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 0. 动态获取注解
CheckDSConnect checkDSConnect = getCheckDSConnectAnnotation(joinPoint);
// 1. 获取数据库连接信息
String dbUrl = getCurrentDSUrl();
//2. 检查数据库连接是否正常
Boolean isConnection = checkIsConnection(dbUrl);
// 3. 获取目标方法的返回类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Class<?> returnType = signature.getReturnType();
// 4. 连接正常,执行原方法
if (isConnection) {
return joinPoint.proceed();
}
// 5. 处理连接失败
log.error("数据库连接失败,原方法未执行,数据源:{} 方法名:{}", dbUrl, joinPoint.getSignature().getName());
if (checkDSConnect.throwException() || StrUtil.isNotEmpty(checkDSConnect.throwExceptionMsg()) || StrUtil.isNotEmpty(checkDSConnect.throwExceptionCode())){// 指定了异常时抛出
String code = "500";
String msg = "存在连接失败的数据源";
if (StrUtil.isNotEmpty(checkDSConnect.throwExceptionMsg())){
msg = checkDSConnect.throwExceptionMsg();
}
if (StrUtil.isNotEmpty(checkDSConnect.throwExceptionCode())){
code = checkDSConnect.throwExceptionCode();
}
throw new BusinessException(code,msg);
}
return getDefaultReturnValue(returnType);// 未指定时返回类型默认值
}
/**
* 获取CheckDSConnect注解,优先获取方法上的注解
* @param joinPoint
* @return
*/
private CheckDSConnect getCheckDSConnectAnnotation(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
CheckDSConnect methodAnnotation = method.getAnnotation(CheckDSConnect.class);
CheckDSConnect classAnnotation = null;
if (methodAnnotation == null) {
// 获取目标类(考虑代理)
Object target = joinPoint.getTarget();
Class<?> targetClass = target.getClass();
// 如果是代理对象,获取其原始类
if (AopUtils.isAopProxy(target)) {
targetClass = AopUtils.getTargetClass(target);
}
classAnnotation = targetClass.getAnnotation(CheckDSConnect.class);
}
// 最终使用的注解:方法注解优先,如果没有则使用类注解
return methodAnnotation != null ? methodAnnotation : classAnnotation;
}
/**
* 获取当前数据源的URL
* @return
*/
private String getCurrentDSUrl() {
String dbUrl = "";
ItemDataSource currentDataSource = (ItemDataSource)dataSource.determineDataSource();
HikariDataSource hikariDataSource = (HikariDataSource)currentDataSource.getRealDataSource();
dbUrl = hikariDataSource.getJdbcUrl();
if (hikariDataSource.getDriverClassName().contains("sqlserver")){
dbUrl = dbUrl.substring(0, dbUrl.indexOf(";",dbUrl.indexOf(";")+1));
}else{
dbUrl = dbUrl.substring(0, dbUrl.indexOf("?"));
}
return dbUrl;
}
/**
* 检查数据库连接是否正常
* @param dbUrl
* @return
*/
private Boolean checkIsConnection(String dbUrl) {
Object o = redisUtil.get(SystemConstants.DATASOURCE_STATUS+dbUrl);
if (o == null){
return Boolean.FALSE;
}
return Boolean.parseBoolean(o.toString());
}
/**
* 根据返回类型生成默认返回值
* @param returnType 方法返回类型
* @return 对应类型的默认值
*/
private Object getDefaultReturnValue(Class<?> returnType) {
// 处理void类型
if (returnType == void.class) {
return null;
}
// 处理基本类型
if (returnType.isPrimitive()) {
if (returnType == boolean.class) {
return false;
} else if (returnType == char.class) {
return '\0';
} else if (returnType == byte.class || returnType == short.class
|| returnType == int.class) {
return 0;
} else if (returnType == long.class) {
return 0L;
} else if (returnType == float.class) {
return 0.0f;
} else if (returnType == double.class) {
return 0.0d;
}
}
// 处理数组类型
if (returnType.isArray()) {
return Array.newInstance(returnType.getComponentType(), 0);
}
// 处理集合类型(可选:返回空集合而非null)
if (List.class.isAssignableFrom(returnType)) {
return Collections.emptyList();
}
if (Set.class.isAssignableFrom(returnType)) {
return Collections.emptySet();
}
if (Map.class.isAssignableFrom(returnType)) {
return Collections.emptyMap();
}
//处理js
if (JSONObject.class.isAssignableFrom(returnType)){
return new JSONObject();
}
// 其他引用类型返回null
return null;
}
}
4.使用示例
在已有的@DS旁加上我们的@CheckDSConnect即可,aop的无侵入特性可以说是十分的方便

406

被折叠的 条评论
为什么被折叠?



