7

30个类手写Spring核心原理之动态数据源切换(8)

 2 years ago
source link: https://my.oschina.net/gupaoedutom/blog/5375917
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文节选自《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弹架构 』回复“Spring”可获取完整源码。

Tom弹架构

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注微信公众号『 Tom弹架构 』可获取更多技术干货!

原创不易,坚持很酷,都看到这里了,小伙伴记得点赞、收藏、在看,一键三连加关注!如果你觉得内容太干,可以分享转发给朋友滋润滋润!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK