springboot基於注解動態配置多數據源以及多數據源的事務統一


參考文檔:https://www.cnblogs.com/zhangboyu/p/7622412.html

                 https://blog.csdn.net/qq_34322777/article/details/80833935

一、動態注入多數據源

1、配置多數據源配置文件(application-db.properties)

######多數據源配置文件####################
###第一個####
spring.datasource.first.name=first
spring.datasource.first.url=jdbc:oracle:thin:@127.0.0.1:1521:orcl
spring.datasource.first.username=sepcore
spring.datasource.first.password=sepcore
spring.datasource.first.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.first.mapperLocations=classpath:mappers/*Mapper.xml
####第二個####
spring.datasource.second.name=second
spring.datasource.second.url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
spring.datasource.second.username=root
spring.datasource.second.password=123456
spring.datasource.second.driverClassName=com.mysql.jdbc.Driver
spring.datasource.second.mapperLocations=classpath:mappers/*Mapper.xml

#####mapper接口所在包#######
scanner.mapperInterfacePackage=com.example.demo.mappers

2、讀取配置文件類(DataSourceConfig)

public class DataSourceConfig {
    /**
     * 存儲dataSource、SqlSessionTemplate、DataSourceTransactionManager
     */
 private  Map<String,Map<String,Object>>mapMap;
    /**
     * 獲取mybatis掃描的指定接口包(所有數據源的接口放在同一的父包下面)
     */
 private  String mapperInterfacePackage;

 public DataSourceConfig(){
        mapMap = new HashMap<>();
        InputStream in = DataSourceConfig.class.getClassLoader().
                getResourceAsStream("application-db.properties");
        Properties properties = new Properties();
        try {
            properties.load(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
        Set<String> set = properties.stringPropertyNames();
        for (String s : set) {
            //判斷是否是mapper接口指定包路徑
            if (s.contains("mapperInterfacePackage")){
                mapperInterfacePackage = properties.get(s).toString();
                continue;
            }
            String key = s.substring(0, s.lastIndexOf("."));
            if (mapMap.containsKey(key)){
                Map<String, Object> map = mapMap.get(key);
                map.put(s,properties.get(s));
            }else{
                Map<String,Object>map = new HashMap<>();
                map.put(s,properties.get(s));
                mapMap.put(key,map);
            }
        }
    }

    public String getMapperInterfacePackage() {
        return mapperInterfacePackage;
    }

    /**
     * 獲取SqlSessionTemplate
     * @return
     * @throws Exception
     */
    public Map<String,Object>getSqlSessionTemplateAndDataSource() throws Exception {
        Set<Map.Entry<String, Map<String, Object>>> entries = this.mapMap.entrySet();
        Map<String,Object>result = new HashMap<>(entries.size());
        for (Map.Entry<String, Map<String, Object>> entry : entries) {
            String key = entry.getKey();
            Map<String, Object> map = entry.getValue();
            DataSource dataSource = DataSourceBuilder.create().url(map.get(key+".url").toString()).
                    username(map.get(key+".username").toString()).password(map.get(key+".password").toString()).
                    driverClassName(map.get(key+".driverClassName").toString()).
                    build();
            //為每個數據源設置事務
            DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
            dataSourceTransactionManager.setDataSource(dataSource);

            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            //設置dataSource數據源
            sqlSessionFactoryBean.setDataSource(dataSource);
            //設置*mapper.xml路徑
            sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(map.get(key+".mapperLocations").toString()));
            String s = map.get(key + ".name").toString();
            result.put(s+"SqlSessionTemplate",new SqlSessionTemplate(sqlSessionFactoryBean.getObject()));
            result.put(s+"DataSource",dataSource);
            result.put(s+"DataSourceTransactionManager",dataSourceTransactionManager);
        }
        return result;
    }
}

3、使用注解(DataSourceRoute),確定每個mapper接口使用哪個數據源

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceRoute {
    String name() default "first";
}
@DataSourceRoute
public interface SepUserMapper {
    List<Map<String,Object>> findByUserLevel(Long userLevel);

    void insert(Map<String,Object>map);
}
@DataSourceRoute(name = "second")
public interface IDiDataItemMapper {

    @Select("SELECT dataitem_id,name FROM di_dataitem WHERE dataitem_id=#{dataItemId}")
    Map<String,Object>selectOne(Long dataItemId);

    void insert(Map<String, Object> map);
}

4、掃描指定包下面的mapper接口

public class ClassScanner {

