java 基于注解+AOP切面 实现dynamic多数据源连接自动检查其健康状态

前言

客户那边的服务器不靠谱,多个基地多个数据源,经常有数据源连不上,他们又不想影响访问,

故此有了此次尝试

可以搭配注解@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的无侵入特性可以说是十分的方便

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值