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

简介: 这里简单介绍一下AbstractRoutingDataSource的基本原理。实现数据源切换的功能就是自定义一个类扩展AbstractRoutingDataSource抽象类,其实相当于数据源的路由中介,可以实现在项目运行时根据相应key值切换到对应的DataSource上。先看看AbstractRoutingDataSource类的源码:

本文节选自《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弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!

如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。

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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
3月前
|
缓存 Java 开发者
【Spring】原理:Bean的作用域与生命周期
本文将围绕 Spring Bean 的作用域与生命周期展开深度剖析,系统梳理作用域的类型与应用场景、生命周期的关键阶段与扩展点,并结合实际案例揭示其底层实现原理,为开发者提供从理论到实践的完整指导。
508 22
|
3月前
|
人工智能 Java 开发者
【Spring】原理解析:Spring Boot 自动配置
Spring Boot通过“约定优于配置”的设计理念,自动检测项目依赖并根据这些依赖自动装配相应的Bean,从而解放开发者从繁琐的配置工作中解脱出来,专注于业务逻辑实现。
1302 0
|
2月前
|
XML Java 测试技术
《深入理解Spring》:IoC容器核心原理与实战
Spring IoC通过控制反转与依赖注入实现对象间的解耦,由容器统一管理Bean的生命周期与依赖关系。支持XML、注解和Java配置三种方式,结合作用域、条件化配置与循环依赖处理等机制,提升应用的可维护性与可测试性,是现代Java开发的核心基石。
|
2月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
422 2
|
4月前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
|
5月前
|
缓存 安全 Java
Spring 框架核心原理与实践解析
本文详解 Spring 框架核心知识,包括 IOC(容器管理对象)与 DI(容器注入依赖),以及通过注解(如 @Service、@Autowired)声明 Bean 和注入依赖的方式。阐述了 Bean 的线程安全(默认单例可能有安全问题,需业务避免共享状态或设为 prototype)、作用域(@Scope 注解,常用 singleton、prototype 等)及完整生命周期(实例化、依赖注入、初始化、销毁等步骤)。 解析了循环依赖的解决机制(三级缓存)、AOP 的概念(公共逻辑抽为切面)、底层动态代理(JDK 与 Cglib 的区别)及项目应用(如日志记录)。介绍了事务的实现(基于 AOP
189 0
|
5月前
|
监控 架构师 NoSQL
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
|
Java Spring
spring基于注解配置数据源
spring基于注解配置数据源
102 0
|
druid Java 测试技术
Spring配置数据源
Spring配置数据源
224 0
Spring配置数据源
|
druid Java 关系型数据库
Spring(四、配置数据源)
​命名空间xmlnscontext=“http//www.springframework.org/schema/context”​ 约束路径http//www.springframework.org/schema/context。applicationContext.xml 加载jdbc.properties配置文件获得连接信息。 常见的数据源(连接池)DBCP、C3P0、BoneCP、Druid等。 读取jdbc.properties配置文件创建连接池。 创建Druid连接池。
304 0
Spring(四、配置数据源)