手机用户请
横屏
获取最佳阅读体验,REFERENCES
中是本文参考的链接,如需要链接和更多资源,可以关注其他博客发布地址。
平台 | 地址 |
---|---|
CSDN | https://blog.csdn.net/sinat_28690417 |
简书 | https://www.jianshu.com/u/3032cc862300 |
个人博客 | http://xiazhaoyang.tech/ |
开发环境描述
------------------------------------------------------------
Gradle 4.7
------------------------------------------------------------
Build time: 2018-04-18 09:09:12 UTC
Revision: b9a962bf70638332300e7f810689cb2febbd4a6c
Groovy: 2.4.12
Ant: Apache Ant(TM) version 1.9.9 compiled on February 2 2017
JVM: 1.8.0_171 (Oracle Corporation 25.171-b11)
OS: Mac OS X 10.13.5 x86_64
依赖描述
spring-boot-starter-web:2.1.0.RELEASE
mybatis-spring-boot-starter:1.3.2
aspectjrt:1.9.2
aspectjweaver:1.9.2
mysql-connector-java:8.0.13
准备两个数据源
(mysql中schema和database是一个意思)并生成对应Mapper
、Model
CREATE SCHEMA db_capsule;
CREATE SCHEMA db_flowable;
分别在这两个数据库中创建一张表:db_capsule.tb_common_user_info
、db_flowable.tb_common_account_info
create table tb_common_user_info
(
user_id bigint auto_increment
comment '人员ID'
primary key,
age int not null
comment '年龄',
name varchar(128) not null
comment '姓名',
email varchar(128) not null
comment '邮箱',
remark varchar(1024) null
comment '备注',
is_delete int default '0' null
comment '表明数据是否已删除 0-未删除,1-已删除',
create_time datetime default CURRENT_TIMESTAMP not null
comment '创建时间'
)
comment '通用人员信息表';
create table tb_common_account_info
(
account_id bigint auto_increment
comment '账号ID'
primary key,
account_name varchar(128) not null
comment '登录名',
user_id bigint null
comment '关联用户ID',
remark varchar(1024) null
comment '备注',
is_delete int default '0' null
comment '表明数据是否已删除 0-未删除,1-已删除',
create_time datetime default CURRENT_TIMESTAMP not null
comment '创建时间'
)
comment '账号信息表';
生成基础Mapper(代码生成插件即可)
yaml中配置默认数据源和自定义数据源
spring:
mvc:
static-path-pattern: /**
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/static/flowable-modeler
# 默认主数据源
datasource:
type: com.zaxxer.hikari.HikariDataSource
username: xx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://x.x.x.x:3306/db_flowable?useUnicode=true&characterEncoding=utf-8
jpa:
hibernate:
ddl-auto: update
database: MYSQL
# 自定义数据源
custom:
datasource:
- key: capsule
type: com.alibaba.druid.pool.DruidDataSource
username: xxx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://x.x.x.x:3306/db_capsule?useUnicode=true&characterEncoding=utf-8
package com.example;
import com.example.common.config.database.DynamicDataSourceRegister;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
*
* - SpringBootApplication 启动类
* - ComponentScan 实例扫描
* - MapperScan Mybatis Dao 扫描
* - EnableTransactionManagement 开启事务
* - Import 启动前注入实例,动态切换数据源
*
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019/3/14 23:08
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/3/14
* @modify reason: {方法名}:{原因}
* ...
*/
@SpringBootApplication
@ComponentScan(basePackages = {"com.example"})
@Import({DynamicDataSourceRegister.class})
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class CapsuleFlowableApplication {
public static void main(String[] args) {
SpringApplication.run(CapsuleFlowableApplication.class, args);
}
}
构建Service层测试代码
AccountInfoServiceImpl
package com.example.service.common.impl;
import com.example.common.base.BaseServiceImpl;
import com.example.common.base.RootMapper;
import com.example.common.config.database.TargetDataSource;
import com.example.core.common.dao.AccountInfoMapper;
import com.example.core.common.model.AccountInfo;
import com.example.core.common.model.AccountInfoExample;
import com.example.service.common.AccountInfoService;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.util.List;
/**
*
*
*
*
* @author xiazhaoyang
* @version V1.0.0
* @date 2019/03/24
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/03/24
* @modify reason: {方法名}:{原因}
* ...
*/
@Slf4j
@Service
public class AccountInfoServiceImpl extends BaseServiceImpl<AccountInfo,AccountInfoExample> implements AccountInfoService {
@Resource
private AccountInfoMapper accountInfoMapper;
@Override
public RootMapper<AccountInfo, AccountInfoExample> getMapper() {
return accountInfoMapper;
}
@Override
@TargetDataSource(name="dataSource")//此处为切换数据源的注解
public List<AccountInfo> selectList() {
return selectByExample(new AccountInfoExample());
}
}
UserInfoServiceImpl
package com.example.service.common.impl;
import com.example.common.config.database.TargetDataSource;
import com.example.core.common.dao.UserInfoMapper;
import com.example.core.common.model.UserInfo;
import com.example.core.common.model.UserInfoExample;
import com.example.service.common.UserInfoService;
import com.example.common.base.BaseServiceImpl;
import com.example.common.base.RootMapper;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.util.List;
/**
*
*
*
*
* @author xiazhaoyang
* @version V1.0.0
* @date 2019/03/24
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/03/24
* @modify reason: {方法名}:{原因}
* ...
*/
@Slf4j
@Service
public class UserInfoServiceImpl extends BaseServiceImpl<UserInfo,UserInfoExample> implements UserInfoService {
@Resource
private UserInfoMapper userInfoMapper;
@Override
public RootMapper<UserInfo, UserInfoExample> getMapper() {
return userInfoMapper;
}
@Override
@TargetDataSource(name="capsule")
public List<UserInfo> selectList() {
return selectByExample(new UserInfoExample());
}
@Override
@TargetDataSource(name="capsule")//此处为切换数据源的注解
public int insert(UserInfo userInfo) {
return super.insert(userInfo);
}
}
AccountInfoController
package com.example.web.controller.common;
import com.example.common.base.BaseController;
import com.example.common.base.ResponseJson;
import com.example.core.common.model.AccountInfo;
import com.example.service.common.AccountInfoService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Date;
/**
*
*
*
*
* @author xiazhaoyang
* @version V1.0.0
* @date 2019/03/24
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/03/24
* @modify reason: {方法名}:{原因}
* ...
*/
@Slf4j
@RestController
@RequestMapping("/common/accountInfo/")
public class AccountInfoController extends BaseController<AccountInfo>{
@Resource
AccountInfoService accountInfoService;
@Override
@RequestMapping(value="addition", method = RequestMethod.POST)
public ResponseJson add(@RequestBody AccountInfo accountInfo) {
accountInfoService.insert(AccountInfo.builder()
.accountId(1L)
.accountName("admin")
.createTime(new Date())
.userId(1L)
.remark("ADMIN REMARK")
.isDelete(0)
.build());
return new ResponseJson();
}
@Override
@RequestMapping(value="deletion", method = RequestMethod.POST)
public ResponseJson delete(@RequestParam String id) {
return new ResponseJson();
}
@Override
@RequestMapping(value="update", method = RequestMethod.POST)
public ResponseJson update(@RequestBody AccountInfo accountInfo) {
return new ResponseJson();
}
@Override
@RequestMapping(value="detail", method = RequestMethod.POST)
public ResponseJson detail(@RequestParam String id) {
return ResponseJson.OK(accountInfoService.selectList());
}
}
UserInfoController
package com.example.web.controller.common;
import com.example.core.common.model.AccountInfo;
import com.example.core.common.model.UserInfo;
import com.example.service.common.UserInfoService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import com.example.common.base.BaseController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import com.example.common.base.ResponseJson;
import java.util.Date;
/**
*
*
*
*
* @author xiazhaoyang
* @version V1.0.0
* @date 2019/03/24
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/03/24
* @modify reason: {方法名}:{原因}
* ...
*/
@Slf4j
@RestController
@RequestMapping("/common/userInfo/")
public class UserInfoController extends BaseController<UserInfo>{
@Resource
UserInfoService userInfoService;
@Override
@RequestMapping(value="addition", method = RequestMethod.POST)
public ResponseJson add(@RequestBody UserInfo userInfo) {
userInfoService.insert(UserInfo.builder()
.userId(1L)
.age(11)
.createTime(new Date())
.email("[email protected]")
.name("Yiyuery")
.remark("xxx")
.build());
return new ResponseJson();
}
@Override
@RequestMapping(value="deletion", method = RequestMethod.POST)
public ResponseJson delete(@RequestParam String id) {
return new ResponseJson();
}
@Override
@RequestMapping(value="update", method = RequestMethod.POST)
public ResponseJson update(@RequestBody UserInfo userInfo) {
return new ResponseJson();
}
@Override
@RequestMapping(value="detail", method = RequestMethod.POST)
public ResponseJson detail(@RequestParam String id) {
return ResponseJson.OK(userInfoService.selectList());
}
}
/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/3/23 17:05
* @email: [email protected]
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.common.config.database;
import java.lang.annotation.*;
/**
*
* 在方法上使用,用于指定使用哪个数据源
*
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019/3/23 17:05
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/3/23
* @modify reason: {方法名}:{原因}
* ...
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String name();
}
DynamicDataSourceRegister
数据源实例bean注册 /*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/3/23 17:04
* @email: [email protected]
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.common.config.database;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.boot.context.properties.bind.Binder;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.example.common.util.CapsuleStringUtil.requireNotSatisfy;
/**
*
*
*
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019/3/23 17:04
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/3/23
* @modify reason: {方法名}:{原因}
* ...
*/
@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
/**
* 参数绑定工具
*/
private Binder binder;
/**
* 如配置文件中未指定数据源类型,使用该默认值
*/
private static final Object DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
/**
* 默认数据源
*/
private DataSource defaultDataSource;
/**
* 自定义数据源
*/
private Map<String, DataSource> customDataSources = new HashMap<>();
/**
* 数据源参数配置别名
*/
private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); //别名
/**
* 配置上下文(也可以理解为配置文件的获取工具)
*/
private Environment env;
static {
//由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况
aliases.addAliases("url", "jdbc-url");
aliases.addAliases("username", "user");
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
// 将主数据源添加到更多数据源中
targetDataSources.put("dataSource", defaultDataSource);
DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
// 添加更多数据源
targetDataSources.putAll(customDataSources);
DynamicDataSourceContextHolder.dataSourceIds.addAll(customDataSources.keySet());
// 创建DynamicDataSource
GenericBeanDefinition define = new GenericBeanDefinition(); //bean定义类
define.setBeanClass(DynamicDataSource.class); //设置bean的类型,此处DynamicDataSource是继承AbstractRoutingDataSource的实现类
MutablePropertyValues mpv = define.getPropertyValues(); //需要注入的参数,类似spring配置文件中的
mpv.add("defaultTargetDataSource", defaultDataSource); //添加默认数据源,避免key不存在的情况没有数据源可用
mpv.add("targetDataSources", targetDataSources); //添加其他数据源
registry.registerBeanDefinition("datasource", define); //将该bean注册为datasource,不使用spring-boot自动生成的datasource
log.info("Dynamic DataSource Registry");
}
/**
* 创建DataSource
*
* @return
* @author SHANHY
* @create 2016年1月24日
*/
private DataSource buildDataSource(Map<String, Object> dsMap) {
try {
Object type = dsMap.get("type");
if (type == null)
type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource
Class<? extends DataSource> dataSourceType;
dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
String driverClassName = dsMap.get("driver-class-name").toString();
String url = dsMap.get("url").toString();
String username = dsMap.get("username").toString();
String password = dsMap.get("password").toString();
DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
.username(username).password(password).type(dataSourceType);
return factory.build();
} catch (Throwable e) {
log.error("buildDataSource failed!",e);
}
return null;
}
/**
* 加载多数据源配置
*/
@Override
public void setEnvironment(Environment environment) {
this.env = environment;
binder = Binder.get(env); //绑定配置器
initDefaultDataSource();
initCustomDataSources();
}
/**
* 初始化主数据源
*
* @author SHANHY
* @create 2016年1月24日
*/
private void initDefaultDataSource() {
//读取数据源参数配置
Map props = binder.bind("spring.datasource", Map.class).get();
Map<String, Object> dsMap = new HashMap<>();
dsMap.put("type", props.get("type"));
dsMap.put("driver-class-name", props.get("driver-class-name"));
dsMap.put("url", props.get("url"));
dsMap.put("username", props.get("username"));
dsMap.put("password", props.get("password"));
defaultDataSource = buildDataSource(dsMap);
dataBinder(defaultDataSource, props);
}
/**
* 初始化更多数据源
*
* @author SHANHY
* @create 2016年1月24日
*/
private void initCustomDataSources() {
// 读取配置文件获取更多数据源,也可以通过defaultDataSource读取数据库获取更多数据源
List<Map> configs = binder.bind("custom.datasource", Bindable.listOf(Map.class)).get();
String dsPrefix;
DataSource custom;
for (Map config : configs) {
dsPrefix = requireNotSatisfy(p -> StringUtils.isNotEmpty(config.get("key").toString()), config.get("key").toString(), "default");
custom = buildDataSource(config);
customDataSources.put(dsPrefix, custom);
dataBinder(custom, config);
}
}
/**
* 绑定参数,以下三个方法都是参考DataSourceBuilder的bind方法实现的,
* 目的是尽量保证我们自己添加的数据源构造过程与spring-boot保持一致
*
* @param dataSource
* @param properties
*/
private void dataBinder(DataSource dataSource, Map properties) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
Binder binderEx = new Binder(source.withAliases(aliases));
binderEx.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(dataSource)); //将参数绑定到对象
}
}
DynamicDataSourceContextHolder缓存
当前线程上下文中数据源标识ID/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/3/23 16:57
* @email: [email protected]
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.common.config.database;
import com.example.common.util.CapsuleStringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import static com.example.common.util.CapsuleStringUtil.requireNotSatisfyThrowException;
/**
*
*
*
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019/3/23 16:57
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/3/23
* @modify reason: {方法名}:{原因}
* ...
*/
@Slf4j
class DynamicDataSourceContextHolder {
//保存当前线程的数据源对应的key
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
static List<String> dataSourceIds = new ArrayList<>();
static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
static String getDataSourceType() {
try {
return requireNotSatisfyThrowException(p->StringUtils.isNotBlank(contextHolder.get()),contextHolder.get(),"can not found datasource by key: '%s',this session may use default datasource","");
} catch (NullPointerException e) {
contextHolder.set("dataSource");
log.error(e.getMessage());
//如果动态数据源获取为空,返回默认数据源
return contextHolder.get();
}
}
static void clearDataSourceType() {
contextHolder.remove();
}
/**
*
* 判断指定DataSrouce当前是否存在
* @return
* @author xiazhaoyang
* @date 2019/3/24 17:52
* @modify by: {修改人} 2019/3/24 17:52
* @modify by reason:
* @since 1.0.0
*/
static boolean containsDataSource(String dataSourceId){
return dataSourceIds.contains(dataSourceId);
}
}
DataSourceDynamicAspect
AOP切片解析注解 /*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/3/24 11:03
* @email: [email protected]
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.common.config.database;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.aspectj.lang.JoinPoint;
/**
*
* 声明数据源切面
*
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019/3/24 11:03
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/3/24
* @modify reason: {方法名}:{原因}
* ...
*/
@Component
@Aspect
@Order(-10) //使该切面在事务之前执行
@Slf4j
public class DataSourceDynamicAspect {
/**
* AOP切面拦截注解 TargetDataSource 先从当前线程中取出数据库标识
* @param point
* @param ds
*/
@Before("@annotation(ds)")
public void changeDataSource(JoinPoint point, TargetDataSource ds) {
String dsId = ds.name();
if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
log.error("数据源[{}]不存在,使用默认数据源 > {}", ds.name(), point.getSignature());
} else {
log.debug("Use DataSource : {} > {}", ds.name(), point.getSignature());
DynamicDataSourceContextHolder.setDataSourceType(ds.name());
}
}
/**
* AOP切面拦截注解 TargetDataSource 从当前线程中删除数据库标识
* @param point
* @param ds
*/
@After("@annotation(ds)")
public void restoreDataSource(JoinPoint point, TargetDataSource ds) {
log.debug("Revert DataSource : {} > {}", ds.name(), point.getSignature());
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
DynamicDataSource
动态数据源对象定义,用于数据源实例注册到Spring实例工厂后的路由/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/3/23 16:56
* @email: [email protected]
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.common.config.database;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
*
*
*
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019/3/23 16:56
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/3/23
* @modify reason: {方法名}:{原因}
* ...
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* AbstractRoutingDataSource抽象类实现方法,即获取当前线程数据源的key
*
* @return data unique key
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
AbstractRoutingDataSource
Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
calls to one of various target DataSources based on a lookup key. The latter is usually
(but not necessarily) determined through some thread-bound transaction context.
基于查找标识键来调用各种目标数据源之一的路由 {@link #getConnection ()} 的抽象实现{@link javax.sql.DataSource}。后者通常是(但不一定) 通过某些线程绑定事务上下文确定。
Spring Boot 动态数据源(多数据源自动切换)
springboot2动态数据源的绑定
Spring Boot AOP 不生效排查
Spring Boot 日志配置(超详细)
SpringBoot根据包名进行区分使用多数据源
Spring 中基于 AOP 的 @AspectJ
扫码关注“架构探险之道”,获取更多源码和文章资源
知识星球(扫码加入获取源码和文章资源链接)