由于项目里原来的数据分了几个库,有一部分数据来源不止一个库,需要配置多数据源
第一步:
在application-dev.properties中配置数据源信息
# 开发环境 # #第一个数据源 spring.datasource.db_ku.driverClassName=com.mysql.jdbc.Driver spring.datasource.db_ku.url=jdbc:mysql://ip:3306/ku?useUnicode=true&characterEncoding=utf-8&useSSL=false&tinyInt1isBit=false&allowMultiQueries=true spring.datasource.db_ku.username=root spring.datasource.db_ku.password=root #第二个数据源 spring.datasource.db_ku1.driverClassName = com.mysql.jdbc.Driver spring.datasource.db_ku1.url = jdbc:mysql://ip:3306/ku1?useUnicode=true&characterEncoding=utf-8&useSSL=false&tinyInt1isBit=false spring.datasource.db_ku1.username = root spring.datasource.db_ku1.password = root
第二步:
创建动态数据源上下文类DynamicDataSourceContextHolder
/** * 动态数据源切换上下文 * Ami */ public class DynamicDataSourceContextHolder { // 默认的数据源名 public static final String DEFAULT_DS = "ds_ku"; private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static List<String> dataSourceIds = new ArrayList<String>(); /** * 设置数据源名,绑定到线程 * * @param dbName */ public static void setDB(String dbName) { if (StringUtils.isBlank(dbName)) { dbName = DEFAULT_DS; } contextHolder.set(dbName); } /** * 获取数据源名 * * @return */ public static String getDB() { String dbName = contextHolder.get(); if (StringUtils.isBlank(dbName)) { dbName = DEFAULT_DS; } return dbName; } /** * 清除数据源名 */ public static void clearDB() { contextHolder.remove(); } /** * 判断数据源是否存在 * * @param dataSourceId * @return */ public static boolean containsDB(String dataSourceId) { return dataSourceIds.contains(dataSourceId); } }
此类的作用主要是配合下面的一个注解,在线程中绑定当前使用的数据源名称,在数据源路由中介AbstractRoutingDataSource中切换数据源来实现从不同的数据源中查询数据。
第三步:
自定义注解TargetDataSource
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) /** *@description 切换数据源注解,Aspect指定注解用在方法级别上 *@author Ami *@date 2018/4/14 11:22 */ public @interface TargetDataSource { String value(); }
这个注解配合第二步的上下文,在方法上使用,具体使用往下面看
第四步:
使用AOP来切换数据源和清除绑定在线程上的数据源信息
@Aspect @Order(-1) @Component /** * 动态数据源切面类 * Ami */ public class DynamicDataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class); /** * 前置通知 * @param point * @param ds */ @Before("@annotation(ds)") public void before(JoinPoint point,TargetDataSource ds) { String dbName = ds.value(); if (StringUtils.isNotBlank(dbName) && !DynamicDataSourceContextHolder.containsDB(dbName)) { logger.error("不存在的数据源:" + dbName); throw new NonExistDataSourceException("不存在的数据源:" + dbName); // 自定义异常 } DynamicDataSourceContextHolder.setDB(dbName); } /** * 后置最终通知 * @param point * @param ds */ @After("@annotation(ds)") public void after(JoinPoint point,TargetDataSource ds) { DynamicDataSourceContextHolder.clearDB(); } }
AOP只会对加有TargetDataSource注解的方法进行拦截来切换数据源,其他的不加TargetDataSource注解的方法默认都是使用默认数据源。
第五步:
继承数据源路由中介AbstractRoutingDataSource,重写determineTargetDataSource方法
/** * 多数据源的路由中介 * Ami */ public class DynamicDataSource extends AbstractRoutingDataSource { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class); @Override protected Object determineCurrentLookupKey() { logger.debug("切换数据源为{},默认主数据源为{}", DynamicDataSourceContextHolder.getDB(), DynamicDataSourceContextHolder.DEFAULT_DS); return DynamicDataSourceContextHolder.getDB(); } }
在AbstractRoutingDataSource中源码:
/** * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs * a lookup in the {@link #setTargetDataSources targetDataSources} map, * falls back to the specified * {@link #setDefaultTargetDataSource default target DataSource} if necessary. * @see #determineCurrentLookupKey() */ protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); // 注意这个方法就是下面那个,抽象的,需要我们自己实现 DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; } /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction context. * <p>Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ protected abstract Object determineCurrentLookupKey(); // 自己实现
第六步:
配置数据源
/** * 多数据源的配置类 * Ami */ @Configuration public class DataSourceConfig { @Value("${mybatis.mapper-locations}") private String mapperLocations; @Value("${mybatis.type-aliases-package}") private String typeAliasesPackage; @Value("${mybatis.configuration.lazy-loading-enabled}") private Boolean lazyLoadingEnabled; @Value("${mybatis.configuration.aggressive-lazy-loading}") private Boolean aggressiveLazyLoading; @Value("${mybatis.page-helper.properties.reasonable}") private String reasonable; @Value("${mybatis.page-helper.properties.offsetAsPageNum}") private String offsetAsPageNum; @Value("${mybatis.page-helper.properties.rowBoundsWithCount}") private String rowBoundsWithCount; @Value("${mybatis.page-helper.properties.dialect}") private String dialect; /** * 主数据源 * @return */ @Bean(name = "ds_ku") @ConfigurationProperties(prefix = "spring.datasource.db_ku") public DataSource getDs_ku(){ DynamicDataSourceContextHolder.dataSourceIds.add("ds_ku"); return DataSourceBuilder.create().build(); } /** * 配置第二个数据源 * @return */ @Bean(name = "ds_ku1") @ConfigurationProperties(prefix = "spring.datasource.db_ku1") public DataSource getDs_ku1(){ DynamicDataSourceContextHolder.dataSourceIds.add("ds_ku1"); return DataSourceBuilder.create().build(); } @Bean(name = "dynamicDataSource") public DataSource dataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 默认数据源 dynamicDataSource.setDefaultTargetDataSource(getDs_ku()); // 配置多数据源 Map<Object, Object> map = new HashMap(5); map.put("ds_ku", getDs_ku()); map.put("ds_ku1", getDs_ku1()); dynamicDataSource.setTargetDataSources(map); return dynamicDataSource; } @Bean(name = "sqlSessionFactory") public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); bean.setMapperLocations(resolver.getResources(mapperLocations)); bean.setTypeAliasesPackage(typeAliasesPackage); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setAggressiveLazyLoading(aggressiveLazyLoading); configuration.setLazyLoadingEnabled(lazyLoadingEnabled); bean.setConfiguration(configuration); // 配置分页插件 PageHelper pageHelper = new PageHelper(); Properties properties = new Properties(); properties.setProperty("reasonable",reasonable); properties.setProperty("offsetAsPageNum", offsetAsPageNum); properties.setProperty("rowBoundsWithCount", rowBoundsWithCount); properties.setProperty("dialect", dialect); pageHelper.setProperties(properties); bean.setPlugins(new Interceptor[]{pageHelper}); return bean.getObject(); } // 事务管理 @Bean(name = "transactionManager") public PlatformTransactionManager getDataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
这样在项目初始化时,将读取的数据源以及名称放入到路由中介中targetDataSources这个Map中了
上面的sqlSessionFactory配置的多了点,可以看个人情况删减,比如分页插件用不到就不要配置,mybatis的懒加载用不到也不需要配置,但是mapper的xml路径需要配置,因为我们重写了sqlSessionFactory,springboot就会使用我们自己的,这样AutoConfiguration就不起作用了,需要手动去配置
最后一步:
在项目入口类需要排除DataSourceAutoConfiguration类的自动配置
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
不然会报找到多个数据源的错误,排除掉后就会根据路由中介的key去配置数据源了
使用方式:
@TargetDataSource("ds_ku1") public Employee getEmployeeById(Long empId) { Employee employee = employeeMapper.getEmployeeById(empId); return employee; }
通过注解,在运行时就切换到ds_ku1数据源上了。
还有一种方式采用分包的方式,即在配置数据源时使用MapperScan扫描不同的mapper包,这样代理的时候就会采用不同的数据源,不过这样代码改动大,增加一种就要加个mapper包以及配置数据源,分的更细的话,service层也要分开,微服务的话肯定不行,因为这样的话就可以继续拆分项目了。
上面两种都是基于一个方法使用一个数据源,还有一种就是在同一个方法使用多个数据源,这样需要用到分布式数据源。有时间配置下springboot+Atomikos分布式事务,采用三阶段提交协议。后续。。。。