技术栈方面,会采用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
)进群学习~