最近有個需求:須要先啓動一個web服務,而後這個服務去部署mysql,redis等相關服務,而且該服務要動態的把mysql在不重啓項目的狀況加載進去。javascript
項目先使用內存數據庫或者內存進行數據暫存(這裏我選擇了內存數據庫hsql),啓動先加載hsqldb數據源,而後根據mysql部署狀況動態的添加到相應的數據源java
springboot-2.11+mysql+hsqlmysql
1.pom文件(由於是直接貼出來的,有些不須要的請自行忽略)git
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> <dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis-spring-boot-starter.version}</version> <exclusions> <exclusion> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jdbc</artifactId> </exclusion> </exclusions> </dependency> <!-- spring data jpa --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- Swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${io.springfox.version}</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${io.springfox.version}</version> </dependency> <!-- MySQL Connector--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connetcor.version}</version> <!--<scope>runtime</scope>--> </dependency> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> <scope>compile</scope> <optional>true</optional> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <dependency> <groupId>commons-httpclient</groupId> <artifactId>commons-httpclient</artifactId> <version>${commons-httpclient.version}</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>${commons-lang.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> </dependency> <!-- 內存數據庫hsqldb --> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.70</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.2.1</version> </dependency> </dependencies>
首先須要定義一個數據源切換註解,調用時代表使用哪個數據源github
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { DataSourceTypeEnum value() default DataSourceTypeEnum.MYSQL; }
既然申明瞭註解,那麼咱們就須要去攔截並切換到對應的數據源,這時候就申明一個aop進行攔截處理web
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; @Aspect @Component public class DataSourceAop { private static final Logger logger = LoggerFactory.getLogger(DataSourceAop.class); @Autowired private HttpServletRequest request; /** * 攔截類做用域的註解 * @param dataSource */ @Before("@within(dataSource) ") public void beforeDataSource(DataSource dataSource) { String value = dataSource.value().getDb(); DataSourceContextHolder.setDataSource(value); try { logger.info("當前請求:{},當前使用的數據源爲:{}",request.getRequestURI(), value); }catch (Exception e){ } } /** * 攔截method做用域的註解 * @param dataSource */ @Before("@annotation(dataSource) ") public void beforeDataSourceMethod(JoinPoint joinPoint,DataSource dataSource) { String value = dataSource.value().getDb(); DataSourceContextHolder.setDataSource(value); try { logger.info("當前請求:{},當前使用的數據源爲:{}",request.getRequestURI(), value); }catch (Exception e){ } } /** * 按需在結束以後處理一些業務 * @param dataSource */ @After("@within(dataSource) ") public void afterDataSource(DataSource dataSource) { // todo something with yourself; } @After("@annotation(dataSource) ") public void afterDataSourceMethod(DataSource dataSource) { // todo something with yourself; } }
細心的朋友應該已經發現這裏用到了DataSourceContextHolder,而且設置了對應的數據源,那這個是作什麼的呢?咱們繼續往下看redis
import java.util.Map; public class DataSourceContextHolder { // 存放當前線程使用的數據源類型 private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); //把當前事物下的鏈接塞入,用於事物處理 private static ThreadLocal<Map<String, ConnectWarp>> connectionThreadLocal = new ThreadLocal<>(); // 設置數據源 public static void setDataSource(String type){ contextHolder.set(type); } // 獲取數據源 public static String getDataSource(){ return contextHolder.get(); } // 清除數據源 public static void clearDataSource(){ contextHolder.remove(); } // 設置鏈接 public static void setConnection(Map<String, ConnectWarp> connections){ connectionThreadLocal.set(connections); } // 獲取鏈接 public static Map<String, ConnectWarp> getConnection(){ return connectionThreadLocal.get(); } // 清除鏈接 public static void clearConnection(){ connectionThreadLocal.remove(); }
經過上面的代碼咱們發現它好像沒作什麼業務處理,只是單純的申明瞭線程變量,以及基本的賦值和查詢,那麼數據源真正的切換是如何實現的呢?別急,咱們立刻接近真相了spring
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import java.sql.Connection; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; public class DynamicDataSource extends AbstractRoutingDataSource { private static Map<Object, Object> dataSourceMap = new HashMap<Object, Object>(); private static DynamicDataSource instance; private static byte[] lock = new byte[0]; public static DynamicDataSource getInstance() { if (instance == null) { synchronized (lock) { if (instance == null) { instance = new DynamicDataSource(); } } } return instance; } /** * 重寫setTargetDataSources,經過入參targetDataSources進行數據源的添加 * @param targetDataSources */ @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { super.setTargetDataSources(targetDataSources); dataSourceMap.putAll(targetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { String dataSource = DataSourceContextHolder.getDataSource(); return dataSource; } /** * 獲取到原有的多數據源,並從該數據源基礎上添加一個或多個數據源後,經過上面的setTargetDataSources進行加載 * * @return */ public Map<Object, Object> getDataSourceMap() { return dataSourceMap; } /** * 開啓事物的時候,把鏈接放入 線程中,後續crud 都會拿對應的鏈接操做 * * @param key * @param connection */ public void bindConnection(String key, Connection connection) { Map<String, ConnectWarp> connectionMap = DataSourceContextHolder.getConnection(); if (connectionMap == null) { connectionMap = new HashMap<>(); } ConnectWarp connectWarp = new ConnectWarp(connection); connectionMap.put(key, connectWarp); DataSourceContextHolder.setConnection(connectionMap); } /** * 提交事物 * * @throws SQLException */ protected void doCommit() throws SQLException { Map<String, ConnectWarp> stringConnectionMap = DataSourceContextHolder.getConnection(); if (stringConnectionMap == null) { return; } for (String dataSourceName : stringConnectionMap.keySet()) { ConnectWarp connection = stringConnectionMap.get(dataSourceName); connection.commit(true); connection.close(true); } DataSourceContextHolder.clearConnection(); } /** * 撤銷事物 * * @throws SQLException */ protected void rollback() throws SQLException { Map<String, ConnectWarp> stringConnectionMap = DataSourceContextHolder.getConnection(); if (stringConnectionMap == null) { return; } for (String dataSourceName : stringConnectionMap.keySet()) { ConnectWarp connection = stringConnectionMap.get(dataSourceName); connection.rollback(); connection.close(true); } DataSourceContextHolder.clearConnection(); } /** * 若是 在connectionThreadLocal 中有 說明開啓了事物,就從這裏面拿 * * @return * @throws SQLException */ @Override public Connection getConnection() throws SQLException { Map<String, ConnectWarp> stringConnectionMap = DataSourceContextHolder.getConnection(); if (stringConnectionMap == null) { //沒開事物 直接走 return determineTargetDataSource().getConnection(); } else { //開了事物,從當前線程中拿,並且拿到的是 包裝過的connect 只有我能關閉O__O "… String currentName = (String) determineCurrentLookupKey(); return stringConnectionMap.get(currentName); } } }
這個類裏面最核心的就是setTargetDataSources和determineCurrentLookupKey兩個方法。首先setTargetDataSources重寫,保證咱們可以動態的添加數據源;而後determineCurrentLookupKey重寫,保證咱們可以從咱們動態添加的數據源裏面取出相應的數據源。
到這裏呢就差很少快成功了,只差最後一步了,咱們來看看這關鍵的最後一步:添加到spring管理sql
import com.github.pagehelper.PageInterceptor; import com.zaxxer.hikari.HikariDataSource; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; import java.util.Properties; @Configuration public class DataSourceConfig { @Autowired private HsqlConfig hsqlConfig; @Bean("hsqlDataSource") @Primary public DataSource hsqlDataSource() { HikariDataSource dataSource = new HikariDataSource(); BeanUtils.copyProperties(hsqlConfig, dataSource); dataSource.setJdbcUrl(hsqlConfig.getJdbcUrl()); return dataSource; } @Bean public DynamicDataSource dataSource(@Qualifier("hsqlDataSource") DataSource hsqlDataSource) { Map<Object, Object> map = new HashMap<>(); map.put(DataSourceTypeEnum.HSQL.getDb(), hsqlDataSource); DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance(); dynamicDataSource.setTargetDataSources(map); return dynamicDataSource; } @Bean public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dynamicDataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); //分頁插件 Interceptor interceptor = new PageInterceptor(); Properties properties = new Properties(); properties.setProperty("helperDialect", "mysql"); properties.setProperty("offsetAsPageNum", "true"); properties.setProperty("rowBoundsWithCount", "true"); properties.setProperty("reasonable", "true"); properties.setProperty("supportMethodsArguments", "true"); interceptor.setProperties(properties); Interceptor[] plugins = {interceptor}; factoryBean.setPlugins(plugins); return factoryBean.getObject(); } @Bean @Qualifier("transactionManager") public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); } }
到這裏這個多數據動態添加已經切換就已經OK了,而後有些很細心很細心的朋友發現了,我裏面對connect也作了寫處理,這個是重寫了事務機制,這個咱們下期繼續分析。數據庫