30個類手寫Spring核心原理之動態資料來源切換(8)

Tom彈架構發表於2021-12-21

本文節選自《Spring 5核心原理》

閱讀本文之前,請先閱讀以下內容:

30個類手寫Spring核心原理之自定義ORM(上)(6)

30個類手寫Spring核心原理之自定義ORM(下)(7)

4 動態資料來源切換的底層原理

這裡簡單介紹一下AbstractRoutingDataSource的基本原理。實現資料來源切換的功能就是自定義一個類擴充套件AbstractRoutingDataSource抽象類,其實相當於資料來源的路由中介,可以實現在專案執行時根據相應key值切換到對應的DataSource上。先看看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 Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

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

    ...

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

可以看出,AbstractRoutingDataSource類繼承了AbstractDataSource類,並實現了InitializingBean。AbstractRoutingDataSource類的getConnection()方法呼叫了determineTargetDataSource()的該方法。這裡重點看determineTargetDataSource()方法的程式碼,它使用了determineCurrentLookupKey()方法,它是AbstractRoutingDataSource類的抽象方法,也是實現資料來源切換擴充套件的方法。該方法的返回值就是專案中所要用的DataSource的key值,得到該key值後就可以在resolvedDataSource中取出對應的DataSource,如果找不到key對應的DataSource就使用預設的資料來源。
自定義類擴充套件AbstractRoutingDataSource類時要重寫determineCurrentLookupKey()方法來實現資料來源切換。

4.1 DynamicDataSource

DynamicDataSource類封裝自定義資料來源,繼承原生Spring的AbstractRoutingDataSource類的資料來源動態路由器。


package javax.core.common.jdbc.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/** 
 * 動態資料來源 
 */  
public class DynamicDataSource extends AbstractRoutingDataSource {  
   private DynamicDataSourceEntry dataSourceEntry;  
    @Override  
    protected Object determineCurrentLookupKey() {
        return this.dataSourceEntry.get();  
    }  
    public void setDataSourceEntry(DynamicDataSourceEntry dataSourceEntry) {  
        this.dataSourceEntry = dataSourceEntry;
    }
    public DynamicDataSourceEntry getDataSourceEntry(){
          return this.dataSourceEntry;
    }
}

4.2 DynamicDataSourceEntry

DynamicDataSourceEntry類實現對資料來源的操作功能,程式碼如下:


package javax.core.common.jdbc.datasource;

import org.aspectj.lang.JoinPoint;

/**
 * 動態切換資料來源
 */
public class DynamicDataSourceEntry {
   
   //預設資料來源  
    public final static String DEFAULT_SOURCE = null;  
  
    private final static ThreadLocal<String> local = new ThreadLocal<String>();  
   
    /** 
     * 清空資料來源 
     */  
    public void clear() {  
        local.remove();
    }  
    
    /** 
     * 獲取當前正在使用的資料來源的名字
     *  
     * @return String 
     */  
    public String get() {  
         return local.get();  
    }  
  
    /** 
     * 還原指定切面的資料來源 
     *  
     * @param joinPoint 
     */
    public void restore(JoinPoint join) {  
        local.set(DEFAULT_SOURCE);  
    }
    
    /**
     * 還原當前切面的資料來源
     */
    public void restore() {  
        local.set(DEFAULT_SOURCE);
    }  
  
    /** 
     * 設定已知名字的資料來源 
     *  
     * @param dataSource 
     */  
    public void set(String source) {  
        local.set(source); 
    }

    /**
     * 根據年份動態設定資料來源
     * @param year
     */
   public void set(int year) {
      local.set("DB_" + year);
   }
}

5 執行效果演示

5.1 建立Member實體類

建立Member實體類程式碼如下:


package com.tom.orm.demo.entity;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name="t_member")
@Data
public class Member implements Serializable {
    @Id private Long id;
    private String name;
    private String addr;
    private Integer age;

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", addr='" + addr + '\'' +
                ", age=" + age +
                '}';
    }
}

5.2 建立Order實體類

建立Order實體類程式碼如下:


package com.tom.orm.demo.entity;

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name="t_order")
@Data
public class Order implements Serializable {
    private Long id;
    @Column(name="mid")
    private Long memberId;
    private String detail;
    private Long createTime;
    private String createTimeFmt;

    @Override
    public String toString() {
        return "Order{" +
                "id=" + id +
                ", memberId=" + memberId +
                ", detail='" + detail + '\'' +
                ", createTime=" + createTime +
                ", createTimeFmt='" + createTimeFmt + '\'' +
                '}';
    }
}

5.3 建立MemberDao

建立MemberDao程式碼如下:


