[Spring] Using Spring's Abstract Routing Data Source to switch multiple data sources

Keywords: Java JDBC Spring xml Apache

Recently, because the project needs to do data synchronization between two projects, specifically project 1's data is synchronized to project 2 through message queue, because this update operation also involves updating the data of multiple databases, so it needs the operation of switching multiple data sources. Here's how to switch data sources in Spring. Here we use the AbstractRoutingDataSource class to complete the specific operation. AbstractRoutingDataSource was added after Spring 2.0.

The function of data source switching is to customize a class to extend AbstractRouting DataSource Abstract class, which is equivalent to the routing mediation of data source DataSource. It can switch to the corresponding data source DataSource according to the corresponding key value when the project runs. First look at the source code of AbstractRouting Data Source:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    /* List only part of the code */
    private Map<Object, Object> targetDataSources;

    private Object defaultTargetDataSource;

    private boolean lenientFallback = true;

    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

    private Map<Object, DataSource> resolvedDataSources;

    private DataSource resolvedDefaultDataSource;

    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

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

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

    protected abstract Object determineCurrentLookupKey();
}

From the source code, we can see that AbstractRouting DataSource inherits AbstractDataSource and implements Initializing Bean. The getConnection() method of AbstractRouting DataSource calls the method of determineTargetDataSource(). Here we focus on the code of determineTargetDataSource() method. The method uses the LookupKey () method, which is AbstractRouting DataSource. The abstract method of class is also the method to extend the switch of data sources. The return value of this method is the key value of DataSource used in the project. When the key is obtained, the corresponding DataSource can be retrieved from the resolvedDataSource. If the key cannot find the corresponding DataSource, the default data source will be used.

When a custom class extends the AbstractRoutingDataSource class, it overrides the determineCurrentLookupKey() method to implement data source switching. The following is the implementation of the custom extended AbstractRoutingDataSource class:

/**
 * Obtaining data sources
 */
public class MultipleDataSource extends AbstractRoutingDataSource{

    @Override
    protected Object determineCurrentLookupKey() {
         return DynamicDataSourceHolder.getRouteKey();
    }
}

The DynamicDataSourceHolder class implements the operation of data sources as follows:

/**
 * Data Source Operations Class
 */
public class DynamicDataSourceHolder {
    private static ThreadLocal<String> routeKey = new ThreadLocal<String>();

    /**
     * Get the key of the current thread's data source routing
     */
    public static String getRouteKey()
    {
        String key = routeKey.get();
        return key;
    }

    /**
     * key that binds the current thread data source routing
     * You must call the removeRouteKey() method to delete it after using it
     */
    public static void  setRouteKey(String key)
    {
        routeKey.set(key);
    }

    /**
     * Delete the key of the data source routing bound to the current thread
     */
    public static void removeRouteKey()
    {
        routeKey.remove();
    }
}

The following configures multiple data sources in the xml file:

<!-- data source -->
<bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource">
     <property name="driverClassName" value="net.sourceforge.jtds.jdbc.Driver">
     </property>
     <property name="url" value="jdbc:jtds:sqlserver://127.0.0.1;databaseName=test">
     </property>
     <property name="username" value="***"></property>
     <property name="password" value="***"></property>
 </bean>
 <bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource">
     <property name="driverClassName" value="net.sourceforge.jtds.jdbc.Driver">
     </property>
     <property name="url" value="jdbc:jtds:sqlserver://127.0.0.2:1433;databaseName=test">
     </property>
     <property name="username" value="***"></property>
     <property name="password" value="***"></property>
</bean>

<!-- Configure multiple data source mapping -->
<bean id="multipleDataSource" class="MultipleDataSource" >
     <property name="targetDataSources">
         <map key-type="java.lang.String">
             <entry value-ref="dataSource1" key="dataSource1"></entry>
             <entry value-ref="dataSource2" key="dataSource2"></entry>
         </map>
     </property>
     <!-- Default data source -->
     <property name="defaultTargetDataSource" ref="dataSource1" >
     </property>
</bean>

The basic configuration is completed here. Next, just call the method where the data source needs to be switched. Usually, the method is switched before the dao layer operates the database. Just add the following code before the database operation:

DynamicDataSourceHolder.setRouteKey("dataSource2");

In the dao layer, the code for switching data sources is added manually when the data sources need to be switched. AOP can also be used to set the configuration data source types as annotation labels. In the dao layer, annotation labels are written on the methods or classes for switching data sources, which makes the implementation more operable.

@DataSourceKey("dataSource1")
public interface TestEntityMapper extends MSSQLMapper<TestEntity> {
    public void insertTest(TestEntity testEntity);
}

The DataSourceKey annotation code is as follows:

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceKey {
    String value() default "";
}

After the annotation is configured, a class for switching data sources is written, as follows:

public class MultipleDataSourceExchange {

    /** 
     * Intercept the target method, get the data source identifier specified by @DataSource, and set it to the thread store to switch the data source 
     */  
    public void beforeDaoMethod(JoinPoint point) throws Exception {  
        Class<?> target = point.getTarget().getClass();  
        MethodSignature signature = (MethodSignature) point.getSignature();  
        // By default, annotations of the target type are used, and if not, annotation classes that implement the interface are used.  
        for (Class<?> cls : target.getInterfaces()) {  
            resetDataSource(cls, signature.getMethod());  
        }  
        resetDataSource(target, signature.getMethod());  
    }  


    /** 
     * Extracting Data Source Identification in Object Method Annotations and Class Annotations 
     */  
    private void resetDataSource(Class<?> cls, Method method) {  
        try {  
            Class<?>[] types = method.getParameterTypes();  
            // Default class annotations  
            if (cls.isAnnotationPresent(DataSourceKey.class)) {  
                DataSourceKey source = cls.getAnnotation(DataSourceKey.class);  
                DynamicDataSourceHolder.setRouteKey(source.value());  
            }  
            // Method annotations can override class annotations  
            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) {  
            System.out.println(cls + ":" + e.getMessage());  
        }  
    }  
}

After the code is written, configurations are added to the xml configuration file (only a few configurations are listed):

<bean id="multipleDataSourceExchange" class="MultipleDataSourceExchange "/>

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="multipleDataSource" />
</bean>

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
       <tx:method name="insert*" propagation="NESTED" rollback-for="Exception"/>
       <tx:method name="add*" propagation="NESTED" rollback-for="Exception"/>
       ...
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="service" expression="execution(* com.datasource..*.service.*.*(..))"/>
    <!-- Note that switching data sources is performed before persistent layer code -->
    <aop:advisor advice-ref="multipleDataSourceExchange" pointcut-ref="service" order="1"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="service" order="2"/>
</aop:config>

This completes the dynamic switching of multiple data sources using AOP.

Posted by breath18 on Sun, 31 Mar 2019 19:18:30 -0700