基于注解的Spring多数据源配置和使用

前一段时间研究了一下spring多数据源的配置和使用,为了后期从多个数据源拉取数据定时进行数据分析和报表统计做准备。由于之前做过的项目都是单数据源的,没有遇到这种场景,所以也一直没有去了解过如何配置多数据源。
后来发现其实基于spring来配置和使用多数据源还是比较简单的,因为spring框架已经预留了这样的接口可以方便数据源的切换。
先看一下spring获取数据源的源码:

可以看到AbstractRoutingDataSource获取数据源之前会先调用determineCurrentLookupKey方法查找当前的lookupKey,这个lookupKey就是数据源标识。
因此通过重写这个查找数据源标识的方法就可以让spring切换到指定的数据源了。
第一步:创建一个DynamicDataSource的类,继承AbstractRoutingDataSource并重写determineCurrentLookupKey方法,代码如下:

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;

/**
 * Created by WJ on 2018/3/23 0023.
 */
public class DynamicDataSource extends AbstractRoutingDataSource {


    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        // 从自定义的位置获取数据源标识
        return DynamicDataSourceHolder.getDataSource();
    }
}

第二步:创建DynamicDataSourceHolder用于持有当前线程中使用的数据源标识,代码如下:

/**
 * Created by WJ on 2018/3/23 0023.
 */
public class DynamicDataSourceHolder {

    /**
     * 注意:数据源标识保存在线程变量中,避免多线程操作数据源时互相干扰
     */
    private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<String>();

    public static String getDataSource() {
        return THREAD_DATA_SOURCE.get();
    }

    public static void setDataSource(String dataSource) {
        THREAD_DATA_SOURCE.set(dataSource);
    }

    public static void clearDataSource() {
        THREAD_DATA_SOURCE.remove();
    }
}

第三步:配置多个数据源和第一步里创建的DynamicDataSource的bean,简化的配置如下:

<!-- 阿里巴巴Druid数据库连接池 -->
<!--创建数据源1,连接数据库db1 -->
<bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 基本属性 url、user、password -->
    <property name="url" value="${db1.jdbc.url}" />
    <property name="username" value="${db1.jdbc.user}" />
    <property name="password" value="${db1.jdbc.password}" />

    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="1" />
    <property name="minIdle" value="1" />
    <property name="maxActive" value="${jdbc.pool.maxActive}" />

    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="60000" />

    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="60000" />

    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="300000" />

    <property name="validationQuery" value="select 1 from dual" />
    <property name="testWhileIdle" value="true" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />

    <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
    <property name="poolPreparedStatements" value="false" />
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />

    <!-- 配置druid监控统计拦截的filters -->
    <!-- stat 统计监控信息,wall 防sql注入,slf4j 日志打印 -->
    <!--
     <property name="filters" value="stat,wall,slf4j,config"/>
     -->
    <property name="filters" value="stat,slf4j,config"/>
    <!-- 解压数据库连接密码 -->
    <property name="connectionProperties" value="config.decrypt=false" />
</bean>

<!--创建数据源2,连接数据库db2 -->
<bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 基本属性 url、user、password -->
    <property name="url" value="${db2.jdbc.url}" />
    <property name="username" value="${db2.jdbc.user}" />
    <property name="password" value="${db2.jdbc.password}" />

    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="1" />
    <property name="minIdle" value="1" />
    <property name="maxActive" value="${jdbc.pool.maxActive}" />

    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="60000" />

    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="60000" />

    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="300000" />

    <property name="validationQuery" value="select 1 from dual" />
    <property name="testWhileIdle" value="true" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />

    <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
    <property name="poolPreparedStatements" value="false" />
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />

    <!-- 配置druid监控统计拦截的filters -->
    <!-- stat 统计监控信息,wall 防sql注入,slf4j 日志打印 -->
    <!--
     <property name="filters" value="stat,wall,slf4j,config"/>
     -->
    <property name="filters" value="stat,slf4j,config"/>
    <!-- 解压数据库连接密码 -->
    <property name="connectionProperties" value="config.decrypt=false" />
</bean>


<bean id="dynamicDataSource" class="com.keeprisk.core.support.DynamicDataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <!-- 指定lookupKey和与之对应的数据源 -->
            <entry key="dataSource1" value-ref="dataSource1"></entry>
            <entry key="dataSource2" value-ref="dataSource2"></entry>
        </map>
    </property>
    <!-- 这里可以指定默认的数据源 -->
    <property name="defaultTargetDataSource" ref="dataSource1" />
