前面我們配置過單個數據源了,本節講解下如何實現多數據源的動態切換(c3p0和druid)。
修改下數據源的連接,使其不屬於同一個數據庫:
# c3p0.properties c3p0.jdbc.jdbcUrl=jdbc:mysql://localhost:3305/spring?useSSL=false&characterEncoding=UTF-8 # druiddb.properties druid.jdbc.url=jdbc:mysql://127.0.0.1:3305/db_link?characterEncoding=utf-8&useSSL=false
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
AbstractRoutingDataSource本身實現了javax.sql.DataSource接口(由其父類抽象類AbstractDataSource實現),因此其實際上也是一個標准數據源的實現類。該類是Spring專為多數據源管理而增加的一個接口層。它根據一個數據源唯一標識key來尋找已經配置好的數據源隊列,它通常是與當前線程綁定在一起的。
AbstractRoutingDataSource的內部維護了一個名為targetDataSources的Map,並提供的setter方法用於設置數據源關鍵字與數據源的關系,實現類被要求實現其determineCurrentLookupKey()方法,由此方法的返回值決定具體從哪個數據源中獲取連接。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * DynamicDataSource類繼承了Spring的抽象類AbstractRoutingDataSource, * 而AbstractRoutingDataSource本身實現了javax.sql.DataSource接口(由其父類抽象類AbstractDataSource實現), * 因此其實際上也是一個標准數據源的實現類。該類是Spring專為多數據源管理而增加的一個接口層。 * 它根據一個數據源唯一標識key來尋找已經配置好的數據源隊列,它通常是與當前線程綁定在一起的。 * * AbstractRoutingDataSource的內部維護了一個名為targetDataSources的Map, * 並提供的setter方法用於設置數據源關鍵字與數據源的關系,實現類被要求實現其determineCurrentLookupKey()方法, * 由此方法的返回值決定具體從哪個數據源中獲取連接。 * */ public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { String dataSource = DynamicDataSourceContextHolder.getDataSourceType(); System.out.println("當前數據源:" + dataSource); return dataSource; } }
設置數據源的工具類DynamicDataSourceContextHolder
public class DynamicDataSourceContextHolder { //存放當前線程使用的數據源類型信息 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); //存放數據源id public static List<String> dataSourceIds = new ArrayList(); //設置數據源 public static void setDataSourceType(String dataSourceType) { contextHolder.set(dataSourceType); } //獲取數據源 public static String getDataSourceType() { return contextHolder.get(); } //清除數據源 public static void clearDataSourceType() { contextHolder.remove(); System.out.println("清除數據源:" + contextHolder.get()); } //判斷當前數據源是否存在 public static boolean isContainsDataSource(String dataSourceId) { return dataSourceIds.contains(dataSourceId); } }
創建動態數據源和JdbcTemplate
在Spring根容器(SpringConfig)中使用@Bean創建動態數據源和JdbcTemplate。
/** * 這個是Spring容器,相當於ApplicationContext.xml,負責掃描相關的service和dao,排除controller的掃描 * 數據源、事務等均在這里配置 */ @ComponentScan(value = {"com.codedot"}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class}), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {RestController.class})}) @Import({C3p0DBConfig.class, DruidDBConfig.class}) @PropertySource(value = {"classpath:c3p0.properties", "classpath:druiddb.properties"}) //讀取resources下的配置文件 @Configuration public class SpringConfig { @Bean public JdbcTemplate jdbcTemplate(DynamicDataSource dynamicDataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dynamicDataSource); return jdbcTemplate; } // 前提:需要將DruidDataSource和ComboPooledDataSource的單數據源,可以查看單數據源的配置方式 @Bean public DynamicDataSource dynamicDataSource(DruidDataSource druidDataSource, ComboPooledDataSource c3p0DataSource) { DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setDefaultTargetDataSource(c3p0DataSource); Map<Object, Object> targetDataSourceMap = new HashMap(); targetDataSourceMap.put("defaultTargetDataSource", c3p0DataSource); targetDataSourceMap.put("druidTargetDataSource", druidDataSource); dynamicDataSource.setTargetDataSources(targetDataSourceMap); return dynamicDataSource; } }
使用AOP攔截JdbcTemplate方法
注意:需要在Spring根容器(SpringConfig)添加類注解@EnableAspectJAutoProxy(proxyTargetClass = true)來開啟aop。
/** * 這里使用JdbcTemplate操作數據庫,所以選擇JdbcTemplate的方法作為切入點 */ @Component @Order(-1)//這里一定要保證在@Transactional之前執行 @Aspect public class TemplateDbAspect { @Before("execution(* org.springframework.jdbc.core.JdbcTemplate.update*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.batchUpdate(..))") public void setMasterDb(){ System.out.println("master db"); DynamicDataSourceContextHolder.setDataSourceType("defaultTargetDataSource"); } @Before("execution(* org.springframework.jdbc.core.JdbcTemplate.query*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.execute(..))") public void setSlaveDb(){ System.out.println("slave db"); DynamicDataSourceContextHolder.setDataSourceType("druidTargetDataSource"); } }
單元測試
@RunWith(SpringRunner.class) @WebAppConfiguration @ContextHierarchy({ @ContextConfiguration(classes = SpringConfig.class), @ContextConfiguration(classes = SpringMVCConfig.class) }) public class DbTest { @Autowired private JdbcTemplate jdbcTemplate; @Test public void test() throws SQLException { //DynamicDataSourceContextHolder.setDataSourceType("defaultTargetDataSource"); System.out.println(jdbcTemplate.getDataSource()); jdbcTemplate.update("update sp_user set name = 'codedot' where id = 5"); //DynamicDataSourceContextHolder.setDataSourceType("druidTargetDataSource"); Map<String, Object> userMap = jdbcTemplate.queryForMap("select * from fm_user where id = 1"); System.out.println(jdbcTemplate.getDataSource()); System.out.println(userMap.get("name")); } }
有時我們需要在自定義的方法上動態切入數據源,可以定義一個注解,使用aop在方法進入前解析注解,獲取注解中指定的數據源key,並進行設置,來達到動態切換的效果。
如果你是使用JdbcTemplate操作數據庫,請注意:在存在不同數據源切換的方法中添加了事務注解(即使有加了分布式事務),也只能管理默認的數據源的事務,因為在事務開啟后,JdbcTemplate會緩存連接,獲取到的都是前面的連接。
public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource); if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) { logger.debug("Fetching JDBC Connection from DataSource"); Connection con = fetchConnection(dataSource); if (TransactionSynchronizationManager.isSynchronizationActive()) { logger.debug("Registering transaction synchronization for JDBC Connection"); ConnectionHolder holderToUse = conHolder; if (conHolder == null) { holderToUse = new ConnectionHolder(con); } else { conHolder.setConnection(con); } holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } return con; } else { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(fetchConnection(dataSource)); } return conHolder.getConnection(); } }