SSM動態切換數據源




有需求就要想辦法解決,最近參與的項目其涉及的三個數據表分別在三台不同的服務器上,這就有點突兀了,第一次遇到這種情況,可這難不倒筆者,資料一查,代碼一打,回頭看看源碼,萬事大吉


1. 預備知識

這里默認大家都會SSM框架了,使用時我們要往sqlSessionFactory里注入數據源。那么猜測:1、可以往sqlSessionFactory里注入多數據源來實現切換;2、將多個數據源封裝成一個總源,再把這個總源注入到sqlSessionFactory里實現切換。答案是使用后者,即封裝成總源的形式。Spring提供了動態切換數據源的功能,那么我們來看看其實現原理





2. 實現原理

筆者是根據源碼講解的,這些步驟講完會貼出源碼內容


一、

Spring提供了AbstractRoutingDataSource抽象類,其繼承了AbstractDataSource。而AbstractDataSource又實現了DataSource。因此我們可以將AbstractRoutingDataSource的實現類注入到sqlSessionFactory中來實現切換數據源


二、

剛才我們將多個數據源封裝成總源的想法在AbstractRoutingDataSource中有體現,其內部用一個Map集合封裝多個數據源,即 private Map<Object, DataSource> resolvedDataSources; ,那么要使用時從該Map集合中獲取即可


三、

AbstractRoutingDataSource中有個determineTargetDataSource()方法,其作用是決定使用哪個數據源。我們通過determineTargetDataSource()方法從Map集合中獲取數據源,那么必須有個key值指定才行。所以determineTargetDataSource()方法內部通過調用determineCurrentLookupKey()方法來獲取key值,Spring將determineCurrentLookupKey()方法抽象出來給用戶實現,從而讓用戶決定使用哪個數據源


四、

既然知道我們需要重寫determineCurrentLookupKey()方法,那么就開始把。實現時發現該方法沒有參數,我們無法傳參來決定返回的key值,又不能改動方法(因為是重寫),所以方法內部調用我們自定義類的靜態方法即可解決問題

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDataSourceKey();
    }
}

五、

自定義類,作用是讓我們傳入key值來決定使用哪個key

public class DynamicDataSourceHolder {

    // ThreadLocal沒什么好說的,綁定當前線程
    private static final ThreadLocal<String> dataSourceKey = new ThreadLocal<String>();

    public static String getDataSourceKey(){
        return dataSourceKey.get();
    }

    public static void setDataSourceKey(String key){
        dataSourceKey.set(key);
    }

    public static void clearDataSourceKey(){
        dataSourceKey.remove();
    }
}

六、

AbstractRoutingDataSource抽象類源碼(不喜可跳

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

    public AbstractRoutingDataSource() {
    }

    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    public void setLenientFallback(boolean lenientFallback) {
        this.lenientFallback = lenientFallback;
    }

    public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
        this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
    }

    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }

    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
        return lookupKey;
    }

    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
        if (dataSource instanceof DataSource) {
            return (DataSource)dataSource;
        } else if (dataSource instanceof String) {
            return this.dataSourceLookup.getDataSource((String)dataSource);
        } else {
            throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
        }
    }

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

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

    public <T> T unwrap(Class<T> iface) throws SQLException {
        return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface);
    }

    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
    }

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource 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 + "]");
        } else {
            return dataSource;
        }
    }

    @Nullable
    protected abstract Object determineCurrentLookupKey();
}




3. 配置


3.1 配置db.properties

這里配置兩個數據庫,一個評論庫,一個用戶庫

# 問題庫
howl.comments.driverClassName = com.mysql.jdbc.Driver
howl.comments.url = jdbc:mysql://127.0.0.1:3306/comment
howl.comments.username = root
howl.comments.password =

# 用戶庫
howl.users.driverClassName = com.mysql.jdbc.Driver
howl.users.url = jdbc:mysql://127.0.0.1:3306/user
howl.users.username = root
howl.users.password =

3.2 配置applicationContext.xml

<!--  加載properties文件  -->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>


<!--  問題的數據源  -->
<bean id="commentsDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<property name="driverClassName" value="${howl.comments.driverClassName}"></property>
	<property name="url" value="${howl.comments.url}"></property>
    <property name="username" value="${howl.comments.username}"></property>
    <property name="password" value="${howl.comments.password}"></property>
</bean>


<!--  用戶的數據源  -->
<bean id="usersDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<property name="driverClassName" value="${howl.users.driverClassName}"></property>
    <property name="url" value="${howl.users.url}"></property>
    <property name="username" value="${howl.users.username}"></property>
    <property name="password" value="${howl.users.password}"></property>
</bean>


<!--  通過setter方法,往DynamicDataSource的Map集合中注入數據  -->
<!--  具體參數,看名字可以明白  -->
<bean id="dynamicDataSource" class="com.howl.util.DynamicDataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry key="cds" value-ref="commentsDataSource"/>
            <entry key="uds" value-ref="usersDataSource"/>
        </map>
    </property>
    <property name="defaultTargetDataSource" ref="commentsDataSource"></property>
</bean>


<!--  將`總源`注入SqlSessionFactory工廠  -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="configLocation" value="classpath:mybatis-config.xml"></property>
    <property name="dataSource" ref="dynamicDataSource"></property>
</bean>

因為dynamicDataSource是繼承AbstractRoutingDataSource,所以setter注入方法得去父類里面去找,開始筆者也是懵了一下


3.3 切換數據源

數據源是在Service層切換的


UserService

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public User selectUserById(int id) {

        // 表明使用usersDataSource庫
        DynamicDataSourceHolder.setDataSourceKey("uds");
        return userDao.selectUserById(id);
    }
}

CommentService

@Service
public class CommentService {

    @Autowired
    CommentDao commentDao;

    public List<Comment> selectCommentById(int blogId) {

        // 表明使用評論庫
        DynamicDataSourceHolder.setDataSourceKey("cds");
        return commentDao.selectCommentById(blogId, -1);
    }
}

3.4 自動切換

手動切換容易忘記,我們學了AOP可以使用AOP來切換,這里使用注解實現


<!-- 開啟AOP注解支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

切面類

@Component
@Aspect
public class DataSourceAspect {

    @Pointcut("execution(* com.howl.service.impl.*(..))")
    private void pt1() {
    }

    @Around("pt1()")
    public Object around(ProceedingJoinPoint pjp) {

        Object rtValue = null;
        try {
            String name = pjp.getTarget().getClass().getName();
            if (name.equals("com.howl.service.UserService")) {
                DynamicDataSourceHolder.setDataSourceKey("uds");
            }
            if (name.equals("com.howl.service.CommentService")){
                DynamicDataSourceHolder.setDataSourceKey("cds");
            }
            // 調用業務層方法
            rtValue = pjp.proceed();

            System.out.println("后置通知");
        } catch (Throwable t) {
            System.out.println("異常通知");
            t.printStackTrace();
        } finally {
            System.out.println("最終通知");
        }
        return rtValue;
    }
}

使用環繞通知實現切入com.howl.service.impl里的所有方法,在遇到UserService、CommentService時,前置通知動態切換對應的數據源





4. 總結

  1. 以前筆者認為Service層多了impl包和接口是多余的,現在要用到AOP的時候后悔莫及,所以默認結構如此肯定有道理的
  2. 出bug的時候,才知道分步測試哪里出問題了,如果TDD推動那么能快速定位報錯地方,日志也很重要


參考

https://www.jianshu.com/p/d97cd60e404f




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM