动态切换数据库(新增/修改/删除均通过配置文件)

场景:

假设我们的项目有多个数据库。我们一个请求过来的时候,是操作哪一个数据库,我们应该如何进行数据库的切换?
基于这个问题,下面做了一套通过 配置文件 + 注解 + 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)
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值