SimpleDriverDataSource
和java.sql.Driver强相关。它直接继承自AbstractDriverBasedDataSource。它表示一个简单的数据源,每次获取Connection时,会重新建立一个Connection。通过Driver来获取Connection对象。 获取代码如下:
Connection connection = driver.connect(url, props);
以上实现类基于AbstractDriverBasedDataSource的实现方式,**当然也能做为管理多数据源的方案。**这就是我想说的方式二,具体详细代码省略。
注意,注意,注意:在性能要求不高的情况,可以试试直接使用它们玩玩。否则请务必使用连接池技术提升性能~
我一般只会把此种方式放在测试上~~
上面已经介绍了管理多数据源的两种方式,但都有弊端,真正使用起来也稍显麻烦。
接下来介绍的这种方式是使用最广泛也是本文的主菜~~~
方式三:AbstractRoutingDataSource动态切换数据源
在基于三层的后端架构中,操作数据库的是Dao层。比如现在我们一般使用SSM框架。若我们像上面两种方式操作多数据源,首先最大的缺点就是代码入侵性强、便管理。
此处我们还有一个方法,也就是使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样的入侵性较低,非常好的满足使用的需求。比如我们希望对于读写分离或者其他的数据同步的业务场景。
如上图,使用AbstractRoutingDataSource的实现类,进行灵活的切换,可以通过AOP或者手动编程设置当前的DataSource。这样的编写方式比较好,至于其中的实现原理是下面会分析的重点
AbstractRoutingDataSource 是个抽象类,具体实现需要调用者书写实现类的。
Spring 2.0.1引入了一个AbstractRoutingDataSource ,我相信这值得关注。
// @since 2.0.1 注意起始版本(该版本2006见发布) // 并且实现了InitializingBean 接口 public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { // 根据key 缓存下来其对应的DataSource~~~~ @Nullable private Map<Object, Object> targetDataSources; @Nullable private Object defaultTargetDataSource; // 默认数据源 // 如果找不到当前查找键的特定数据源,请指定是否对默认数据源应用宽限回退。 private boolean lenientFallback = true; // DataSourceLookup为一个函数式接口 只有一个方法DataSource getDataSource(String dataSourceName) // 此处使用的默认实现为JNDI private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); @Nullable private Map<Object, DataSource> resolvedDataSources; @Nullable private DataSource resolvedDefaultDataSource; ... // 省略所有的set方法 @Override public void afterPropertiesSet() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); } this.resolvedDataSources = new HashMap<>(this.targetDataSources.size()); // 遍历设置进来的目标数据源们~~~~ this.targetDataSources.forEach((key, value) -> { Object lookupKey = resolveSpecifiedLookupKey(key); DataSource dataSource = resolveSpecifiedDataSource(value); // 把已经解决好的缓存起来(注意key和value和上有可能就是不同的了) // 注意:key可以是个Object 而不一定只能是String类型 this.resolvedDataSources.put(lookupKey, dataSource); }); if (this.defaultTargetDataSource != null) { this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); } } // 子类IsolationLevelDataSourceRouter有复写此方法 // 绝大多数情况下,我们会直接使用DataSource protected Object resolveSpecifiedLookupKey(Object lookupKey) { return lookupKey; } // 此处兼容String类型,若是string就使用dataSourceLookup去查找(默认是JNDI) protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { if (dataSource instanceof DataSource) { return (DataSource) dataSource; } else if (dataSource instanceof String) { return this.dataSourceLookup.getDataSource((String) dataSource); } else { throw new IllegalArgumentException( "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); } } /链接数据库 @Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); } protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); // 若根据key没有找到dataSource,并且lenientFallback=true或者lookupKey == null 那就回滚到使用默认的数据源 // 备注:此处可以看出key=null和this.lenientFallback =true有一样的效果 if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; } // 子类必须实现的抽象方法:提供key即可~~~~ @Nullable protected abstract Object determineCurrentLookupKey(); @Override @SuppressWarnings("unchecked") public <T> T unwrap(Class<T> iface) throws SQLException { if (iface.isInstance(this)) { return (T) this; } return determineTargetDataSource().unwrap(iface); } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface)); } }
具体选择哪个数据源是由determineCurrentLookupKey()方法的返回值决定的,该方法需要我们继承AbstractRoutingDataSource来重写。
通过上面源码展示,我们也可以看出AbstractRoutingDataSource切换数据源的源码不多,并且非常简单,相信建立在源码的基础上再去应用,会让你感觉到简直不要太easy。
使用AbstractRoutingDataSource动态切换数据源示例代码
这种功能属于技术模块,完全可以独立于业务模块之外开发出一个类似中间件的组件形式存在。下面结合我具体的使用案例,给大家贡献参考如下参考代码:
1、定义一个常量,表示所有的DataSource的key。(建议用这样的全局常量维护key,当然这不是必须的)
public abstract class DynamicDataSourceId { public static final String MASTER = "master"; public static final String SLAVE1 = "slave1"; public static final String SLAVE2 = "slave2"; //... 可以继续无线扩展 // 保存着有效的(调用者设置进来的)所有的DATA_SOURCE_IDS public static final List<String> DATA_SOURCE_IDS = new ArrayList(); public static boolean containsDataSourceId(final String dataSourceId) { return dataSourceId != null && !dataSourceId.trim().isEmpty() ? DATA_SOURCE_IDS.contains(dataSourceId) : false; } }
2、定义一个Holder,可以把数据源名称和当前线程绑定,提升易用性(当然也不是必须的)
public abstract class DynamicDataSourceContextHolder { //每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 注意:使用静态方法setDataSourceId设置当前线程需要使用的数据源id(和当前线程绑定) */ public static void setDataSourceId(final String dataSourceId) { CONTEXT_HOLDER.set(dataSourceId); } /** * 获取当前线程使用的数据源id */ public static String getDataSourceId() { return CONTEXT_HOLDER.get(); } /** * 清空当前线程使用的数据源id */ public static void clearDataSourceId() { CONTEXT_HOLDER.remove(); } }