SpringBoot+JPA多數(shù)據(jù)源(注解方式)

title: SpringBoot+JPA多數(shù)據(jù)源(注解方式)
date: 2019-06-27
author: maxzhao
tags:
  - JAVA
  - 多數(shù)據(jù)源
  - SpringBoot
  - 自定義注解
  - AOP
  - MYSQL8
categories:
  - SpringBoot
  - JPA
  - JAVA

First

  • 項(xiàng)目中經(jīng)常會(huì)遇到使用多個(gè)數(shù)據(jù)源的情況。
  • 這里是基于 JPA 來(lái)配置多個(gè)數(shù)據(jù)源。
  • 使用了 注解 + AOP 的方式實(shí)現(xiàn)。
  • 如果多個(gè)數(shù)據(jù)源的表結(jié)構(gòu)大不相同,不推薦使用,會(huì)產(chǎn)生冗余空表。
  • 上面問(wèn)題也可以通過(guò)分包掃描實(shí)現(xiàn)
  • 基于 MySql 8.x
  • alibaba Druid pool

優(yōu)點(diǎn)

  • 注解+AOP 簡(jiǎn)化切換工作
  • 配置多數(shù)據(jù)源簡(jiǎn)單

缺點(diǎn)

  • 不能簡(jiǎn)單的跟據(jù)參數(shù)動(dòng)態(tài)切換數(shù)據(jù)源,也就是說(shuō),啟動(dòng)打那一刻,該方法執(zhí)行連接的數(shù)據(jù)源就確定了。
  • 如果其它數(shù)據(jù)源的表在主數(shù)據(jù)源中沒(méi)有,則會(huì)自動(dòng)在主數(shù)據(jù)源中添加。需要另外添加解決方案(掃描包的方式配置數(shù)據(jù)源)。這是JPA在初始化 Table Bean 的時(shí)候,必須要映射到對(duì)應(yīng)數(shù)據(jù)庫(kù)中的 Table。

構(gòu)建

添加依賴(lài)

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.16</version>
</dependency>
<!--使用啦Lombok插件,需要自己添加 其它需要自己添加了-->

配置文件

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/maxzhao_ittest?charset=utf8mb4&useSSL=false
    username: maxzhao
    password: maxzhao
  main:
    allow-bean-definition-overriding: true

  jpa:
    database: MYSQL
    database-plinatform: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true
    generate-ddl: true
    open-in-view: false

    hibernate:
      ddl-auto: update
    #       naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
    properties:
      #不加此配置,獲取不到當(dāng)前currentsession
      hibernate:
        current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
        dialect: org.hibernate.dialect.MySQL5Dialect
# 多數(shù)據(jù)源配置
gt:
  maxzhao:
    boot:
    #主動(dòng)開(kāi)啟多數(shù)據(jù)源
      multiDatasourceOpen: true
      datasource[0]:
        dbName: second
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/pos?charset=utf8mb4&useSSL=false
        username: maxzhao
        password: maxzhao
      datasource[1]:
        dbName: third
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/biz?charset=utf8mb4&useSSL=false
        username: maxzhao
        password: maxzhao

添加注解類(lèi)

package gt.maxzhao.boot.common.annotation;

import java.lang.annotation.*;

/**
 * <p>多數(shù)據(jù)源標(biāo)識(shí)</p>
 * <p>使用方式:必須用在方法上</p>
 *
 * @author maxzhao
 * @date 2019-06-26 16:13
 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MultiDataSource {
    String name() default "main";
}

數(shù)據(jù)源配置映射 yml配置類(lèi)

package gt.maxzhao.boot.config.source.model;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.util.JdbcConstants;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.sql.SQLException;

/**
 * <p>多數(shù)據(jù)源配置</p>
 * <p>數(shù)據(jù)庫(kù)數(shù)據(jù)源配置</p>
 * <p>說(shuō)明:這個(gè)類(lèi)中包含了許多默認(rèn)配置,建議不要修改本類(lèi),直接在"application.yml"中配置即可</p>
 *
 * @author maxzhao
 * @date 2019-06-26 16:13
 */
