写点什么

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

作者:Tom弹架构
  • 2021 年 12 月 21 日
  • 本文字数:7798 字

    阅读完需:约 26 分钟

本文节选自《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")@Datapublic 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")@Datapublic 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;
@Repositorypublic 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;

@Repositorypublic 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.Driverdb2018.mysql.jdbc.url=jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-2018?characterEncoding=UTF-8&rewriteBatchedStatements=truedb2018.mysql.jdbc.username=rootdb2018.mysql.jdbc.password=123456
db2019.mysql.jdbc.driverClassName=com.mysql.jdbc.Driverdb2019.mysql.jdbc.url=jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-2019?characterEncoding=UTF-8&rewriteBatchedStatements=truedb2019.mysql.jdbc.username=rootdb2019.mysql.jdbc.password=123456
#alibaba druid configdbPool.initialSize=1dbPool.minIdle=1dbPool.maxActive=200dbPool.maxWait=60000dbPool.timeBetweenEvictionRunsMillis=60000dbPool.minEvictableIdleTimeMillis=300000dbPool.validationQuery=SELECT 'x' dbPool.testWhileIdle=truedbPool.testOnBorrow=falsedbPool.testOnReturn=falsedbPool.poolPreparedStatements=falsedbPool.maxPoolPreparedStatementPerConnectionSize=20dbPool.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 弹架构 』可获取更多技术干货!


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

发布于: 1 小时前阅读数: 5
用户头像

Tom弹架构

关注

不只做一个技术者,更要做一个思考者 2021.10.22 加入

畅销书作者,代表作品:《Spring 5核心原理》、《Netty 4核心原理》、《设计模式就该这样学》

评论

发布
暂无评论
30个类手写Spring核心原理之动态数据源切换(8)