package com.tom.orm.demo.dao;

import com.tom.orm.demo.entity.Member;
import com.tom.orm.framework.BaseDaoSupport;
import com.tom.orm.framework.QueryRule;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;

@Repository
public class MemberDao extends BaseDaoSupport<Member,Long> {
    
    @Override
    protected String getPKColumn() {
        return "id";
    }

    @Resource(name="dataSource")
    public void setDataSource(DataSource dataSource){
        super.setDataSourceReadOnly(dataSource);
        super.setDataSourceWrite(dataSource);
    }


    public List<Member> selectAll() throws  Exception{
        QueryRule queryRule = QueryRule.getInstance();
        queryRule.andLike("name","Tom%");
        return super.select(queryRule);
    }
}

5.4 建立OrderDao

建立OrderDao程式碼如下:


package com.tom.orm.demo.dao;

import com.tom.orm.demo.entity.Order;
import com.tom.orm.framework.BaseDaoSupport;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import javax.core.common.jdbc.datasource.DynamicDataSource;
import javax.sql.DataSource;
import java.text.SimpleDateFormat;
import java.util.Date;


@Repository
public class OrderDao extends BaseDaoSupport<Order, Long> {

   private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
   private SimpleDateFormat fullDataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
   private DynamicDataSource dataSource;
   @Override
   protected String getPKColumn() {return "id";}

   @Resource(name="dynamicDataSource")
   public void setDataSource(DataSource dataSource) {
      this.dataSource = (DynamicDataSource)dataSource;
      this.setDataSourceReadOnly(dataSource);
      this.setDataSourceWrite(dataSource);
   }

   /**
    * @throws Exception
    *
    */
   public boolean insertOne(Order order) throws Exception{
      //約定優於配置
      Date date = null;
      if(order.getCreateTime() == null){
         date = new Date();
         order.setCreateTime(date.getTime());
      }else {
         date = new Date(order.getCreateTime());
      }
      Integer dbRouter = Integer.valueOf(yearFormat.format(date));
      System.out.println("自動分配到【DB_" + dbRouter + "】資料來源");
      this.dataSource.getDataSourceEntry().set(dbRouter);

      order.setCreateTimeFmt(fullDataFormat.format(date));

      Long orderId = super.insertAndReturnId(order);
      order.setId(orderId);
      return orderId > 0;
   }

   
}

5.5 修改db.properties檔案

修改db.properties檔案程式碼如下:


#sysbase database mysql config

#mysql.jdbc.driverClassName=com.mysql.jdbc.Driver
#mysql.jdbc.url=jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-demo?characterEncoding=UTF-8&rewriteBatchedStatements=true
#mysql.jdbc.username=root
#mysql.jdbc.password=123456

db2018.mysql.jdbc.driverClassName=com.mysql.jdbc.Driver
db2018.mysql.jdbc.url=jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-2018?characterEncoding=UTF-8&rewriteBatchedStatements=true
db2018.mysql.jdbc.username=root
db2018.mysql.jdbc.password=123456

db2019.mysql.jdbc.driverClassName=com.mysql.jdbc.Driver
db2019.mysql.jdbc.url=jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-2019?characterEncoding=UTF-8&rewriteBatchedStatements=true
db2019.mysql.jdbc.username=root
db2019.mysql.jdbc.password=123456

#alibaba druid config
dbPool.initialSize=1
dbPool.minIdle=1
dbPool.maxActive=200
dbPool.maxWait=60000
dbPool.timeBetweenEvictionRunsMillis=60000
dbPool.minEvictableIdleTimeMillis=300000
dbPool.validationQuery=SELECT 'x' 
dbPool.testWhileIdle=true
dbPool.testOnBorrow=false
dbPool.testOnReturn=false
dbPool.poolPreparedStatements=false
dbPool.maxPoolPreparedStatementPerConnectionSize=20
dbPool.filters=stat,log4j,wall

5.6 修改application-db.xml檔案

修改application-db.xml檔案程式碼如下:


<bean id="datasourcePool" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
   <property name="initialSize" value="${dbPool.initialSize}" />
   <property name="minIdle" value="${dbPool.minIdle}" />
   <property name="maxActive" value="${dbPool.maxActive}" />
   <property name="maxWait" value="${dbPool.maxWait}" />
   <property name="timeBetweenEvictionRunsMillis" value="${dbPool.timeBetweenEvictionRunsMillis}" />
   <property name="minEvictableIdleTimeMillis" value="${dbPool.minEvictableIdleTimeMillis}" />
   <property name="validationQuery" value="${dbPool.validationQuery}" />
   <property name="testWhileIdle" value="${dbPool.testWhileIdle}" />
   <property name="testOnBorrow" value="${dbPool.testOnBorrow}" />
   <property name="testOnReturn" value="${dbPool.testOnReturn}" />
   <property name="poolPreparedStatements" value="${dbPool.poolPreparedStatements}" />
   <property name="maxPoolPreparedStatementPerConnectionSize" value="${dbPool.maxPoolPreparedStatementPerConnectionSize}" />
   <property name="filters" value="${dbPool.filters}" />
</bean>

<bean id="dataSource" parent="datasourcePool">
   <property name="driverClassName" value="${db2019.mysql.jdbc.driverClassName}" />
   <property name="url" value="${db2019.mysql.jdbc.url}" />
   <property name="username" value="${db2019.mysql.jdbc.username}" />
   <property name="password" value="${db2019.mysql.jdbc.password}" />
</bean>

<bean id="dataSource2018" parent="datasourcePool">
   <property name="driverClassName" value="${db2018.mysql.jdbc.driverClassName}" />
   <property name="url" value="${db2018.mysql.jdbc.url}" />
   <property name="username" value="${db2018.mysql.jdbc.username}" />
   <property name="password" value="${db2018.mysql.jdbc.password}" />
</bean>


<bean id="dynamicDataSourceEntry"  class="javax.core.common.jdbc.datasource.DynamicDataSourceEntry" />

<bean id="dynamicDataSource" class="javax.core.common.jdbc.datasource.DynamicDataSource" >
   <property name="dataSourceEntry" ref="dynamicDataSourceEntry"></property>
   <property name="targetDataSources">
      <map>
         <entry key="DB_2019" value-ref="dataSource"></entry>
         <entry key="DB_2018" value-ref="dataSource2018"></entry>
      </map>
   </property>
   <property name="defaultTargetDataSource" ref="dataSource" />
</bean>

5.7 編寫測試用例

編寫測試用例程式碼如下:


package com.tom.orm.test;

import com.tom.orm.demo.dao.MemberDao;
import com.tom.orm.demo.dao.OrderDao;
import com.tom.orm.demo.entity.Member;
import com.tom.orm.demo.entity.Order;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

@ContextConfiguration(locations = {"classpath:application-context.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class OrmTest {

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmdd");

    @Autowired private MemberDao memberDao;

    @Autowired private OrderDao orderDao;

    @Test
    public void testSelectAllForMember(){
        try {
            List<Member> result = memberDao.selectAll();
            System.out.println(Arrays.toString(result.toArray()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test
    @Ignore
    public void testInsertMember(){
        try {
            for (int age = 25; age < 35; age++) {
                Member member = new Member();
                member.setAge(age);
                member.setName("Tom");
                member.setAddr("Hunan Changsha");
                memberDao.insert(member);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }


    @Test
// @Ignore
    public void testInsertOrder(){
        try {
            Order order = new Order();
            order.setMemberId(1L);
            order.setDetail("歷史訂單");
            Date date = sdf.parse("20180201123456");
            order.setCreateTime(date.getTime());
            orderDao.insertOne(order);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

所謂ORM就是,物件關係對映,Object Relation Mapping,市面上ORM框架也非常多,比如Hibernate、Spring JDBC、MyBatis、JPA,它們都有物件關係管理的機制比如一對多、多對多、一對一關係。以上思路僅供參考,有興趣的小夥伴可以參考本文提供的思想,約定優於配置,先制定頂層介面,引數返回值全部統一,比如:


    //List<?> Page<?>  select(QueryRule queryRule)
    //Int delete(T entity) entity中的ID不能為空,如果ID為空,其他條件不能為空,都為空不予執行
    //ReturnId  insert(T entity) 只要entity不等於null
    //Int update(T entity) entity中的ID不能為空,如果ID為空,其他條件不能為空,都為空不予執行

然後在此基礎上進行擴充套件,基於Spring JDBC封裝一套,基於Redis封裝一套,基於MongoDB封裝一套,基於ElasticSearch封裝一套,基於Hive封裝一套,基於HBase封裝一套。本文完整地演示了自研ORM框架的原理,以及資料來源動態切換的基本原理,並且瞭解了Spring JdbcTemplate的API應用。希望通過本章的學習,“小夥伴們”在日常工作中能夠有更好的解決問題的思路,提高工作效率。

本文為“Tom彈架構”原創,轉載請註明出處。技術在於分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力。

原創不易,堅持很酷,都看到這裡了,小夥伴記得點贊、收藏、在看,一鍵三連加關注!如果你覺得內容太乾,可以分享轉發給朋友滋潤滋潤!

相關文章