@Component
@ConfigurationProperties(prefix = "spring.datasource")
@Setter
@Getter
@Slf4j
public class DruidProperties {
    public DruidProperties() {
        log.info("default 數(shù)據(jù)源加載");
    }

    /**
     * 數(shù)據(jù)源名稱(chēng)
     */
    private String dbName = "main";

    private String url;

    private String username;

    private String password;
    /**
     * 默認(rèn)為 MYSQL 8.x 配置
     */
    private String driverClassName = "com.mysql.cj.jdbc.Driver";

    private Integer initialSize = 10;

    private Integer minIdle = 3;

    private Integer maxActive = 60;

    private Integer maxWait = 60000;

    private Boolean removeAbandoned = true;

    private Integer removeAbandonedTimeout = 180;

    private Integer timeBetweenEvictionRunsMillis = 60000;

    private Integer minEvictableIdleTimeMillis = 300000;

    private String validationQuery = "SELECT 'x'";

    private Boolean testWhileIdle = true;

    private Boolean testOnBorrow = false;

    private Boolean testOnReturn = false;

    private Boolean poolPreparedStatements = true;

    private Integer maxPoolPreparedStatementPerConnectionSize = 50;

    private String filters = "stat";

    public DruidDataSource config() {
        DruidDataSource dataSource = new DruidDataSource();
        return config(dataSource);
    }

    public DruidDataSource config(DruidDataSource dataSource) {
        dataSource.setDbType(JdbcConstants.MYSQL);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setDriverClassName(driverClassName);
        dataSource.setInitialSize(initialSize);     // 定義初始連接數(shù)
        dataSource.setMinIdle(minIdle);             // 最小空閑
        dataSource.setMaxActive(maxActive);         // 定義最大連接數(shù)
        dataSource.setMaxWait(maxWait);             // 獲取連接等待超時(shí)的時(shí)間
        dataSource.setRemoveAbandoned(removeAbandoned); // 超過(guò)時(shí)間限制是否回收
        dataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout); // 超過(guò)時(shí)間限制多長(zhǎng)

        // 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒
        dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        // 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒
        dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        // 用來(lái)檢測(cè)連接是否有效的sql,要求是一個(gè)查詢(xún)語(yǔ)句
        dataSource.setValidationQuery(validationQuery);
        // 申請(qǐng)連接的時(shí)候檢測(cè)
        dataSource.setTestWhileIdle(testWhileIdle);
        // 申請(qǐng)連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效,配置為true會(huì)降低性能
        dataSource.setTestOnBorrow(testOnBorrow);
        // 歸還連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效,配置為true會(huì)降低性能
        dataSource.setTestOnReturn(testOnReturn);
        // 打開(kāi)PSCache,并且指定每個(gè)連接上PSCache的大小
        dataSource.setPoolPreparedStatements(poolPreparedStatements);
        dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        // 屬性類(lèi)型是字符串,通過(guò)別名的方式配置擴(kuò)展插件,常用的插件有:
        // 監(jiān)控統(tǒng)計(jì)用的filter:stat
        // 日志用的filter:log4j
        // 防御SQL注入的filter:wall
        try {
            dataSource.setFilters(filters);
        } catch (SQLException e) {
            log.error("擴(kuò)展插件失敗.{}", e.getMessage());
        }
        return dataSource;
    }

}

多數(shù)據(jù)源配置映射 yml配置類(lèi)

package gt.maxzhao.boot.config.source;

import gt.maxzhao.boot.config.source.model.DruidProperties;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * <p>多數(shù)據(jù)源配置</p>
 * <p>多個(gè)數(shù)據(jù)源</p>
 *
 * @author maxzhao
 * @date 2019-06-26 16:22
 */
@Configuration
@ConfigurationProperties(prefix = "gt.maxzhao.boot")
@Getter
@Setter
@Slf4j
public class MultiDataSource {
    public MultiDataSource() {
        log.info("加載多數(shù)據(jù)源配置信息  -->  {}", "gt.maxzhao.boot.datasource");
    }
    /**
     * 多個(gè)數(shù)據(jù)源
     */
    private List<DruidProperties> datasource;
}

多數(shù)據(jù)源配置類(lèi)

