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");
}
}