在Spring-Mybatis中,有這樣一個類AbstractRoutingDataSource,根據名字能夠猜到,這是一個框架提供的用於動態選擇數據源的類。這個類有兩個重要的參數,分別叫html
defaultTargetDataSource和targetDataSources。通常的工程都是一個數據源,因此不太接觸到這個類。java
[html]程序員
- <bean id="myoneDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
- <property name="driverClassName" value="${jdbc.myone.driver}"/>
- <property name="url" value="${jdbc.myone.url}"/>
- <property name="username" value="${jdbc.myone.username}"/>
- <property name="password" value="${jdbc.myone.password}"/>
- </bean>
- <bean id="mytwoDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
- <property name="driverClassName" value="${jdbc.mytwo.driver}"/>
- <property name="url" value="${jdbc.mytwo.url}"/>
- <property name="username" value="${jdbc.mytwo.username}"/>
- <property name="password" value="${jdbc.mytwo.password}"/>
- </bean>
- 下載
- <bean id="multipleDataSource" class="dal.datasourceswitch.MultipleDataSource">
- <property name="defaultTargetDataSource" ref="myoneDataSource"/> <!--默認主庫-->
- <property name="targetDataSources">
- <map>
- <entry key="myone" value-ref="myoneDataSource"/> <!--輔助aop完成自動數據庫切換-->
- <entry key="mytwo" value-ref="mytwoDataSource"/>
- </map>
- </property>
- </bean>
上面的配置文件對這兩個參數的描述已經很清楚了,但這是多個數據源已經肯定的場景。咱們這篇博客中的場景是多個數據源的信息存在於數據庫中,可能數據庫中的數據源信息會動態的增長或者減小。這樣的話,就不能像上面這樣配置了。那怎麼辦呢?spring
咱們僅僅須要設定默認的數據源,即defaultDataSource參數,至於targetDataSources參數咱們須要在代碼中動態的設定。來看下具體的xml配置:數據庫
[html] apache
- <bean id="defaultDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
- p:driverClassName="${db_driver}"
- p:url="${db_url}"
- p:username="${db_user}"
- p:password="${db_pass}"
- p:validationQuery="select 1"
- p:testOnBorrow="true"/>
- <!--動態數據源相關-->
- <bean id="dynamicDataSource" class="org.xyz.test.service.datasourceswitch.impl.DynamicDataSource">
- <property name="targetDataSources">
- <map key-type="java.lang.String">
- <entry key="defaultDataSource" value-ref="defaultDataSource"/>
- </map>
- </property>
- <property name="defaultTargetDataSource" ref="defaultDataSource"/>
- </bean>
從上面的配置文件中能夠看到,咱們僅僅配置了默認的數據源defaultDataSource下載。至於其餘的數據源targetDataSources,咱們沒有配置,須要在代碼中動態的建立。關於配置就講清楚啦!但咱們注意到,支持動態數據源的不該該是AbstractRoutingDataSource類嗎?怎麼上面的配置中是DynamicDataSource類。沒錯,這個是咱們自定義的繼承自AbstractRoutingDataSource類的類,也只最重要的類,來看下:(理解這個類,你須要熟練掌握JAVA反射,以及ThreadLocal變量,和Spring的注入機制。別退縮,你們都是這樣一步步學過來的!)(下面僅僅是看下全貌,代碼的下面會有詳細的說明)app
[java] 框架
- final class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware{
- private static final String DATA_SOURCES_NAME = "targetDataSources";
- private ApplicationContext applicationContext;
- @Override
- protected Object determineCurrentLookupKey() {
- DataSourceBeanBuilder dataSourceBeanBuilder = DataSourceHolder.getDataSource();
- System.out.println("----determineCurrentLookupKey---"+dataSourceBeanBuilder);
- if (dataSourceBeanBuilder == null) {
- return null;
- }
- DataSourceBean dataSourceBean = new DataSourceBean(dataSourceBeanBuilder);
- //查看當前容器中是否存在
- try {
- if (!getTargetDataSources().keySet().contains(dataSourceBean.getBeanName())) {
- addNewDataSourceToTargerDataSources(dataSourceBean);
- }
- return dataSourceBean.getBeanName();
- } catch (NoSuchFieldException | IllegalAccessException e) {
- throw new SystemException(ErrorEnum.MULTI_DATASOURCE_SWITCH_EXCEPTION);
- }
- }
- private void addNewDataSourceToTargerDataSources(DataSourceBean dataSourceBean) throws NoSuchFieldException, IllegalAccessException {
- getTargetDataSources().put(dataSourceBean.getBeanName(), createDataSource(dataSourceBean));
- super.afterPropertiesSet();//通知spring有bean更新
- }
- private Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {
- //在spring容器中建立而且聲明bean
- ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
- DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
- BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);
- //將dataSourceBean中的屬性值賦給目標bean
- Map<String, Object> properties = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);
- for (Map.Entry<String, Object> entry : properties.entrySet()) {
- beanDefinitionBuilder.addPropertyValue((String) entry.getKey(), entry.getValue()); 下載
- }
- beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());
- return applicationContext.getBean(dataSourceBean.getBeanName());
- }
- private Map<Object, Object> getTargetDataSources() throws NoSuchFieldException, IllegalAccessException {
- Field field = AbstractRoutingDataSource.class.getDeclaredField(DATA_SOURCES_NAME);
- field.setAccessible(true);
- return (Map<Object, Object>) field.get(this);
- }
- private <T> Map<String, Object> getPropertyKeyValues(Class<T> clazz, Object object) throws IllegalAccessException {
- Field[] fields = clazz.getDeclaredFields();
- Map<String, Object> result = new HashMap<>();
- for (Field field : fields) {
- field.setAccessible(true);
- result.put(field.getName(), field.get(object));
- }
- result.remove("beanName");
- return result;
- }
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- this.applicationContext=applicationContext;
- }
- }
首先來看覆蓋方法determineCurrentLookupKey(),框架在每次調用數據源時會先調用這個方法,以便知道使用哪一個數據源。在本文的場景中,數據源是由程序員在即將切換數據源以前,將要使用的那個數據源的名稱放到當前線程的ThreadLocal中,這樣在determineCurrentLookupKey()方法中就能夠從ThreadLocal中拿到當前請求鑰匙用的數據源,從而進行初始化數據源並返回該數據源的操做。在ThreadLocal變量中,咱們保存了一個DataSourceBuilder,這是一個建造者模式。讀者直接把他理解爲是一個數據源的描述就好。所以,determineCurrentLookupKey()方法的流程就是下載:先從ThreadLocal中拿出要使用的數據源信息,而後看當前的targetDataSources中是否有了這個數據源。若是有直接返回。若是沒有,建立一個這樣的數據源,放到targetDataSources中而後返回。ide
因爲targetDataSources是父類AbstractRoutingDataSource中的一個私有域,所以想要得到他的實例只能經過反射機制。這也是下面的方法存在的意義!ui
[java]
- private Map<Object, Object> getTargetDataSources() throws NoSuchFieldException, IllegalAccessException {
- Field field = AbstractRoutingDataSource.class.getDeclaredField(DATA_SOURCES_NAME);
- field.setAccessible(true);
- return (Map<Object, Object>) field.get(this);
- }
而後,咱們來看具體是怎麼建立數據源的。
[java]
- private Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {
- //在spring容器中建立而且聲明bean
- ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
- DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
- BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);
- //將dataSourceBean中的屬性值賦給目標bean
- Map<String, Object> properties = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);
- for (Map.Entry<String, Object> entry : properties.entrySet()) {
- beanDefinitionBuilder.addPropertyValue((String) entry.getKey(), entry.getValue());
- }
- beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());
- return applicationContext.getBean(dataSourceBean.getBeanName());
- }
你們知道,Spring最主要的功能是做爲bean容器,即他負責bean生命週期的管理。所以,咱們自定義的datasource也不能「逍遙法外」,必須交給Spring容器來管理。這也正是DynamicDataSource類須要實現ApplicationContextAware而且注入ApplicationContext的緣由。上面的代碼就是根據指定的信息建立一個數據源。這種建立是Spring容器級別的建立。建立完畢以後,須要把剛剛建立的這個數據源放到targetDataSources中,而且還要通知Spring容器,targetDataSources對象變了。下面的方法就是在作這樣的事情下載:
[java]
- private void addNewDataSourceToTargerDataSources(DataSourceBean dataSourceBean) throws NoSuchFieldException, IllegalAccessException {
- getTargetDataSources().put(dataSourceBean.getBeanName(), createDataSource(dataSourceBean));
- super.afterPropertiesSet();//通知spring有bean更新
- }
上面的這一步很重要。沒有這一步的話,Spring壓根就不會知道targetDataSources中多了一個數據源。至此DynamicDataSource類就講完了。其實仔細想一想,思路仍是很清晰的。啃掉了DynamicDataSource類這塊硬骨頭,下面就是一些輔助類了。好比說DataSourceHolder,業務代碼經過使用這個類來通知DynamicDataSource中的determineCurrentLookupKey()方法到底使用那個數據源:
[java]
- public final class DataSourceHolder {
- private static ThreadLocal<DataSourceBeanBuilder> threadLocal=new ThreadLocal<DataSourceBeanBuilder>(){
- @Override
- protected DataSourceBeanBuilder initialValue() {
- return null;
- }
- };
- static DataSourceBeanBuilder getDataSource(){
- return threadLocal.get();
- }
- public static void setDataSource(DataSourceBeanBuilder dataSourceBeanBuilder){
- threadLocal.set(dataSourceBeanBuilder);
- }
- public static void clearDataSource(){
- threadLocal.remove();
- }
- }