這里需要配置動(dòng)態(tài)開(kāi)啟多數(shù)據(jù)源,如果不主動(dòng)開(kāi)啟,配置了注解也不會(huì)生效。

這里也做了一個(gè)不必要的處理,如果多數(shù)據(jù)源中有處理失敗或名稱(chēng)填寫(xiě)錯(cuò)誤,默認(rèn)使用主數(shù)據(jù)源。

/**
 * <p>多數(shù)據(jù)源配置</p>
 * <p>多數(shù)據(jù)源配置</p>
 *
 * @author maxzhao
 * @date 2019-06-26 16:07
 */
@Slf4j
@Component
public class MultiSourceConfig {
    @Autowired
    private DruidProperties druidProperties;

    @Autowired
    private MultiDataSource multiDataSource;


    /**
     * 單數(shù)據(jù)源連接池配置
     */
    @Bean
    @ConditionalOnProperty(name = "gt.maxzhao.boot.multiDatasourceOpen", havingValue = "false")
    public DruidDataSource singleDatasource() {
        log.error("singleDatasource");
        return druidProperties.config(new DruidDataSource());
    }

    /**
     * 多數(shù)據(jù)源連接池配置
     */
    @Bean
    @ConditionalOnProperty(name = "gt.maxzhao.boot.multiDatasourceOpen", havingValue = "true")
    public DynamicDataSource mutiDataSource() {
        log.error("mutiDataSource");

        //存儲(chǔ)數(shù)據(jù)源別名與數(shù)據(jù)源的映射
        HashMap<Object, Object> dbNameMap = new HashMap<>();
        // 核心數(shù)據(jù)源
        DruidDataSource mainDataSource = druidProperties.config();
        // 這里添加 主要數(shù)據(jù)庫(kù),其它數(shù)據(jù)庫(kù)掛了,默認(rèn)使用主數(shù)據(jù)庫(kù)
        dbNameMap.put("main", mainDataSource);
        // 其它數(shù)據(jù)源
        // 當(dāng)前多數(shù)據(jù)源是否存在
        if (multiDataSource.getDatasource() != null) {
            //過(guò)濾掉沒(méi)有添加 dbName 的數(shù)據(jù)源,先加載娟全局配置,再次加載當(dāng)前配置
            List<DruidDataSource> multiDataSourceList = multiDataSource.getDatasource().stream()
                    .filter(dp -> !"".equals(Optional.ofNullable(dp.getDbName()).orElse("")))
                    .map(dp -> {
                        DruidDataSource druidDataSource = dp.config(druidProperties.config());
                        dbNameMap.put(dp.getDbName(), druidDataSource);
                        return druidDataSource;
                    })
                    .collect(Collectors.toList());

            // 測(cè)試所有的數(shù)據(jù)源
            try {
                mainDataSource.init();
                for (DruidDataSource druidDataSource : multiDataSourceList) {
                    druidDataSource.init();
                }
            } catch (SQLException sql) {
                log.error("=======================    多數(shù)據(jù)源配置錯(cuò)誤   ==========================");
                sql.printStackTrace();
            }
        }
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(dbNameMap);
        dynamicDataSource.setDefaultTargetDataSource(mainDataSource);
        return dynamicDataSource;
    }

}

DataSource 的 router

/**
 * <p>多數(shù)據(jù)源配置</p>
 * <p>動(dòng)態(tài)數(shù)據(jù)源</p>
 * <p>多 datasource 的上下文</p>
 *
 * @author xiongneng
 * @since 2017年3月5日 上午9:11:49
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * <p>多 datasource 的上下文</p>
     * <p>每個(gè)線程獨(dú)立的數(shù)據(jù)庫(kù)連接名稱(chēng)</p>
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    /**
     * @param dataSourceDbName 數(shù)據(jù)庫(kù)別名
     * @Description: 設(shè)置數(shù)據(jù)源別名
     */
    public static void setDataSourceDbName(String dataSourceDbName) {
        contextHolder.set(dataSourceDbName);
    }

    /**
     * @Description: 獲取數(shù)據(jù)源別名
     */
    public static String getDataSourceDbName() {
        return contextHolder.get();
    }

    /**
     * @Description: 清除數(shù)據(jù)源別名
     */
    public static void clearDataSourceDbName() {
        contextHolder.remove();
    }

    /**
     * 重寫(xiě)獲取連接名稱(chēng)的方法
     * @return 連接名稱(chēng)
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSourceDbName();
    }

}

AOP配置

切點(diǎn)是自定義注解的包路徑

/**
 * <p>多數(shù)據(jù)源切換的 aop</p>
 *
 * @author maxzhao
 * @date 2019-06-26 16:22
 */