</bean>

到这里已经可以使用多数据源了,在操作数据库之前只要DynamicDataSourceHolder.setDataSource("dataSource2")即可切换到数据源2并对数据库db2进行操作了。

示例代码如下:

@Service
public class DataServiceImpl implements DataService {
     @Autowired
     private DataMapper dataMapper;
 
    @Override
     public List<Map<String, Object>> getList1() {
         // 没有指定,则默认使用数据源1
         return dataMapper.getList1();
     }
 
     @Override
     public List<Map<String, Object>> getList2() {
         // 指定切换到数据源2
         DynamicDataSourceHolder.setDataSource("dataSource2");
         return dataMapper.getList2();
     }
 }

--------------------------------------------华丽的分割线-----------------------------------

但是问题来了,如果每次切换数据源时都调用DynamicDataSourceHolder.setDataSource("xxx")就显得十分繁琐了,而且代码量大了很容易会遗漏,后期维护起来也比较麻烦。能不能直接通过注解的方式指定需要访问的数据源呢,比如在dao层使用@DataSource("xxx")就指定访问数据源xxx?当然可以!前提是,再加一点额外的配置^_^。
首先,我们得定义一个名为DataSource的注解,代码如下:

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Created by WJ on 2018/3/23 0023.
 */
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
public @interface DataSource {
    String value();
}

然后,定义AOP切面以便拦截所有带有注解@DataSource的方法,取出注解的值作为数据源标识放到DynamicDataSourceHolder的线程变量中:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;

/**
 * Created by WJ on 2018/3/23 0023.
 */
public class DataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * 拦截目标方法,获取由@DataSource指定的数据源标识,设置到线程存储中以便切换数据源
     *
     * @param point
     * @throws Exception
     */
    public void intercept(JoinPoint point) throws Exception {
        Class<?> target = point.getTarget().getClass();
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 默认使用目标类型的注解,如果没有则使用其实现接口的注解
        for (Class<?> clazz : target.getInterfaces()) {
            resolveDataSource(clazz, signature.getMethod());
        }
        resolveDataSource(target, signature.getMethod());
    }

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

        }
    }
}

最后在spring配置文件中配置拦截规则就可以了,比如拦截service层或者dao层的所有方法:

<bean id="dataSourceAspect" class="com.keeprisk.core.support.DataSourceAspect"/>

<aop:config>
    <aop:aspect ref="dataSourceAspect">
        <!-- 拦截所有dao方法 -->
        <aop:pointcut id="dataSourcePointcut" expression="execution(* com.keeprisk.core.dao.*.*(..))"/>
        <aop:before pointcut-ref="dataSourcePointcut" method="intercept" />
    </aop:aspect>
</aop:config>

OK,这样就可以直接在类或者方法上使用注解@DataSource来指定数据源,不需要每次都手动设置了。

示例代码如下:

@Service
// 默认DataServiceImpl下的所有方法均访问数据源1
@DataSource("dataSource1")
public class DataServiceImpl implements DataService {
    @Autowired
    private DataMapper dataMapper;

    @Override
    public List<Map<String, Object>> getList1() {
        // 不指定,则默认使用数据源1
        return dataMapper.getList1();
    }

    @Override
    // 覆盖类上指定的,使用数据源2
    @DataSource("dataSource2")
    public List<Map<String, Object>> getList2() {
        return dataMapper.getList2();
    }

}

提示:注解@DataSource既可以加在方法上,也可以加在接口或者接口的实现类上,优先级别:方法>实现类>接口。也就是说如果接口、接口实现类以及方法上分别加了@DataSource注解来指定数据源,则优先以方法上指定的为准。

说明:若在Service层访问多个数据源,在类中注解上数据源,可能不太方便,所有可以将@DataSource 放在dao层,例如:

import com.keeprisk.core.domain.WeixinCity;
import com.keeprisk.core.support.DataSource;

@DataSource("dataSource1")
public interface WeixinCityMapper {
    int deleteByPrimaryKey(Integer id);

    int insertSelective(WeixinCity record);

    WeixinCity selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(WeixinCity record);
    
    List<WeixinCity> selectByPage(WeixinCity record);
}

猜你喜欢

转载自my.oschina.net/u/3485980/blog/1797817
今日推荐