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

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

作者: Tom弹架构 | 来源:发表于2021-12-21 13:15 被阅读0次

    本文节选自《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弹架构 』可获取更多技术干货!

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/ljlxqrtx.html