一文弄懂spring官方多数据源

2023-06-06 17:07:42 浏览数 (1)

路由键#determineCurrentLookupKey

先看一下AbstractRoutingDataSource的类图

我们可以看到,它间接实现了DataSource。是个抽象类,只有一个抽象方法#determineCurrentLookupKey()

代码语言:javascript复制
java复制代码public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

	/**
	 * Determine the current lookup key. This will typically be
	 * implemented to check a thread-bound transaction context.
	 * <p>Allows for arbitrary keys. The returned key needs
	 * to match the stored lookup key type, as resolved by the
	 * {@link #resolveSpecifiedLookupKey} method.
	 */
	@Nullable
	protected abstract Object determineCurrentLookupKey();

	......
}

通过注释与方法名我们可以知道,这个方法是来确定数据源的路由key的,那他究竟有什么用呢

核心方法#determineTargetDataSource

看代码可知,抽象方法#determineCurrentLookupKey()只有一个地方用到,即#determineTargetDataSource()

代码语言:javascript复制
java复制代码public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

	/**
	 * Retrieve the current target DataSource. Determines the
	 * {@link #determineCurrentLookupKey() current lookup key}, performs
	 * a lookup in the {@link #setTargetDataSources targetDataSources} map,
	 * falls back to the specified
	 * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
	 * @see #determineCurrentLookupKey()
	 */
	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
        //通过当前lookupKey获取数据源
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        //如果数据源为空,开启了宽松模式或者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   "]");
		}
		return dataSource;
	}

	......
}

而#determineTargetDataSource()为什么是核心方法,因为AbstractRoutingDataSource其自身就是一个DataSource,获取jdbc连接时会通过该方法获取数据源

代码语言:javascript复制
java复制代码public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		return determineTargetDataSource().getConnection(username, password);
	}

	......
}

到这里,AbstractRoutingDataSource怎么工作的我们大概已经清楚了。但他是怎么初始化的呢?determineTargetDataSource方法中的resolvedDataSources是怎么来的呢?

初始化

代码语言:javascript复制
java复制代码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;

    getter/setter忽略......

    @Override
	public void afterPropertiesSet() {
    	//targetDataSources是必须的,不然bean初始化时候就会抛错
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
        //已解析数据源map初始化
		this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
		this.targetDataSources.forEach((key, value) -> {
        	//这一步resolveSpecifiedLookupKey其实就是返回key自身
			Object lookupKey = resolveSpecifiedLookupKey(key);
            //如果value是DataSource类型直接返回,是String类型则会认为是jndi名称,通过JndiDataSourceLookup类查找
			DataSource dataSource = resolveSpecifiedDataSource(value);
            //放入已解析数据源
			this.resolvedDataSources.put(lookupKey, dataSource);
		});
        //设置默认数据源
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}

	......
}

resolveSpecifiedDataSource部分没有详细介绍,感兴趣的小伙伴们可以自行追踪源码与DataSourceLookup类的源码去查看

简单使用

上面源码看完相信大家都对AbstractRoutingDataSource很了解了,使用方面总结就三步

  • 实现AbstractRoutingDataSource
  • 重写#determineCurrentLookupKey()
  • 设置targetDataSources与defaultTargetDataSource

新建实现类

代码语言:javascript复制
java复制代码public class RoutingDataSource extends AbstractRoutingDataSource {

    /**
     * 获取路由key,通过key可获取已设置数据源中对应的数据源
     * <p>如果lookupKey为空则获取默认数据源
     * <p>详见{@link AbstractRoutingDataSource#determineTargetDataSource}
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getRoutingKey();
    }
}

新建上下文切换类

代码语言:javascript复制
typescript复制代码public class RoutingDataSourceContext {

    private static final ThreadLocal<String> LOOKUP_KEY_HOLDER = new ThreadLocal<>();

    public static void setRoutingKey(String routingKey) {
        LOOKUP_KEY_HOLDER.set(routingKey);
    }

    public static String getRoutingKey() {
        String name = LOOKUP_KEY_HOLDER.get();
        // 如果routingKey不存在则返回默认数据源
        return StringUtils.hasText(name) ? name : null;
    }

    public static void reset() {
        LOOKUP_KEY_HOLDER.remove();
    }
}

注册bean

代码语言:javascript复制
java复制代码@Configuration
public class DataSourceConfiguration {

    /**
     * routingDataSource也是dataSource
     * <p>这里指定该bean为主dataSource,防止多个datasource时导致mybatis的自动装配失效
     */
    @Bean
    public RoutingDataSource routingDataSource() {
        //数据源路由器
        RoutingDataSource routingDataSource = new RoutingDataSource();
        DataSource first = DataSourceBuilder.create()
                .url("")
                .username("")
                .password("")
                .driverClassName("")
                .build();
        DataSource second = DataSourceBuilder.create()
                .url("")
                .username("")
                .password("")
                .driverClassName("")
                .build();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("first", first);
        targetDataSources.put("second", second);
        //设置默认数据源
        routingDataSource.setDefaultTargetDataSource(first);
        //设置总共支持哪些数据源切换
        routingDataSource.setTargetDataSources(targetDataSources);
        return routingDataSource;
    }

}

使用

代码语言:javascript复制
java复制代码@Service
public class CarService {

    private final CarMapper carMapper;

    public CarService(CarMapper carMapper) {
        this.carMapper = carMapper;
    }

    public Car firstGet() {
        RoutingDataSourceContext.setRoutingKey("first");
        return carMapper.getOne(1L);
    }
}

封装成组件

很容易想到,如果能封装成组件,通过配置来动态添加数据源,并新建自定义注解通过AOP来读取就更方便了。还好我已经替你们实现啦Scindapsus-DS,而且还通过本地事务解决了AOP与Spring声明式事务冲突只能单数据源事务的问题,感兴趣的小伙伴们可以自行查看源码

0 人点赞