<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于注解的springboot+mybatis的多數(shù)據(jù)源組件的實(shí)現(xiàn)

          共 20045字,需瀏覽 41分鐘

           ·

          2021-04-19 11:53

          點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”

          優(yōu)質(zhì)文章,第一時(shí)間送達(dá)

          76套java從入門到精通實(shí)戰(zhàn)課程分享

          通常業(yè)務(wù)開發(fā)中,我們會(huì)使用到多個(gè)數(shù)據(jù)源,比如,部分?jǐn)?shù)據(jù)存在mysql實(shí)例中,部分?jǐn)?shù)據(jù)是在oracle數(shù)據(jù)庫(kù)中,那這時(shí)候,項(xiàng)目基于springboot和mybatis,其實(shí)只需要配置兩個(gè)數(shù)據(jù)源即可,只需要按照

          dataSource - SqlSessionFactory - SqlSessionTemplate配置好就可以了。

          如下代碼,首先我們配置一個(gè)主數(shù)據(jù)源,通過@Primary注解標(biāo)識(shí)為一個(gè)默認(rèn)數(shù)據(jù)源,通過配置文件中的spring.datasource作為數(shù)據(jù)源配置,生成SqlSessionFactoryBean,最終,配置一個(gè)SqlSessionTemplate。

          @Configuration
          @MapperScan(basePackages = "com.xxx.mysql.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory")
          public class PrimaryDataSourceConfig {

              @Bean(name = "primaryDataSource")
              @Primary
              @ConfigurationProperties(prefix = "spring.datasource")
              public DataSource druid() {
                  return new DruidDataSource();
              }

              @Bean(name = "primarySqlSessionFactory")
              @Primary
              public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
                  SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
                  bean.setDataSource(dataSource);
                  bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
                  bean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
                  return bean.getObject();
              }

              @Bean("primarySqlSessionTemplate")
              @Primary
              public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("primarySqlSessionFactory") SqlSessionFactory sessionFactory) {
                  return new SqlSessionTemplate(sessionFactory);
              }
          }

          然后,按照相同的流程配置一個(gè)基于oracle的數(shù)據(jù)源,通過注解配置basePackages掃描對(duì)應(yīng)的包,實(shí)現(xiàn)特定的包下的mapper接口,使用特定的數(shù)據(jù)源。

          @Configuration
          @MapperScan(basePackages = "com.nbclass.oracle.mapper", sqlSessionFactoryRef = "oracleSqlSessionFactory")
          public class OracleDataSourceConfig {

              @Bean(name = "oracleDataSource")
              @ConfigurationProperties(prefix = "spring.secondary")
              public DataSource oracleDruid(){
                  return new DruidDataSource();
              }

              @Bean(name = "oracleSqlSessionFactory")
              public SqlSessionFactory oracleSqlSessionFactory(@Qualifier("oracleDataSource") DataSource dataSource) throws Exception {
                  SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
                  bean.setDataSource(dataSource);
                  bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:oracle/mapper/*.xml"));
                  return bean.getObject();
              }

              @Bean("oracleSqlSessionTemplate")
              public SqlSessionTemplate oracleSqlSessionTemplate(@Qualifier("oracleSqlSessionFactory") SqlSessionFactory sessionFactory) {
                  return new SqlSessionTemplate(sessionFactory);
              }
          }

          這樣,就實(shí)現(xiàn)了一個(gè)工程下使用多個(gè)數(shù)據(jù)源的功能,對(duì)于這種實(shí)現(xiàn)方式,其實(shí)也足夠簡(jiǎn)單了,但是如果我們的數(shù)據(jù)庫(kù)實(shí)例有很多,并且每個(gè)實(shí)例都主從配置,那這里維護(hù)起來(lái)難免會(huì)導(dǎo)致包名過多,不夠靈活。

              現(xiàn)在考慮實(shí)現(xiàn)一種對(duì)業(yè)務(wù)侵入足夠小,并且能夠在mapper方法粒度上去支持指定數(shù)據(jù)源的方案,那自然而然想到了可以通過注解來(lái)實(shí)現(xiàn),首先,自定義一個(gè)注解@DBKey:

          @Retention(RetentionPolicy.RUNTIME)
          @Target({ElementType.METHOD, ElementType.TYPE})
          public @interface DBKey {

              String DEFAULT = "default"; // 默認(rèn)數(shù)據(jù)庫(kù)節(jié)點(diǎn)

              String value() default DEFAULT;
          }

          思路和上面基于springboot原生的配置的類似,首先定義一個(gè)默認(rèn)的數(shù)據(jù)庫(kù)節(jié)點(diǎn),當(dāng)mapper接口方法/類沒有指定任何注解的時(shí)候,默認(rèn)走這個(gè)節(jié)點(diǎn),注解支持傳入value參數(shù)表示選擇的數(shù)據(jù)源節(jié)點(diǎn)名稱。至于注解的實(shí)現(xiàn)邏輯,可以通過反射來(lái)獲取mapper接口方法/類的注解值,然后指定特定的數(shù)據(jù)源。

          那在什么時(shí)候執(zhí)行這個(gè)操作獲取呢?可以考慮使用spring AOP織入mapper層,在切入點(diǎn)執(zhí)行具體mapper方法之前,將對(duì)應(yīng)的數(shù)據(jù)源配置放入threaLocal中,有了這個(gè)邏輯,立即動(dòng)手實(shí)現(xiàn):

          首先,定義一個(gè)db配置的上下文對(duì)象。維護(hù)所有的數(shù)據(jù)源key實(shí)例,以及當(dāng)前線程使用的數(shù)據(jù)源key:

          public class DBContextHolder {

              private static final ThreadLocal<String> DB_KEY_CONTEXT = new ThreadLocal<>();

              //在app啟動(dòng)時(shí)就加載全部數(shù)據(jù)源,不需要考慮并發(fā)
              private static Set<String> allDBKeys = new HashSet<>();

              public static String getDBKey() {
                  return DB_KEY_CONTEXT.get();
              }

              public static void setDBKey(String dbKey) {
                  //key必須在配置中
                  if (containKey(dbKey)) {
                      DB_KEY_CONTEXT.set(dbKey);
                  } else {
                      throw new KeyNotFoundException("datasource[" + dbKey + "] not found!");
                  }
              }

              public static void addDBKey(String dbKey) {
                  allDBKeys.add(dbKey);
              }

              public static boolean containKey(String dbKey) {
                  return allDBKeys.contains(dbKey);
              }

              public static void clear() {
                  DB_KEY_CONTEXT.remove();
              }
          }

          然后,定義切點(diǎn),在切點(diǎn)before方法中,根據(jù)當(dāng)前mapper接口的@@DBKey注解來(lái)選取對(duì)應(yīng)的數(shù)據(jù)源key:

          @Aspect
          @Order(Ordered.LOWEST_PRECEDENCE - 1)
          public class DSAdvice implements BeforeAdvice {

              @Pointcut("execution(* com.xxx..*.repository.*.*(..))")
              public void daoMethod() {
              }

              @Before("daoMethod()")
              public void beforeDao(JoinPoint point) {
                  try {
                      innerBefore(point, false);
                  } catch (Exception e) {
                      logger.error("DefaultDSAdviceException",
                              "Failed to set database key,please resolve it as soon as possible!", e);
                  }
              }

              /**
               * @param isClass 攔截類還是接口
               */
              public void innerBefore(JoinPoint point, boolean isClass) {
                  String methodName = point.getSignature().getName();

                  Class<?> clazz = getClass(point, isClass);
                  //使用默認(rèn)數(shù)據(jù)源
                  String dbKey = DBKey.DEFAULT;
                  Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
                  Method method = null;
                  try {
                      method = clazz.getMethod(methodName, parameterTypes);
                  } catch (NoSuchMethodException e) {
                      throw new RuntimeException("can't find " + methodName + " in " + clazz.toString());
                  }
                  //方法上存在注解,使用方法定義的datasource
                  if (method.isAnnotationPresent(DBKey.class)) {
                      DBKey key = method.getAnnotation(DBKey.class);
                      dbKey = key.value();
                  } else {
                      //方法上不存在注解,使用類上定義的注解
                      clazz = method.getDeclaringClass();
                      if (clazz.isAnnotationPresent(DBKey.class)) {
                          DBKey key = clazz.getAnnotation(DBKey.class);
                          dbKey = key.value();
                      }
                  }
                  DBContextHolder.setDBKey(dbKey);
              }


              private Class<?> getClass(JoinPoint point, boolean isClass) {
                  Object target = point.getTarget();
                  String methodName = point.getSignature().getName();

                  Class<?> clazz = target.getClass();
                  if (!isClass) {
                      Class<?>[] clazzList = target.getClass().getInterfaces();

                      if (clazzList == null || clazzList.length == 0) {
                          throw new MutiDBException("找不到mapper class,methodName =" + methodName);
                      }
                      clazz = clazzList[0];
                  }

                  return clazz;
              }
          }

          既然在執(zhí)行mapper之前,該mapper接口最終使用的數(shù)據(jù)源已經(jīng)被放入threadLocal中,那么,只需要重寫新的路由數(shù)據(jù)源接口邏輯即可:

          public class RoutingDatasource extends AbstractRoutingDataSource {

              @Override
              protected Object determineCurrentLookupKey() {
                  String dbKey = DBContextHolder.getDBKey();
                  return dbKey;
              }

              @Override
              public void setTargetDataSources(Map<Object, Object> targetDataSources) {
                  for (Object key : targetDataSources.keySet()) {
                      DBContextHolder.addDBKey(String.valueOf(key));
                  }
                  super.setTargetDataSources(targetDataSources);
                  super.afterPropertiesSet();
              }
          }

          另外,我們?cè)诜?wù)啟動(dòng),配置mybatis的時(shí)候,將所有的db配置加載:

          @Bean
              @ConditionalOnMissingBean(DataSource.class)
              @Autowired
              public DataSource dataSource(MybatisProperties mybatisProperties) {
                  Map<Object, Object> dsMap = new HashMap<>(mybatisProperties.getNodes().size());
                  for (String nodeName : mybatisProperties.getNodes().keySet()) {
                      dsMap.put(nodeName, buildDataSource(nodeName, mybatisProperties));
                      DBContextHolder.addDBKey(nodeName);
                  }
                  RoutingDatasource dataSource = new RoutingDatasource();
                  dataSource.setTargetDataSources(dsMap);
                  if (null == dsMap.get(DBKey.DEFAULT)) {
                      throw new RuntimeException(
                              String.format("Default DataSource [%s] not exists", DBKey.DEFAULT));
                  }
                  dataSource.setDefaultTargetDataSource(dsMap.get(DBKey.DEFAULT));
                  return dataSource;
              }



          @ConfigurationProperties(prefix = "mybatis")
          @Data
          public class MybatisProperties {

              private Map<String, String> params;

              private Map<String, Object> nodes;

              /**
               * mapper文件路徑:多個(gè)location以,分隔
               */
              private String mapperLocations = "classpath*:com/iqiyi/xiu/**/mapper/*.xml";

              /**
               * Mapper類所在的base package
               */
              private String basePackage = "com.iqiyi.xiu.**.repository";

              /**
               * mybatis配置文件路徑
               */
              private String configLocation = "classpath:mybatis-config.xml";
          }

          那threadLocal中的key什么時(shí)候進(jìn)行銷毀呢,其實(shí)可以自定義一個(gè)基于mybatis的攔截器,在攔截器中主動(dòng)調(diào)DBContextHolder.clear()方法銷毀這個(gè)key。具體代碼就不貼了。這樣一來(lái),我們就完成了一個(gè)基于注解的支持多數(shù)據(jù)源切換的中間件。

          那有沒有可以優(yōu)化的點(diǎn)呢?其實(shí),可以發(fā)現(xiàn),在獲取mapper接口/所在類的注解的時(shí)候,使用了反射來(lái)獲取的,那我們知道一般反射調(diào)用是比較耗性能的,所以可以考慮在這里加個(gè)本地緩存來(lái)優(yōu)化下性能:

          private final static Map<String, String> METHOD_CACHE = new ConcurrentHashMap<>();
          //....
          public void innerBefore(JoinPoint point, boolean isClass) {
                  String methodName = point.getSignature().getName();

                  Class<?> clazz = getClass(point, isClass);
                  //key為類名+方法名
                  String keyString = clazz.toString() + methodName;
                  //使用默認(rèn)數(shù)據(jù)源
                  String dbKey = DBKey.DEFAULT;
                  //如果緩存中已經(jīng)有這個(gè)mapper方法對(duì)應(yīng)的數(shù)據(jù)源的key,那直接設(shè)置
                  if (METHOD_CACHE.containsKey(keyString)) {
                      dbKey = METHOD_CACHE.get(keyString);
                  } else {
                      Class<?>[] parameterTypes =
                              ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
                      Method method = null;

                      try {
                          method = clazz.getMethod(methodName, parameterTypes);
                      } catch (NoSuchMethodException e) {
                          throw new RuntimeException("can't find " + methodName + " in " + clazz.toString());
                      }
                       //方法上存在注解,使用方法定義的datasource
                      if (method.isAnnotationPresent(DBKey.class)) {
                          DBKey key = method.getAnnotation(DBKey.class);
                          dbKey = key.value();
                      } else {
                          clazz = method.getDeclaringClass();
                          //使用類上定義的注解
                          if (clazz.isAnnotationPresent(DBKey.class)) {
                              DBKey key = clazz.getAnnotation(DBKey.class);
                              dbKey = key.value();
                          }
                      }
                     //先放本地緩存
                      METHOD_CACHE.put(keyString, dbKey);
                  }
                  DBContextHolder.setDBKey(dbKey);
              }

          這樣一來(lái),只有在第一次調(diào)用這個(gè)mapper接口的時(shí)候,才會(huì)走反射調(diào)用的邏輯去獲取對(duì)應(yīng)的數(shù)據(jù)源,后續(xù),都會(huì)走本地緩存,提升了性能。






          粉絲福利:Java從入門到入土學(xué)習(xí)路線圖

          ??????

          ??長(zhǎng)按上方微信二維碼 2 秒


          感謝點(diǎn)贊支持下哈 

          瀏覽 51
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久色国产精品 | 老妇裸体乱婬视频 | 豆花视频理论在线播放 | 97一区二区三区 | 国产一级乱伦 |