    public static Map<String,Class<?>>getMapperInterface(String mapperInterfacePackage) throws Exception {
        Map<String,Class<?>>classMap = new HashMap<>();
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        //將"."替換成"/"
        String packagePath = mapperInterfacePackage.replace(".", "/");
        URL url = loader.getResource(packagePath);
        List<String> fileNames = null;
        if (url != null) {
            String type = url.getProtocol();
            if ("file".equals(type)) {
                fileNames = getClassNameByFile(url.getPath(), null, true);
            }
        }
        for (String classPath : fileNames) {
            classMap.putAll(getClassByPath(classPath));
        }
        return classMap;
    }

    /**
     * 讀取package下的所有類文件
     * @param filePath
     * @param className
     * @param childPackage
     * @return
     */
    private static List<String> getClassNameByFile(String filePath, List<String> className, boolean childPackage) {
        List<String> myClassName = new ArrayList<>();
        File file = new File(filePath);
        File[] childFiles = file.listFiles();
        for (File childFile : childFiles) {
            if (childFile.isDirectory()) {
                if (childPackage) {
                    myClassName.addAll(getClassNameByFile(childFile.getPath(), myClassName, childPackage));
                }
            } else {
                String childFilePath = childFile.getPath();
                if (childFilePath.endsWith(".class")) {
                    childFilePath = childFilePath.substring(childFilePath.indexOf("\\classes") + 9,
                            childFilePath.lastIndexOf("."));
                    childFilePath = childFilePath.replace("\\", ".");
                    myClassName.add(childFilePath);
                }
            }
        }
        return myClassName;
    }

    /**
     * 將Mapper的標准文件,轉成 Mapper Class
     * @param classPath
     * @return
     * @throws Exception
     */
    private static Map<String, Class<?>> getClassByPath(String classPath)
            throws Exception{
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Map<String, Class<?>> classMap = new HashMap<>();
        classMap.put(getClassAlias(classPath),loader.loadClass(getFullClassName(classPath)));
        return classMap;
    }

    /**
     * 將Mapper的標准文件,轉成java標准的類名稱
     * @param classPath
     * @return
     * @throws Exception
     */
    private static String getFullClassName(String classPath)
            throws Exception{
        int comIndex = classPath.indexOf("com");
        classPath = classPath.substring(comIndex);
        classPath = classPath.replaceAll("\\/", ".");
        return classPath;
    }

    /**
     * 根據類地址,獲取類的Alais,即根據名稱,按照駝峰規則,生成可作為變量的名稱
     * @param classPath
     * @return
     * @throws Exception
     */
    private static String getClassAlias(String classPath)
            throws Exception{
        String  split = "\\/";
        String[] classTmp = classPath.split(split);
        String className = classTmp[classTmp.length-1];
        return toLowerFisrtChar(className);
    }

    /**
     * 將字符串的第一個字母轉小寫
     * @param className
     * @return
     */
    private static String toLowerFisrtChar(String className){
        String  fisrtChar = className.substring(0,1);
        fisrtChar = fisrtChar.toLowerCase();
        return fisrtChar+className.substring(1);
    }

5、使用BeanFactoryPostProcessor動態插入數據源

@Component
public class DataSourceBean implements BeanFactoryPostProcessor{

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        System.out.println("在spring處理bean前,將自定義的bean注冊到容器中======================");
        DataSourceConfig dataSourceConfig = new DataSourceConfig();
        try {
            Map<String, Object> sqlSessionTemplateAndDataSource = dataSourceConfig.getSqlSessionTemplateAndDataSource();
            Map<String, Class<?>> mapperInterface = ClassScanner.getMapperInterface(dataSourceConfig.getMapperInterfacePackage());
            Set<Map.Entry<String, Class<?>>> entries = mapperInterface.entrySet();
            for (Map.Entry<String, Class<?>> entry : entries) {
                MapperFactoryBean mapperFactoryBean = new MapperFactoryBean();
                Class<?> value = entry.getValue();

                DataSourceRoute dataSourceRoute = value.getAnnotation(DataSourceRoute.class);
                if (null==dataSourceConfig){
                    continue;
                }
                String name = dataSourceRoute.name();
                SqlSessionTemplate template = (SqlSessionTemplate) sqlSessionTemplateAndDataSource.get(name + "SqlSessionTemplate");
                mapperFactoryBean.setMapperInterface(value);
                mapperFactoryBean.setSqlSessionTemplate(template);
                mapperFactoryBean.afterPropertiesSet();

                configurableListableBeanFactory.registerSingleton(name+"MapperFactory",mapperFactoryBean.getObject());
                configurableListableBeanFactory.registerSingleton(name+"DataSource",sqlSessionTemplateAndDataSource.get(name + "DataSource"));
                configurableListableBeanFactory.registerSingleton(name+"SqlSessionTemplate",template);
                configurableListableBeanFactory.registerSingleton(name+"DataSourceTransactionManager",sqlSessionTemplateAndDataSource.get(name+"DataSourceTransactionManager"));

            }
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
    }
}

至此多數據源動態加載就完成了。

二、多數據源統一事務控制

當使用多數據源時,單一的事務會出現問題(當在service層同時操作兩個數據源時,當發生異常,只會回滾離拋出異常最近的數據源的數據)

1、自定義事務注解

@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomTransaction {
    String[] name() default {"firstDataSourceTransactionManager"};
}

2、創建aop切面進行事務控制

@Component
@Aspect
public class TransactionAop {
    @Pointcut(value = "@annotation(com.example.demo.annon.CustomTransaction)")
   public void pointCut(){}


