多数据源这里文字就不多介绍了,只需要知道多数据源使用场景就是-----------按需使用 (手动狗头)
配置多数据源的方式有多种,这里我用的是 Druid,其他的有兴趣可以自行上百度google一下,这里不做说明
本文用的是两个mysql,如果是其他的数据库,或者不同数据库,也可按照这样配置,只需修改数据库连接方式即可
直奔主题
第一步,修改yml配置文件改成:
spring:
datasource:
druid:
read:
username: root
password: abc123
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&serverTimezone=UTC&characterEncoding=utf8
initialSize: 5
minIdle: 5
maxActive: 20
write:
username: root
password: abc123
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test2?useUnicode=true&serverTimezone=UTC&characterEncoding=utf8
initialSize: 5
minIdle: 5
maxActive: 20
main:
#是否允许使用相同名称重新注册不同的bean实现
allow-bean-definition-overriding: true
第二步,逆向工程生成实体类---习惯性根据数据源分包
第三步,实现多数据源配置、切换
在此之前,有需要的先看下ThreadLocal简单介绍,后面要用哦!大佬可忽略
ThreadLocal,做线程本地变量,ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。使线程间的变量互不干扰。
好戏开场了。。。。。。
配置文件,实体类等都创建好了,下一步就是配置选择数据源了,毕竟我们要实现的是多数据源,那就存在数据源之间的切换!
所以,我们要定义一个类,来实现数据源之间的切换。有切换就会有存取关系,这个过程有大佬称为数据库上下文
这里用ThreadLocal来实现多数据源的存取
自定义类:DataSourceThreadLocal
/**
* @title: DataSourceLocalThead
* @description: 数据源名称存取
* @author: stuil
* @copyright: Copyright (c) 2020
* @version: 1.0
*/
public class DataSourceThreadLocal {
/**
* 用ThreadLocal存放当前切换后的数据源名称
*/
private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<>();
/**
* @description: 设置数据源
* @author: stuil
*/
public static void set(String dataSource){
contextHolder.set(dataSource);
}
/**
* @description: 获取数据源
* @author: stuil
*/
public static String get(){
return contextHolder.get();
}
/**
* @description: 切换之前,清除数据源
* @author: stuil
*/
public static void remove(){
contextHolder.remove();
}
}
上面的类包含了数据源的set和get方法,我们知道,我们的目的是为了数据源的切换,我们需要另一个类来进行数据源的切换。
spring提供了AbstractRoutingDataSource
AbstractRoutingDataSource是spring-jdbc包提供的一个了AbstractDataSource的抽象类,它实现了DataSource接口的用于获取数据库连接的方法。在进行数据源切换的时候,AbstractRoutingDataSource提供了程序运行时动态切换数据源的方法,在dao类或方法上标注需要访问数据源的关键字,路由到指定数据源,获取连接。
所以我们需要定义一个数据源切换的类,继承AbstractRoutingDataSource,并实现它的抽象方法determineCurrentLookupKey
MultipleDataSource:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @title: MultipleDataSource
* @description: 多数据源的切换
* @author: stuil
* @copyright: Copyright (c) 2020
* @version: 1.0
*/
public class MultipleDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceThreadLocal.get();
}
}
上面类我们可以获取到当前设置的数据源信息,接着,我们就要连接设置数据源了。
创建或修改MybatisPlusConfig:
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.stuil.datamany.datasource.MultipleDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.*;
/**
* @title: MybatisPlusConfig
* @description: Mybatis分页配置
* @author: stuil
* @copyright: Copyright (c) 2020
* @version: 1.0
*/
@Configuration
@MapperScan({"com.stuil.datamany.mapper", "com.stuil.datamany.mapper.*"})
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
/**
* @description: 用Druid 来配置连接的数据源
* @author: stuil
*/
@Bean(name = "read")
@ConfigurationProperties(prefix = "spring.datasource.druid.read")
public DataSource read() {
return DruidDataSourceBuilder.create().build();
}
/**
* @description: 用Druid 来配置连接的数据源
* @author: stuil
*/
@Bean(name = "write")
@ConfigurationProperties(prefix = "spring.datasource.druid.write")
public DataSource write() {
return DruidDataSourceBuilder.create().build();
}
/**
* 动态数据源配置
*
* @return
*/
@Bean
@Primary
public DataSource multipleDataSource(@Qualifier("read") DataSource read, @Qualifier("write") DataSource write) {
MultipleDataSource multipleDataSource = new MultipleDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
// 数据源键值对
targetDataSources.put("read", read);
targetDataSources.put("write", write);
//添加数据源
multipleDataSource.setTargetDataSources(targetDataSources);
//设置默认数据源
multipleDataSource.setDefaultTargetDataSource(read);
return multipleDataSource;
}
/**
* @description: sql会话工厂
* @author: stuil
*/
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(multipleDataSource(read(), write()));
// javabean 配置xml路径的话 yml里会失效
// 单路径 xml 路径
// sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*/*Mapper.xml"));
// 多路径 xml 路径
sqlSessionFactory.setMapperLocations(resolveMapperLocations());
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
// sql 打印
configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class);
sqlSessionFactory.setConfiguration(configuration);
sqlSessionFactory.setPlugins(new Interceptor[]{
//添加分页功能
paginationInterceptor()
});
return sqlSessionFactory.getObject();
}
/**
* @description: 多路径
* @author: stuil
*/
public Resource[] resolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath*:/mapper/*/*Mapper.xml");
mapperLocations.add("classpath*:/mapper/*Mapper.xml");
List<Resource> resources = new ArrayList();
if (mapperLocations != null) {
for (String mapperLocation : mapperLocations) {
try {
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
// ignore
}
}
}
return resources.toArray(new Resource[resources.size()]);
}
}
以下为上面的类的部分代码详解:
1.@Configuration,@MapperScan这两个个不说,懂的都懂,不懂自己上百度google一下
2.说下这一块
/**
* @description: 用Druid 来配置连接的数据源
* @author: stuil
*/
@Bean(name = "read")
@ConfigurationProperties(prefix = "spring.datasource.druid.read")
public DataSource read() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "read")这个自行查询
@ConfigurationProperties,它可以把同类的配置信息自动封装成实体类
这个地方的主要作用就是获取配置文件中的信息,自动赋值给DataSource,就是把数据源连接属性值赋值DataSource,
然后通过 DruidDataSourceBuilder.create().build() 来进行数据源的连接配置,springboot自动装配的,下面那个数据源的同理
3.这块代码
/**
* 动态数据源配置
*
* @return
*/
@Bean
@Primary
public DataSource multipleDataSource(@Qualifier("read") DataSource read, @Qualifier("write") DataSource write) {
MultipleDataSource multipleDataSource = new MultipleDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
// 数据源键值对
targetDataSources.put("read", read);
targetDataSources.put("write", write);
//添加数据源
multipleDataSource.setTargetDataSources(targetDataSources);
//设置默认数据源
multipleDataSource.setDefaultTargetDataSource(read);
return multipleDataSource;
}
| 优先方案,被注解的实现,优先被注入 |
| 先声明后使用,相当于多个实现起多个不同的名字,注入时候告诉我你要注入哪个 |
数据源存取类、mybatis-plus配置类配置完之后,就要实现自动切换了
这里用AOP+自定义注解来实现
言归正传,我们来创建一个元注解:SwitchDataSource
/**
* @title: SwitchDataSource
* @description: 数据源注切换注解
* @author: stuil
* @copyright: Copyright (c) 2020
* @version: 1.0
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchDataSource {
String value() default "read";
}
然后创建一个切面类:DataSourceAspect
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @title: DataSourceAspect
* @description: AOP自动切换数据源
* @author: stuil
* @copyright: Copyright (c) 2020
* @version: 1.0
*/
@Component
@Slf4j
@Aspect
@Order(-1)
public class DataSourceAspect {
@Pointcut("@within(com.stuil.datamany.datasource.SwitchDataSource) || @annotation(com.stuil.datamany.datasource.SwitchDataSource)")
public void pointCut(){
}
@Before("pointCut() && (@annotation(dataSource)||@within(dataSource))")
public void doBefore(SwitchDataSource dataSource){
// 设置数据源
DataSourceThreadLocal.set(dataSource.value());
}
@After("pointCut()")
public void doAfter(){
// 清除当前数据源
DataSourceThreadLocal.remove();
}
}
好了,我们来测试一下
import com.alibaba.fastjson.JSONObject;
import com.stuil.datamany.entity.gas.SysUserEntity;
import com.stuil.datamany.service.gas.SysUserService;
import com.stuil.datamany.service.gas2.CopyUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author stuil
* @since 2020-09-11
*/
@RestController
public class SysUserController {
@Autowired
SysUserService sysUserService;
@Autowired
CopyUserService copyUserService;
@RequestMapping("/index")
public String index(){
List<SysUserEntity> list = sysUserService.list();
copyUserService.list();
return JSONObject.toJSONString(list);
}
}
运行,启动,访问。。。
大功。。。。。。
失败。???????
emmmmmm......
我们来分析下,看下报错,提示库test中不存在这个表。。。
确实不存在!因为我们只设置了默认库,没有使用动态切换!
那你倒是用啊!
那就用。。
我们之前定义了一个切面类,关于AOP的东西想必大家也大概了解了,不了解的看上面文章。
我们虽然定义了,但是没有用到啊 所以我们现在要用自定义的切面类也就是AOP来进行数据源的切换
由于我们默认的是read库,所以我们只需要在操作write的时候,切换数据源即可。
首先,在你需要的server实现类的上方加上注解
@SwitchDataSource("write")
我们在write库的某个server内写一个抽象方法
public interface CopyUserService extends IService<CopyUserEntity> {
List<CopyUserEntity> getList();
}
下面的就不细说,只说一点,实现类里重写的方法要这样写
@Service
@SwitchDataSource("write")
public class CopyUserServiceImpl extends ServiceImpl<CopyUserMapper, CopyUserEntity> implements CopyUserService {
@Override
public List<CopyUserEntity> getList() {
return this.list();
}
}
这里用mapper调用也行,道理都是一样的
简单解释下,当我们加了那个注解之后,在访问这个方法之前会进入到切面类,进行数据源的切换,切换的数据源名字就是注解括号里的值。这个方法是利用了AOP的执行流程实现的
还是那句话,上面有AOP执行的流程,可以就看看
本篇项目结构: