基于AOP环绕通知实现应用多数据源切换

实现原理

动态数据源切换,就是在程序运行时,更换你获取到的数据源,从而选择读取的数据库(下面举例为自己工程的数据库以及报表程序访问的数据库)。主要使用的技术是:annotation,Spring AOP ,反射。

我们先来看下AbstractRoutingDataSource的定义:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {  
      
        private Map<Object, Object> targetDataSources;  
      
        private Object defaultTargetDataSource;  
      
        private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();  
      
        private Map<Object, DataSource> resolvedDataSources;  
      
        private DataSource resolvedDefaultDataSource;
}

AbstractRoutingDataSource继承了AbstractDataSource ,而AbstractDataSource 又是DataSource 的子类。

DataSource 是javax.sql 的数据源接口,定义如下:

public interface DataSource  extends CommonDataSource,Wrapper {  
      
      Connection getConnection() throws SQLException;  
       
      Connection getConnection(String username, String password)  
        throws SQLException;  
}  

DataSource 接口定义了2个方法,都是获取数据库连接。我们在看下AbstractRoutingDataSource 如何实现了DataSource接口:

public Connection getConnection() throws SQLException {  
        return determineTargetDataSource().getConnection();  
    }  
      
    public Connection getConnection(String username, String password) throws SQLException {  
        return determineTargetDataSource().getConnection(username, password);  
    }  

很显然就是调用自己的determineTargetDataSource() 方法获取到connection。determineTargetDataSource方法定义如下:

protected DataSource determineTargetDataSource() {  
            Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");  
            Object lookupKey = determineCurrentLookupKey();  
            DataSource dataSource = this.resolvedDataSources.get(lookupKey);  
            if (dataSource == null && (this.lenientFallback || lookupKey == null)) {  
                dataSource = this.resolvedDefaultDataSource;  
            }  
            if (dataSource == null) {  
                throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");  
            }  
            return dataSource;  
}  

主要是下面两段代码获取到数据源:

  Object lookupKey = determineCurrentLookupKey();

   DataSource dataSource = this.resolvedDataSources.get(lookupKey);

determineCurrentLookupKey方法返回lookupKey,resolvedDataSources方法就是根据lookupKey从Map中获得数据源。resolvedDataSources 和determineCurrentLookupKey定义如下:

private Map<Object, DataSource> resolvedDataSources;

protected abstract Object determineCurrentLookupKey()

看到以上定义,是不是有点思路了,resolvedDataSources是Map类型,我们可以把MasterDataSource和ReportDataSource存到Map中,如下:

key        value

master                  MasterDataSource

Report                        ReportDataSource

我们可以写一个类DynamicDataSource 继承AbstractRoutingDataSource,实现其determineCurrentLookupKey() 方法,该方法控制返回Map的key,master或 Report。

实现过程

1.在sapplicationContext.xml中设置多个数据数据库的连接信息,Spring默认会去AbstractRoutingDataSource(Bean)获取数据库的Connection。

2.所以使用spring的AbstractRoutingDataSource指定对应的数据库,并使用 defaultTargetDataSource设置默认数据库

3.这个时候根据写好的自定义注解,在对应的Mapper或者service方法上,例如查找方法上增加注解,指定要查找某个数据库(注意一个点,加注解的方法一定要在你的AOP切面表达式中)

4.AOP切面的方法要在对应的spring.xml配置文件中进行初始化,否则aop切面不能正常生效

5.进行测试(注意IOC域问题,AOP配置在与包扫描统一文件中

DataSource设置

可以使用JDBC、DBCP、alibaba.druid等等任何一种DataSource配置bean

添加到重写的AbstractRoutingDataSource类中(这里是MultipleDataSource)

applicationContext.xml文件:

    <!-- report dataSource-->
    <bean id="reportDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="${reportdb.jndiName}"/>
    </bean>
    
    <!-- hap dataSource-->
    <bean id="hapDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="${db.jndiName}"/>
    </bean>

    <!--千万注意:这里的dataSource名字一定得叫这个-->
    <bean id="dataSource" class="mryx.report.utils.datasource.MultipleDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="hapDataSource" value-ref="hapDataSource"></entry>
                <entry key="reportDataSource" value-ref="reportDataSource"></entry>
            </map>
        </property>
        <!-- 默认目标数据源为你主库数据源 -->
        <property name="defaultTargetDataSource" ref="hapDataSource"/>
    </bean>

    <bean id="DataSourceExchange" class="mryx.report.utils.datasource.MultipleDataSourceExchange"/>

<aop:config>
        <aop:aspect id="aspect" ref="DataSourceExchange" order="1" >
            <!--建议切入到mapper,当然也可以根据不同的execution表达式切入到不同的点-->
            <aop:pointcut id="reportDataMgr" expression="execution(* mryx.report..*.*Mapper.*(..))||execution(* hsrp.core.rp.mapper.IGenericMapper.*(..))"/>
            <aop:before method="beforeMethod"  pointcut-ref="reportDataMgr" />
            <aop:after method="afterMethod" pointcut-ref="reportDataMgr" />
        </aop:aspect>
</aop:config>

注解类:

import java.lang.annotation.*;

/**
 * @author jiaqing.xu@hand-china.com
 * @date 2018/12/26
 * 数据源注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceKey {
    String value() default DataSourceKey.master;

    String master = "hapDataSource";

    String report = "reportDataSource";

}

数据源操作类:

import org.apache.commons.lang3.StringUtils;

/**
 * @author jiaqing.xu@hand-china.com
 * @date 2018/12/26
 * 数据源操作类
 */
public class DynamicDataSourceHolder {
    private static ThreadLocal<String> routeKey = new ThreadLocal<String>();

    /**
     * 获取当前线程的数据源路由的key
     */
    public static String getRouteKey() {
        String key = routeKey.get();
        if (!StringUtils.isNotEmpty(key)) {
            return DataSourceKey.master;
        }
        return key;
    }

    /**
     * 绑定当前线程数据源路由的key
     * 使用完成后必须调用removeRouteKey()方法删除
     */
    public static void setRouteKey(String key) {
        routeKey.set(key);
    }

    /**
     * 删除与当前线程绑定的数据源路由的key
     */
    public static void removeRouteKey() {
        routeKey.remove();
    }
}

获取多数据源类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author jiaqing.xu@hand-china.com
 * @date 2018/12/26
 * 获得数据源
 */
public class MultipleDataSource extends AbstractRoutingDataSource {
    private static final Logger logger = LoggerFactory.getLogger(MultipleDataSource.class);

    @Override
    protected Object determineCurrentLookupKey() {
        logger.debug("---------------------当前数据源:---------------{}", DynamicDataSourceHolder.getRouteKey());
        return DynamicDataSourceHolder.getRouteKey();
    }
}

数据源AOP类:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;

/**
 * @author jiaqing.xu@hand-china.com
 * @date 2018/12/26
 * 数据源AOP
 */
public class MultipleDataSourceExchange {

    /**
     * 前置操作
     *
     * @param joinpoint
     */
    public void beforeMethod(JoinPoint joinpoint) {
        Class<?> target = joinpoint.getTarget().getClass();
        MethodSignature signature = (MethodSignature) joinpoint.getSignature();
        // 默认使用目标类型的注解,如果没有则使用其实现接口的注解类
        for (Class<?> cls : target.getInterfaces()) {
            resetDataSource(cls, signature.getMethod());
        }
        resetDataSource(target, signature.getMethod());
    }
    /**
     * 后置操作
     *
     */
    public void afterMethod() {
        DynamicDataSourceHolder.removeRouteKey();
    }

    /**
     * 提取目标对象方法注解和类注解中的数据源标识
     */
    private void resetDataSource(Class<?> cls, Method method) {
        try {
            Class<?>[] types = method.getParameterTypes();
            // 默认使用类注解
            if (cls.isAnnotationPresent(DataSourceKey.class)) {
                DataSourceKey source = cls.getAnnotation(DataSourceKey.class);
                DynamicDataSourceHolder.setRouteKey(source.value());
            }
            // 方法注解可以覆盖类注解
            Method m = cls.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSourceKey.class)) {
                DataSourceKey source = m.getAnnotation(DataSourceKey.class);
                DynamicDataSourceHolder.setRouteKey(source.value());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用示意: 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值