技術棧方面,會採用Spring Boot 2.0 做爲底層框架,主要爲了後續可以接入Spring Cloud 進行學習拓展。而且Spring Boot 2.0基於Spring5,也能夠提早預習一些Spring5的新特性。後續技術會在相應博客中提出。html
項目GitHub地址:Spring-Blog mysql
介紹一下目錄結構:git
爲了讓各位朋友可以更好理解這一模塊的內容,演示代碼將存放在Spring Boot 項目下: github
Github 地址:示例代碼 web
在開始講解前,咱們須要先構建後咱們的運行環境。Spring Boot 引入 Mybatis 的教程 能夠參考 傳送門 。這裏咱們不細述了,首先來看一下咱們的目錄結構:
面試
有使用過Spring Boot 的童鞋應該清楚,當咱們在application.properties 配置好了咱們的數據庫鏈接信息後,Spring Boot 將會幫咱們自動裝載好DataSource。但若是咱們須要進行讀寫分離操做是,如何配置本身的數據源,是咱們必須掌握的。spring
首先咱們來看一下配置文件中的信息:sql
spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog2 spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver #別名掃描目錄 mybatis.type-aliases-package=com.jaycekon.demo.model #Mapper.xml掃描目錄 mybatis.mapper-locations=classpath:mybatis-mappers/*.xml #tkmapper 幫助工具 mapper.mappers=com.jaycekon.demo.MyMapper mapper.not-empty=false mapper.identity=MYSQL 複製代碼
咱們首先來看一下使用 DataSourceBuilder 來構建出DataSource:數據庫
@Configuration @MapperScan("com.jaycekon.demo.mapper") @EnableTransactionManagement public class SpringJDBCDataSource { /** * 經過Spring JDBC 快速建立 DataSource * 參數格式 * spring.datasource.master.jdbcurl=jdbc:mysql://localhost:3306/charles_blog * spring.datasource.master.username=root * spring.datasource.master.password=root * spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver * * @return DataSource */ @Bean @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource dataSource() { return DataSourceBuilder.create().build(); } } 複製代碼
從代碼中咱們能夠看出,使用DataSourceBuilder 構建DataSource 的方法很是簡單,可是須要注意的是:api
DataSourceBuilder 只能自動識別配置文件中的 jdbcurl,username,password,driver-class-name等命名,所以咱們須要在方法體上加上 @ ConfigurationProperties 註解。
數據庫鏈接地址變量名須要使用 jdbcurl
數據庫鏈接池使用 com.zaxxer.hikari.HikariDataSource
執行單元測試時,咱們能夠看到 DataSource 建立以及關閉的過程。
除了使用上述的構建方法外,咱們能夠選擇使用阿里提供的 Druid 數據庫鏈接池建立 DataSource
@Configuration @EnableTransactionManagement public class DruidDataSourceConfig { @Autowired private DataSourceProperties properties; @Bean public DataSource dataSoucre() throws Exception { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(properties.getUrl()); dataSource.setDriverClassName(properties.getDriverClassName()); dataSource.setUsername(properties.getUsername()); dataSource.setPassword(properties.getPassword()); dataSource.setInitialSize(5); dataSource.setMinIdle(5); dataSource.setMaxActive(100); dataSource.setMaxWait(60000); dataSource.setTimeBetweenEvictionRunsMillis(60000); dataSource.setMinEvictableIdleTimeMillis(300000); dataSource.setValidationQuery("SELECT 'x'"); dataSource.setTestWhileIdle(true); dataSource.setTestOnBorrow(false); dataSource.setTestOnReturn(false); dataSource.setPoolPreparedStatements(true); dataSource.setMaxPoolPreparedStatementPerConnectionSize(20); dataSource.setFilters("stat,wall"); return dataSource; } } 複製代碼
使用 DruidDataSource 做爲數據庫鏈接池可能看起來會比較麻煩,可是換一個角度來講,這個更加可控。咱們能夠經過 DataSourceProperties 來獲取 application.properties 中的配置文件:
spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog2
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
複製代碼
須要注意的是,DataSourceProperties 讀取的配置文件 前綴是 spring.datasource ,咱們能夠進入到 DataSourceProperties 的源碼中觀察:
@ConfigurationProperties(prefix = "spring.datasource") public class DataSourceProperties implements BeanClassLoaderAware, EnvironmentAware, InitializingBean 複製代碼
能夠看到,在源碼中已經默認標註了前綴的格式。
除了使用 DataSourceProperties 來獲取配置文件 咱們還可使用通用的環境變量讀取類:
@Autowired private Environment env; env.getProperty("spring.datasource.write") 複製代碼
配置多數據源主要須要如下幾個步驟:
這裏直接使用枚舉類型區分,讀數據源和寫數據源
public enum DatabaseType { master("write"), slave("read"); DatabaseType(String name) { this.name = name; } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "DatabaseType{" + "name='" + name + '\'' + '}'; } } 複製代碼
該類主要用於記錄當前線程使用的數據源,使用 ThreadLocal 進行記錄數據
public class DatabaseContextHolder { private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>(); public static void setDatabaseType(DatabaseType type) { contextHolder.set(type); } public static DatabaseType getDatabaseType() { return contextHolder.get(); } } 複製代碼
該類繼承 AbstractRoutingDataSource 用於管理 咱們的數據源,主要實現了 determineCurrentLookupKey 方法。 後續細述這個類是如何進行多數據源管理的。
public class DynamicDataSource extends AbstractRoutingDataSource { @Nullable @Override protected Object determineCurrentLookupKey() { DatabaseType type = DatabaseContextHolder.getDatabaseType(); logger.info("====================dataSource ==========" + type); return type; } } 複製代碼
最後一步就是配置咱們的數據源,將數據源放置到 DynamicDataSource 中:
@Configuration @MapperScan("com.jaycekon.demo.mapper") @EnableTransactionManagement public class DataSourceConfig { @Autowired private DataSourceProperties properties; /** * 經過Spring JDBC 快速建立 DataSource * 參數格式 * spring.datasource.master.jdbcurl=jdbc:mysql://localhost:3306/charles_blog * spring.datasource.master.username=root * spring.datasource.master.password=root * spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver * * @return DataSource */ @Bean(name = "masterDataSource") @Qualifier("masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } /** * 手動建立DruidDataSource,經過DataSourceProperties 讀取配置 * 參數格式 * spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog * spring.datasource.username=root * spring.datasource.password=root * spring.datasource.driver-class-name=com.mysql.jdbc.Driver * * @return DataSource * @throws SQLException */ @Bean(name = "slaveDataSource") @Qualifier("slaveDataSource") public DataSource slaveDataSource() throws SQLException { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(properties.getUrl()); dataSource.setDriverClassName(properties.getDriverClassName()); dataSource.setUsername(properties.getUsername()); dataSource.setPassword(properties.getPassword()); dataSource.setInitialSize(5); dataSource.setMinIdle(5); dataSource.setMaxActive(100); dataSource.setMaxWait(60000); dataSource.setTimeBetweenEvictionRunsMillis(60000); dataSource.setMinEvictableIdleTimeMillis(300000); dataSource.setValidationQuery("SELECT 'x'"); dataSource.setTestWhileIdle(true); dataSource.setTestOnBorrow(false); dataSource.setTestOnReturn(false); dataSource.setPoolPreparedStatements(true); dataSource.setMaxPoolPreparedStatementPerConnectionSize(20); dataSource.setFilters("stat,wall"); return dataSource; } /** * 構造多數據源鏈接池 * Master 數據源鏈接池採用 HikariDataSource * Slave 數據源鏈接池採用 DruidDataSource * @param master * @param slave * @return */ @Bean @Primary public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master, @Qualifier("slaveDataSource") DataSource slave) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DatabaseType.master, master); targetDataSources.put(DatabaseType.slave, slave); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法 dataSource.setDefaultTargetDataSource(slave);// 默認的datasource設置爲myTestDbDataSourcereturn dataSource; } @Bean public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource myTestDbDataSource, @Qualifier("slaveDataSource") DataSource myTestDb2DataSource) throws Exception { SqlSessionFactoryBean fb = new SqlSessionFactoryBean(); fb.setDataSource(this.dataSource(myTestDbDataSource, myTestDb2DataSource)); fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package")); fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations"))); return fb.getObject(); } } 複製代碼
上述代碼塊比較長,咱們來解析一下:
接下來咱們來簡單觀察一下 DataSource 的建立過程:
首先咱們能夠看到咱們的兩個數據源以及構建好了,分別使用的是HikariDataSource 和 DruidDataSource,而後咱們會將兩個數據源放入到 targetDataSource 中,而且這裏講咱們的 slave 做爲默認數據源 defaultTargetDataSource
而後到獲取數據源這一塊:
主要是從 AbstractRoutingDataSource 這個類中的 determineTargetDataSource( ) 方法中進行判斷,這裏會調用到咱們在 DynamicDataSource 中的方法, 去判斷須要使用哪個數據源。若是沒有設置數據源,將採用默認數據源,就是咱們剛纔設置的DruidDataSource 數據源。
在最後的代碼運行結果中:
咱們能夠看到確實是使用了咱們設置的默認數據源。
在經歷了千山萬水後,終於來到咱們的讀寫分離模塊了,首先咱們須要添加一些咱們的配置信息:
spring.datasource.read = get,select,count,list,query
spring.datasource.write = add,create,update,delete,remove,insert
複製代碼
這兩個變量主要用於切面判斷中,區分哪一些部分是須要使用 讀數據源,哪些是須要使用寫的。
public class DynamicDataSource extends AbstractRoutingDataSource { static final Map<DatabaseType, List<String>> METHOD_TYPE_MAP = new HashMap<>(); @Nullable @Override protected Object determineCurrentLookupKey() { DatabaseType type = DatabaseContextHolder.getDatabaseType(); logger.info("====================dataSource ==========" + type); return type; } void setMethodType(DatabaseType type, String content) { List<String> list = Arrays.asList(content.split(",")); METHOD_TYPE_MAP.put(type, list); } } 複製代碼
在這裏咱們須要添加一個Map 進行記錄一些讀寫的前綴信息。
在DataSourceConfig 中,咱們再設置DynamicDataSource 的時候,將前綴信息設置進去。
@Bean @Primary public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master, @Qualifier("slaveDataSource") DataSource slave) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DatabaseType.master, master); targetDataSources.put(DatabaseType.slave, slave); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法 dataSource.setDefaultTargetDataSource(slave);// 默認的datasource設置爲myTestDbDataSource String read = env.getProperty("spring.datasource.read"); dataSource.setMethodType(DatabaseType.slave, read); String write = env.getProperty("spring.datasource.write"); dataSource.setMethodType(DatabaseType.master, write); return dataSource; } 複製代碼
在配置好讀寫的方法前綴後,咱們須要配置一個切面,監聽在進入Mapper 方法前將數據源設置好:
主要的操做點在於 DatabaseContextHolder.setDatabaseType(type); 結合咱們上面多數據源的獲取數據源方法,這裏就是咱們設置讀或寫數據源的關鍵了。
@Aspect @Component @EnableAspectJAutoProxy(proxyTargetClass = true) public class DataSourceAspect { private static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class); @Pointcut("execution(* com.jaycekon.demo.mapper.*.*(..))") public void aspect() { } @Before("aspect()") public void before(JoinPoint point) { String className = point.getTarget().getClass().getName(); String method = point.getSignature().getName(); String args = StringUtils.join(point.getArgs(), ","); logger.info("className:{}, method:{}, args:{} ", className, method, args); try { for (DatabaseType type : DatabaseType.values()) { List<String> values = DynamicDataSource.METHOD_TYPE_MAP.get(type); for (String key : values) { if (method.startsWith(key)) { logger.info(">>{} 方法使用的數據源爲:{}<<", method, key); DatabaseContextHolder.setDatabaseType(type); DatabaseType types = DatabaseContextHolder.getDatabaseType(); logger.info(">>{}方法使用的數據源爲:{}<<", method, types); } } } } catch (Exception e) { logger.error(e.getMessage(), e); } } } 複製代碼
方法啓動後,先進入切面中,根據methodName 設置數據源類型。
而後進入到determineTargetDataSource 方法中 獲取到數據源:
運行結果:
但願看完後以爲有幫助的朋友,幫博主到github 上面點個Start 或者 fork
Spring-Blog 項目GitHub地址:Spring-Blog
示例代碼 Github 地址:示例代碼
最後貼一個新生的公衆號 (Java 補習課
),歡迎各位關注,主要會分享一下面試的內容(參考以前博主的文章),阿里的開源技術之類和阿里生活相關。 想要交流面試經驗的,能夠添加個人我的微信(Jayce-K
)進羣學習~