context-path: /db

指定Mybatis的Mapper文件

mybatis:
mapper-locations: classpath:mappers/*xml
type-aliases-package: jin.panpan.database.entity
#日志配置
logging:
config: classpath:log4j2.xml

复制代码

配置文件, 除了基本的应用名称、端口、路径、日志等配置以外, 主要看数据库驱动的配置, 这里我们不像平时的单数据源的配置, 我们需要配置多个数据源, 上面示例的配置文件里, 由两个注意点:

  • db1和db2是我们自定义的数据源名称, 这个我们在后面的MybatisConfig文件里会用到
  • 数据源里的配置jdbc-url对应单数据源配置里的url, 这里使用url会注入失败
  • ${db1.username}等是导入的application-jdbc.properties文件里的配置值

application-jdbc.properties

db1.username = user
db1.password =
db1.url = jdbc:mysql://xx.xx.xx.xx:3306/db1
db2.username = user
db2.password =
db2.url = jdbc:mysql://xx.xx.xx.xx:3306/db2

复制代码

key-value结构, key可以在引入此文件的配置里使用${key}引用

这里我们使用两个数据库, 但是数据库的表结构一致(至少多数据源涉及到的表需要结构一致), 以避免不能共用一套代码

DataSourceType

public enum DataSourceType {
//NONE用来返回默认
NONE(“”),
DB1(“db1”),
DB2(“db2”),
;
//省略部分代码
}
复制代码
此文件是数据源的枚举, 这里的枚举我们只配置里一个参数, 可以依据业务扩展. 本枚举的值代表数据源的名称, 是和配置文件里的配置对应上的
另外说明一下, NONE是用来代表默认数据源的, 是为了方便编程添加的, 不是必须的
DataSourceUtil
public class DataSourceUtil {
private static final ThreadLocal localDataSource = new ThreadLocal<>();
private DataSourceUtil(){
}
public static DataSourceType get() {
return localDataSource.get();
}
public static void set(DataSourceType type){
localDataSource.set(type);
}
public static void remove() {
localDataSource.remove();
}
}

复制代码

数据源切换工具类, 这里的核心是ThreadLocal<DataSourceType>变量, ThreadLocal以前有过文档分析, 主要是用来维护线程内部变量的, 其中:

  • get()方法用来获取数据源
  • set()方法用来设置数据源
  • remove()用来清除数据源

MybatisConfig*

@Configuration
@MapperScan(“jin.panpan.database.dao”)
public class MybatisConfig {
//默认数据源
@Primary
@Bean(“db1”)
@ConfigurationProperties(prefix = “spring.datasource.db1”)
public DataSource dataSource1(){
return DataSourceBuilder.create().build();
}
@Bean(“db2”)
@ConfigurationProperties(prefix = “spring.datasource.db2”)
public DataSource dataSource2(){
return DataSourceBuilder.create().build();
}
//动态数据源选择
@Bean
public DynamicDataSource dynamicDataSource(@Qualifier(“db1”) DataSource db1,
@Qualifier(“db2”) DataSource db2){
Map<Object, Object> map = new HashMap<>();
//此处的key 要和DynamicDataSource类的determineCurrentLookupKey方法返回值一致
map.put(DataSourceType.DB1.getDatabase(), db1);
map.put(DataSourceType.DB2.getDatabase(), db2);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(map);
//设置默认数据库, 选择的数据库Bean注入时要带@Primary注释
dynamicDataSource.setDefaultTargetDataSource(db1);
return dynamicDataSource;
}
//会话工厂配置
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dynamicDataSource);
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources(“classpath:mappers/*xml”);
factoryBean.setMapperLocations(resources);
return factoryBean.getObject();
}
//事务管理配置
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource){
return new DataSourceTransactionManager(dynamicDataSource);
}
}

复制代码

本类主要用来配置和注入mybatis相关配置bean, 主要有下面几步:

  • 自动扫描mapper接口配置

@MapperScan(“jin.panpan.database.dao”)

一般常规配置, 如果不配置多数据源, 一般是在入口类上注解

  • 数据源注入

@ConfigurationProperties 导入配置文件里的配置

@Bean 指定数据源名称

@Primary 标记默认数据源

  • 数据源动态选择Bean注入

加载多个数据源, 如果需要的话, 指定默认数据源

需要注意数据源容器Map的key要和DataSourceType里的字段值一致, 否则无法切换

  • 会话工厂Bean配置

单数据源时, 一般无需手动配置; 多数据源时, 需要手动指定会话工厂, 但是配置是模式化的

  • 事务管理Bean配置

事务管理的配置也是模式化的, 一般无特殊处理之处

DynamicDataSource*

public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
String database = DataSourceUtil.get() == null ? null : DataSourceUtil.get().getDatabase();
logger.info(“DynamicDataSource, 动态数据源返回={}”, database);
return database;
}
}

复制代码

此类其实是动态路由数据源的核心类, 扩展AbstractRoutingDataSource类, 重写determineCurrentLookupKey方法, determineCurrentLookupKey方法的返回值就是动态数据源, 返回值需要和配置文件及MybatisConfig里的一一对应.

DataSource和DataSourceAspect*

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSource {
DataSourceType value() default DataSourceType.NONE;
}
复制代码
DataSource是一个方法注解, 用来作为数据源切换切入点, 此处可以使用类注解或者直接正则织入想监控的点
DataSourceAspect是注解的实现, 是核心实现
@Aspect
@Component
public class DataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
@Pointcut(“@annotation(jin.panpan.database.dynamic.datasource.annotate.DataSource)”)
public void pointCut() {
}
@SneakyThrows
@Around(“pointCut()”)
public Object around(ProceedingJoinPoint pjp) {
//数据源
DataSourceType dataSourceType = null;
//方法对象
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
//注释
DataSource dynamicDataSource = method.getAnnotation(DataSource.class);
String path = null;
//注释中指定数据源, 则使用数据库中的数据源
if(dynamicDataSource.value()!=null && dynamicDataSource.value() != DataSourceType.NONE){
dataSourceType = dynamicDataSource.value();
}
//否则从请求路径中获取
if(dataSourceType == null){
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
path = request.getRequestURL().toString();
//TODO 待优化 需截取域名进行检查
if(path.contains(DataSourceType.DB1.getDatabase())){
dataSourceType = DataSourceType.DB1;
}
if(path.contains(DataSourceType.DB2.getDatabase())){
dataSourceType = DataSourceType.DB2;
}
}
logger.info(“DataSource, 方法={}, 数据源={}, 路径={}, 注释自带数据源={}”,
method.getName(), dataSourceType==null ? null : dataSourceType.getDatabase(),
path, dynamicDataSource.value());
if(dataSourceType == null){
throw new Exception(“非法数据源”);