@Aspect
@Component
@ConditionalOnProperty(prefix = "gt.maxzhao.boot", name = "multiDatasourceOpen", havingValue = "true")
public class MultiDataSourceAop implements Ordered {
    private Logger log = LoggerFactory.getLogger(this.getClass());

    public MultiDataSourceAop() {
        log.info("多數(shù)據(jù)源初始化 AOP ");
    }

    @Pointcut(value = "@annotation(gt.maxzhao.boot.common.annotation.MultiDataSource)")
    private void cut() {
    }

    @Around("cut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {

        Signature signature = point.getSignature();
        MethodSignature methodSignature ;
        if (!(signature instanceof MethodSignature)) {
            throw new IllegalArgumentException("該注解只能用于方法");
        }
        methodSignature = (MethodSignature) signature;
        //獲取當(dāng)點(diǎn)方法的注解
        Object target = point.getTarget();
        Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());

        MultiDataSource datasource = currentMethod.getAnnotation(MultiDataSource.class);
        if (datasource != null) {
            DynamicDataSource.setDataSourceDbName(datasource.name());
            log.debug("設(shè)置數(shù)據(jù)源為:" + datasource.name());
        } else {
            DynamicDataSource.setDataSourceDbName("main");
            log.debug("設(shè)置數(shù)據(jù)源為:默認(rèn)  -->  main");
        }
        try {
            return point.proceed();
        } finally {
            log.debug("清空數(shù)據(jù)源信息!");
            DynamicDataSource.clearDataSourceDbName();
        }
    }

    /**
     * aop的順序要早于spring的事務(wù)
     */
    @Override
    public int getOrder() {
        return 1;
    }
}

到這里構(gòu)建結(jié)束

測(cè)試

model

@Accessors(chain = true)
@Data
@Entity
@Table(name = "temp", schema = "", catalog = "")
public class Temp implements Serializable {
    private static final long serialVersionUID = -1L;

    @Id
    @Column(name = "ID",unique = true)
    @ApiModelProperty(value = "主鍵")
    private Long id;
    @Basic
    @Column(name = "NAME")
    @ApiModelProperty(value = "地區(qū)名稱(chēng)")
    private String name;
}

service

@Service
@Transactional
public class TempServiceDemo {

    @Autowired
    private TempRepository tempRepository;


    public List<Temp> findAll() {
        return tempRepository.findAll();
    }

    @MultiDataSource(name = "second")
    public List<Temp> findAllSecond() {
        return tempRepository.findAll();
    }

    @MultiDataSource(name = "third")
    public List<Temp> findAllThird() {
        return tempRepository.findAll();
    }
}

dao

@Repository("tempRepository")
public interface TempRepository extends JpaRepository<Temp, Long> {
}

Test

@RunWith(SpringRunner.class )
// 這里的 BasicApplication 是當(dāng)前SpringBoot的啟動(dòng)類(lèi)
@SpringBootTest(classes = BasicApplication.class)
@Slf4j
public class MultiDataSourceTest {
    @Resource
    private TempServiceDemo tempServiceDemo;

    @Autowired
    private MultiDataSource multiDataSource;

    @Test
    public void testMultiDataSource() {
        System.out.println("\r\n=================\r\n");
        System.out.println(tempServiceDemo.findAllSecond());
        System.out.println("\r\n=================\r\n");
        System.out.println( tempServiceDemo.findAllThird());
        System.out.println("\r\n=================\r\n");
    }
}

本文地址:
SpringBoot+JPA多數(shù)據(jù)源(注解方式)

推薦
IDEA好用的插件
JAVA自定義注解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容