动态切换数据库(新增/修改/删除均通过配置文件)
场景:
假设我们的项目有多个数据库。我们一个请求过来的时候,是操作哪一个数据库,我们应该如何进行数据库的切换?
基于这个问题,下面做了一套通过 配置文件 + 注解 + AOP 的方式实现动态切换数据库的程序。当数据库增加,修改,
减少的时候,我们只需要去配置文件中进行修改,不必再去修改程序代码。
通过需求,一步一步思考:
基于我们假设的场景,再假设我们有两个数据库,datasource1 和 datasource2 ,项目默认配置的是datasource1 。
那么现在我们有一个请求,需要向datasource2 插入一条数据,应该怎么办呢?一般来说,我们能做到如下步骤:
--------controller层接收到请求:
@Controller
public class DataSourceController {
@Autowired
private DataSourceService dataSourceService;
@RequestMapping("/insert2")
@ResponseBody
public Integer insert02(String name, Integer age){
return dataSourceService.insertUser2(name, age);
}
}
--------service层,dao层处理:
public interface DataSourceService {
Integer insertUser2(String name, Integer age);
}
@Service("dataSourceService")
public class DataSourceServiceImpl implements DataSourceService{
@Resource
private DataSourceDao dataSourceDao;
@Override
public Integer insertUser2(String name, Integer age) {
int result = dataSourceDao.insert("2", age);
int i = 1 / age;
return result;
}
}
@Mapper
public interface DataSourceDao {
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
}
写到这里,大家也许就有疑惑了,我们项目里,默认配置的数据库是 datasource1 ,现在这样写,是将数据插入到
datasource1 里面去了啊。所以,得改啊,怎么改呐?在 service层里使用 jdbc手动连接到 datasource2?这样
单个地方使用多数据库还好,如果地方很多,那么不管是写的时候,还是后面维护的时候,都是极其不方便的。所以想一
下,通过什么样的方式,我们可以很好的去管理这些数据库的切换呢?配置文件 + 注解 的方式好像很不错啊。所以接下
来,终于到正题了!我们可以通过 配置文件 + 注解 来配置数据库,再通过AOP在方法执行前切换到所配置的数据库。
首先定义好数据库配置文件的配置格式
因为 我们要通过这个配置来获取数据库连接,所以必须配置上 数据库的几个要素(这里我是配置到
application.properties中的,这个配置的位置随意,只需要修改获取的配置文件名即可):
##########数据源配置-------》主要用于使用AOP来切换数据源
dynamic.datasource.defaultdatasource.jdbc-url=jdbc:mysql://localhost:3306/sysmanage?serverTimezone=GMT
dynamic.datasource.defaultdatasource.username=root
dynamic.datasource.defaultdatasource.password=811993
dynamic.datasource.defaultdatasource.driver-class-name=com.mysql.cj.jdbc.Driver
###datasource01
dynamic.datasource.methodname1=datasource1
dynamic.datasource.datasource1.jdbc-url=jdbc:mysql://localhost:3306/sysmanage?serverTimezone=GMT
dynamic.datasource.datasource1.username=root
dynamic.datasource.datasource1.password=811993
dynamic.datasource.datasource1.driver-class-name=com.mysql.cj.jdbc.Driver
###datasource02
dynamic.datasource.methodname2=datasource2
dynamic.datasource.datasource2.jdbc-url=jdbc:mysql://localhost:3306/springboottest?serverTimezone=GMT
dynamic.datasource.datasource2.username=root
dynamic.datasource.datasource2.password=811993
dynamic.datasource.datasource2.driver-class-name=com.mysql.cj.jdbc.Driver
有了配置,再创建一个读取配置文件的工具类
public class PropertiesUtil {
private static Properties props = null;
static {
//先读取配置文件中指定前缀的所有配置
Resource resource = new ClassPathResource("/application.properties");
try {
props = PropertiesLoaderUtils.loadProperties(resource);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 通过指定前缀(key)来获取,指定前缀的所有的值
* @param preFix
* @return
*/
public static Map<String, String> getPropertiesByPrefix(String preFix){
Map<String, String> valueMap = new HashMap<String, String>();
if (props != null) {
Enumeration<Object> keys = props.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
if(key.startsWith(preFix)){
valueMap.put(key, props.getProperty(key));
}
}
}
return valueMap;
}
}
配置文件有了,也能读取到配置了,我们就在程序启动的时候,将所有的数据源,全部加载好,方便在项目中切换
package com.dynamicdatasource.datasourceopt.configs;
import com.dynamicdatasource.datasourceopt.DynamicDataSource;
import com.dynamicdatasource.methodCreate.JavaSsistMethodCreater;
import com.dynamicdatasource.properties.PropertiesUtil;
import org.apache.ibatis.javassist.CtClass;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 动态数据源配置类
* 数据源配置在 application.properties 文件中
* 将数据源获取放到 AbstractRoutingDataSource 类中
* https://blog.csdn.net/qq_34968945/article/details/83246958
* springboot jdbc基于注解方式的事务管理
*/
@Configuration
@EnableConfigurationProperties
public class DynamicDataSourceConfig {
//常量,数据源前缀
private static String DATASOURCE_PREFIX = "dynamic.datasource";
//存放配置文件中的数据源配置信息
private static Map<String, String> propDataSourceMap = new HashMap<String, String>();
static {
//读取application.properties中的,所有的 spring.datasource 开头的配置,获取key-value
propDataSourceMap = PropertiesUtil.getPropertiesByPrefix(DATASOURCE_PREFIX);
}
/** 从配置文件中读取配置,并返回对应的数据库连接 **/
//默认数据源,必须要有,在没有配置注解的时候,就用它
@Bean(name="defaultdatasource")
@ConfigurationProperties(prefix="dynamic.datasource.defaultdatasource") //springboot使用@ConfigurationProperties注解来根据application.properties中的key获取其value
public DataSource defaults(){
return DataSourceBuilder.create().build();
}
//配置动态数据源,通过aop切换数据源
@Primary
//对同一个接口,可能会有几种不同的实现类,@Primary 告诉spring 在犹豫的时候优先选择哪一个具体的实现(dataSource1(),dataSource2(),dynamicDataSource()默认选择dynamicDataSource())
@Bean(name="dynamicDataSource")
public DataSource dynamicDataSource(){
//该类继承了jdbc 中的 AbstractRoutingDataSource 类,返回了一个数据源的名字
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//设置默认的数据源
dynamicDataSource.setDefaultTargetDataSource(defaults());
//该集合中,存放所有的数据源,便于切换
Map<Object, Object> dbMap = new HashMap<Object, Object>();
//类全路径: 就是 com.dynamicdatasource.datasourceopt.configs.DynamicDataSourceConfig
String classFullPath = this.getClass().getName().substring(0, this.getClass().getName().indexOf("$"));
//先读取配置文件中所有的方法
Map<String, String> methodMap = getMapFromMapByKeyPrefix(DATASOURCE_PREFIX + ".methodname");
//遍历方法集合
Set<String> keySet = methodMap.keySet();
Map<String, CtClass> map = new HashMap<String, CtClass>();
CtClass cc = null;
for(String key : keySet){
//就是方法名
String value = methodMap.get(key);
//拼接方法体 "public String test() { return \"test() is called \"+ toString(); }"
String prefix = DATASOURCE_PREFIX + "." + value;
String url = propDataSourceMap.get(prefix + ".jdbc-url"),
username = propDataSourceMap.get(prefix + ".username"),
password = propDataSourceMap.get(prefix + ".password"),
driverClassName = propDataSourceMap.get(prefix + ".driver-class-name");
String methodStr = "public javax.sql.DataSource " + value + "(){" +
"org.springframework.boot.jdbc.DataSourceBuilder dataSource = org.springframework.boot.jdbc.DataSourceBuilder.create(); "+
"dataSource.username(\""+ username +"\");"+
"dataSource.password(\""+ password +"\");"+
"dataSource.driverClassName(\""+ driverClassName +"\");"+
"dataSource.url(\""+ url +"\");"+
"return dataSource.build();}";
//配置注解详细信息
Map<String, String> annotInfo = new HashMap<String, String>();
//通过 class.getName() 可以获取到 类的全路径(包名+类名)
//System.out.println(Bean.class.getName()); org.springframework.context.annotation.Bean
annotInfo.put(Bean.class.getName(), value);
cc = JavaSsistMethodCreater.CreateMethodByJavassist(classFullPath, methodStr, annotInfo);
map.put(value, cc);
}
Object obj = JavaSsistMethodCreater.useJavaSsistCreateMethod(classFullPath, cc, new DynamicDataSourceConfig());
Set<String> sets = map.keySet();
for(String s : sets){
try {
dbMap.put(s, obj.getClass().getDeclaredMethod(s).invoke(obj));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
dbMap.put("defaultdatasource", defaults());
//设置数据源集合
dynamicDataSource.setTargetDataSources(dbMap);
return dynamicDataSource;
}
/**
* 使用DataSourceTransactionManager进行事务管理
* 就是将 指定的数据源中的所有数据源,事务进行统一管理。
*
* 也就是说,一个数据源中的数据出问题,则所有数据源中的sql统一全部回滚。
* 只有所有数据源中的sql全部执行成功了,事务才统一提交
*/
@Bean(name="transactionManager")
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dynamicDataSource());
}
/**
* 根据指定的 key 前缀,从Map中获取所有该前缀开头的key-value
* @param keyPrefix
* @return
*/
private Map<String, String> getMapFromMapByKeyPrefix(String keyPrefix){
//返回值
Map<String, String> map = new HashMap<String, String>();
//遍历map
Set<String> keySet = propDataSourceMap.keySet();
for(String key : keySet){
if(!key.startsWith(keyPrefix)){
continue;
}
map.put(key, propDataSourceMap.get(key));
}
return map;
}
}
再加载这些数据源的时候,有两个问题,需要注意一下:
1. 因为是按照配置文件动态加载的,所以需要使用Java的动态代理,动态创建创建数据源的方法,这里使用的是
javassist
2. 这种多数据源,一定要注意,像一个请求操作多个数据源的时候,一定要让他们在同一个事务中,否则会导致一个成功,
一个失败的情况。
javassist动态生成方法
/**
* AbstractRoutingDataSource类的实现,主要用于数据源的切换(一般和AOP合用,就是AOP改变数据源的key,然后返回给 该类的determineCurrentLookupKey方法,从而得到指定key对应的数据源)
* (主要是从ThreadLocal中获取到的)
* AbstractRoutingDataSource:每次执行sql的时候,都会通过 AbstractRoutingDataSource 类,在内存中找到,
* 指定key所对应的数据源。key对应的数据源,可以在 DataSourceConfig 类中的dynamicDataSource方法中看到
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 根据Key获取数据源的信息,上层抽象函数的钩子
* 就是根据 数据源的名字,来获取数据源的详细信息
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
System.out.println("数据源为---" + DataSourceContextHolder.getDB());
return DataSourceContextHolder.getDB();
}
}
public class AppClassLoader extends ClassLoader {
private static class SingletonHolder {
public final static AppClassLoader instance = new AppClassLoader();
}
public static AppClassLoader getInstance() {
return SingletonHolder.instance;
}
private AppClassLoader() {
}
/**
* 通过classBytes加载类
*
* @param className
* @param classBytes
* @return
*/
public Class<?> findClassByBytes(String className, byte[] classBytes) {
return defineClass(className, classBytes, 0, classBytes.length);
}
/**
* 复制对象所有属性值,并返回一个新对象
*
* @param srcObj
* @return
*/
public Object getObj(Class<?> clazz, Object srcObj) {
try {
Object newInstance = clazz.getDeclaredConstructor().newInstance();
Field[] fields = srcObj.getClass().getDeclaredFields();
for (Field oldInstanceField : fields) {
String fieldName = oldInstanceField.getName();
oldInstanceField.setAccessible(true);
Field newInstanceField = newInstance.getClass().getDeclaredField(fieldName);
newInstanceField.setAccessible(true);
newInstanceField.set(newInstance, oldInstanceField.get(srcObj));
}
return newInstance;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public class JavaSsistMethodCreater {
/**
* 动态为指定类创建指定方法
* @param classFullPath 需要被创建方法的类的全路径
* @param methodStr 要创建的方法的字符串 "public String test() { return \"test() is called \"+ toString(); }"
* @return
*/
public static CtClass CreateMethodByJavassist(String classFullPath, String methodStr, Map<String, String> annotInfo){
try {
//ClassPool:CtClass对象的容器
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(classFullPath);
if(cc.isFrozen()){
cc.defrost();
}
//创建方法
CtMethod mthd = CtNewMethod.make(methodStr, cc);
//给类添加方法
cc.addMethod(mthd);
//如果传了注解信息进来,就为方法添加注解
if(annotInfo.size() > 0){
//给方法添加注解
ClassFile ccFile = cc.getClassFile();
ConstPool constpool = ccFile.getConstPool();
AnnotationsAttribute attr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
//遍历注解详细信息
Set<String> keySet = annotInfo.keySet();
for(String key : keySet){
Annotation annot = new Annotation(key, constpool);
if(key.equals("org.springframework.context.annotation.Bean")){
annot.addMemberValue("name", new StringMemberValue(annotInfo.get(key), ccFile.getConstPool()));
}else{
annot.addMemberValue("prefix", new StringMemberValue(annotInfo.get(key), ccFile.getConstPool()));
}
attr.addAnnotation(annot);
}
mthd.getMethodInfo().addAttribute(attr);
}
return cc;
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
}
return null;
}
/**
* 获取指定类
* @param classFullPath
* @param cc
* @param srcObj
* @return
*/
public static Object useJavaSsistCreateMethod(String classFullPath, CtClass cc, Object srcObj){
AppClassLoader appClassLoader = AppClassLoader.getInstance();
Class<?> clazz = null;
try {
clazz = appClassLoader.findClassByBytes(classFullPath, cc.toBytecode());
Object obj = appClassLoader.getObj(clazz, srcObj);
//测试反射调用添加的方法
return obj;
} catch (IOException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
}
return null;
}
数据源都有了,我们可以继续创建一个注解,来配置数据源
/**
* 自定义方法注解
* 用于为当前方法动态配置数据源,如果当前方法没有配置该注解,则使用默认数据源
*/
@Target(ElementType.METHOD)
//当前注解如何去保持
@Retention(RetentionPolicy.RUNTIME)
//生成到API文档
@Documented
public @interface DynamicDataSource {
//设置 默认数据源,其值必须是 application.properties 中配置的默认数据源,也就是DataSourceContextHolder.DEFAULT_DB一致
String value() default DataSourceContextHolder.DEFAULT_DB;
}
最后,我们就可以使用AOP进行数据源切换了
@Aspect
@Component
@Order(-1) //@Order 值越小,优先级越高
public class DynamicDataSourceAop {
/**
* 定义切点(注意:该方法只用申明,内部不用写任何代码)
* @Pointcut 注解作用:定义切点
* @annotation 注解作用:将切点定义为一个注解
*/
@Pointcut("@annotation(com.dynamicdatasource.annotation.DynamicDataSource)")
public void dynamicDataSourcePointCuts() {}
/**
* aop的前置通知,主要用于 将指定的数据源,使用 DataSourceContextHolder 保存到 ThreadLocal 中,方便在
* 找到满足切点的方法,并在执行该方法前,执行该通知
* @param point
*/
@Before("dynamicDataSourcePointCuts()")
public void before(JoinPoint point){
//获取当前访问的类名
Class<?> className = point.getTarget().getClass();
//获取当前访问的方法名
String methodName = point.getSignature().getName();
//获取方法的参数类型
Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
//默认数据源
String dataSource = DataSourceContextHolder.DEFAULT_DB;
try {
//获取访问的方法对象
Method method = className.getMethod(methodName, argClass);
//判断是否存在@DataSource注解
if(method.isAnnotationPresent(DynamicDataSource.class)){
//获取指定方法上的指定注解
DynamicDataSource annotation = method.getAnnotation(DynamicDataSource.class);
//获取注解中指定的数据源
dataSource = annotation.value();
}
} catch (Exception e) {
e.printStackTrace();
}
//切换到指定的数据源(就是将数据源的名字存储到 ThreadLocal 中)
DataSourceContextHolder.setDB(dataSource);
}
/**
* aop 后置通知,执行完方法后,将数据源从 ThreadLocal 中移除
* @param point
*/
@After("dynamicDataSourcePointCuts()")
public void after(JoinPoint point){
DataSourceContextHolder.clearDB();
}
}
/**
* AOP配置类,用于启动AOP功能
*/
@Configuration //JAVA配置类
@ComponentScan("com.dynamicdatasource") //Bean扫描器
@EnableAspectJAutoProxy //开启spring对aspectJ的支持
public class AopConfig {
}
/**
* 在aop中保存,删除,清空当前数据源,
* 设置默认数据源
* 数据源存储在本地线程 ThreadLocal 中,方便随时可以取用
*/
public class DataSourceContextHolder {
//默认的数据源
public static final String DEFAULT_DB = "defaultdatasource";
//将数据源存放到 本地线程中,方便获取
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
//设置数据源(名字)
public static void setDB(String dbType){
System.out.println("切换到---" + dbType + "---数据源");
contextHolder.set(dbType);
}
//获取数据源(名字)
public static String getDB(){
return (contextHolder.get());
}
//清除数据源(名字)
public static void clearDB(){
contextHolder.remove();
}
}
最后再将 service层修改一下:
@DynamicDataSource("datasource2")
@Transactional
@Override
public Integer insertUser2(String name, Integer age) {
int result = dataSourceDao.insert("2", age);
int i = 1 / age;
return result;
}
到这里,基本上就可以了
参考:
[1](https://blog.csdn.net/qq_34968945/article/details/83246958)
[2](https://www.jianshu.com/p/efd06a32148d)