    @Around(value = "pointCut()&&@annotation(annotation)")
    public Object twiceAsOld(ProceedingJoinPoint point, CustomTransaction annotation) throws Throwable {
        Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack = new Stack<DataSourceTransactionManager>();
        Stack<TransactionStatus> transactionStatuStack = new Stack<TransactionStatus>();
        try {
            if (!openTransaction(dataSourceTransactionManagerStack, transactionStatuStack, annotation)) {
                return null;
            }
            Object ret = point.proceed();
            commit(dataSourceTransactionManagerStack, transactionStatuStack);
            return ret;
        } catch (Throwable e) {
            rollback(dataSourceTransactionManagerStack, transactionStatuStack);
            throw e;
        }
    }
    /**
     * 開啟事務處理方法
     *
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     * @param multiTransactional
     * @return
     */
    private boolean openTransaction(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
                                    Stack<TransactionStatus> transactionStatuStack, CustomTransaction multiTransactional) {

        String[] transactionMangerNames = multiTransactional.name();
        if (ArrayUtils.isEmpty(multiTransactional.name())) {
            return false;
        }

        for (String beanName : transactionMangerNames) {
            //根據事務名稱獲取具體的事務
            DataSourceTransactionManager dataSourceTransactionManager = (DataSourceTransactionManager) SpringContextUtil
                    .getBean(beanName);
            TransactionStatus transactionStatus = dataSourceTransactionManager
                    .getTransaction(new DefaultTransactionDefinition());
            transactionStatuStack.push(transactionStatus);
            dataSourceTransactionManagerStack.push(dataSourceTransactionManager);
        }
        return true;
    }

    /**
     * 提交處理方法
     *
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     */
    private void commit(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
                        Stack<TransactionStatus> transactionStatuStack) {
        while (!dataSourceTransactionManagerStack.isEmpty()) {
            dataSourceTransactionManagerStack.pop().commit(transactionStatuStack.pop());
        }
    }
    /**
     * 回滾處理方法
     *
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     */
    private void rollback(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
                          Stack<TransactionStatus> transactionStatuStack) {
        while (!dataSourceTransactionManagerStack.isEmpty()) {
            dataSourceTransactionManagerStack.pop().rollback(transactionStatuStack.pop());
        }
    }
}

3、在service層指定使用哪個事務

//注意事務的命名規則 
@CustomTransaction(name = {"firstDataSourceTransactionManager","secondDataSourceTransactionManager"}) public void setSepUserMapper(){ //操作數據源2 Map<String,Object>mm = new HashMap<>(2); mm.put("dataitemId",1L); mm.put("name","測試"); diDataItemMapper.insert(mm); //操作數據源1 Map<String,Object>map = new HashMap<>(3); map.put("userId",1L); map.put("userName","張三"); map.put("name","平台管理員"); sepUserMapper.insert(map); throw new RuntimeException("sfsa"); }

 輔助類:SpringContextUtil

@Component
public class SpringContextUtil implements ApplicationContextAware{
    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    /**
     * @Description: 獲取spring容器中的bean,通過bean名稱獲取
     * @param beanName bean名稱
     * @return: Object 返回Object,需要做強制類型轉換
     * @author: zongf
     * @time: 2018-12-26 10:45:07
     */
    public static Object getBean(String beanName){
        return applicationContext.getBean(beanName);
    }

    /**
     * @Description: 獲取spring容器中的bean, 通過bean類型獲取
     * @param beanClass bean 類型
     * @return: T 返回指定類型的bean實例
     * @author: zongf
     * @time: 2018-12-26 10:46:31
     */
    public static <T> T getBean(Class<T> beanClass) {
        return applicationContext.getBean(beanClass);
    }

    /**
     * @Description: 獲取spring容器中的bean, 通過bean名稱和bean類型精確獲取
     * @param beanName bean 名稱
     * @param beanClass bean 類型
     * @return: T 返回指定類型的bean實例
     * @author: zongf
     * @time: 2018-12-26 10:47:45
     */
    public static <T> T getBean(String beanName, Class<T> beanClass){
        return applicationContext.getBean(beanName,beanClass);
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM