Annotation-based Spring multiple data source configuration and use

Some time ago, I studied the configuration and use of multiple data sources in spring, in order to prepare for data analysis and report statistics by pulling data from multiple data sources at a later stage. Since the projects I have done before are all single data sources, I have never encountered such a scenario, so I have never learned how to configure multiple data sources.
Later, I found that it is relatively simple to configure and use multiple data sources based on spring, because the spring framework has reserved such an interface to facilitate the switching of data sources.
Let's take a look at the source code of spring to get the data source:

It can be seen that the AbstractRoutingDataSource will call the determineCurrentLookupKey method to find the current lookupKey before obtaining the data source. This lookupKey is the data source identifier.
Therefore, by overriding this method of finding the data source identifier, spring can switch to the specified data source.
Step 1: Create a DynamicDataSource class, inherit AbstractRoutingDataSource and override the determineCurrentLookupKey method. The code is as follows:

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();
    }
}

Step 2: Create a DynamicDataSourceHolder to hold the data source identifier used in the current thread. The code is as follows:

/**
 * 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();
    }
}

Step 3: Configure multiple data sources and the DynamicDataSource bean created in the first step. The simplified configuration is as follows:

<!-- 阿里巴巴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>

At this point, multiple data sources can be used. Before operating the database, just DynamicDataSourceHolder.setDataSource("dataSource2") can switch to data source 2 and operate the database db2.

The sample code is as follows:

@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();
     }
 }

-------------------------------------------- Gorgeous dividing line-- ---------------------------------

But here comes the problem. If you call DynamicDataSourceHolder.setDataSource("xxx") every time you switch the data source, it will be very cumbersome, and the amount of code will be easily missed, and it will be more troublesome to maintain later. Can you directly specify the data source to be accessed through annotations, for example, use @DataSource("xxx") in the dao layer to specify the access data source xxx? of course can! The premise is, add a little extra configuration ^_^.
First, we have to define an annotation called DataSource with the following code:

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();
}

Then, define the AOP aspect to intercept all methods annotated with @DataSource, take out the value of the annotation as the data source identifier and put it in the thread variable of 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);

        }
    }
}

Finally, you can configure the interception rules in the spring configuration file, such as intercepting all methods of the service layer or the dao layer:

<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, so you can use the annotation @DataSource directly on the class or method to specify the data source, and you don't need to manually set it every time.

The sample code is as follows:

@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();
    }

}

Tip: The annotation @DataSource can be added to the method, or to the interface or the implementation class of the interface. Priority: method>implementation class>interface. That is to say, if the @DataSource annotation is added to the interface, the interface implementation class, and the method to specify the data source, the one specified on the method shall prevail.

Note: If you access multiple data sources in the Service layer, it may be inconvenient to annotate the data sources in the class. You can put @DataSource in the dao layer, for example:

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);
}

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325048779&siteId=291194637