SpringBoot+JPA多数据源(注解方式)

First

  • 项目中经常会遇到使用多个数据源的情况。
  • 这里是基于 JPA 来配置多个数据源。
  • 使用了 注解 + AOP 的方式实现。
  • 如果多个数据源的表结构大不相同,不推荐使用,会产生冗余空表。
  • 上面问题也可以通过分包扫描实现
  • 上上面问题也可以通过 EntityManager(doc) 实现(操作类似于JpaRepository,并且没有实体映射数据库的操作)。
  • 上上上面那问题,在分包扫描方式中,对于不同数据库的相同表用注解实现跨数据库访问很方便。
  • 基于 MySql 8.x
  • alibaba Druid pool

优点

  • 注解+AOP 简化切换工作
  • 配置多数据源简单

缺点

  • 不能简单的跟据参数动态切换数据源,也就是说,启动打那一刻,该方法执行连接的数据源就确定了。
  • 如果其它数据源的表在主数据源中没有,则会自动在主数据源中添加。需要另外添加解决方案(扫描包的方式配置数据源)。这是JPA在初始化 Table Bean 的时候,必须要映射到对应数据库中的 Table。

构建

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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插件,需要自己添加 其它需要自己添加了-->

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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:
#不加此配置,获取不到当前currentsession
hibernate:
current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
dialect: org.hibernate.dialect.MySQL5Dialect
# 多数据源配置
gt:
maxzhao:
boot:
#主动开启多数据源
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

添加注解类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package gt.maxzhao.boot.common.annotation;

import java.lang.annotation.*;

/**
* <p>多数据源标识</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";
}

数据源配置映射 yml配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
* <p>多数据源配置</p>
* <p>数据库数据源配置</p>
* <p>说明:这个类中包含了许多默认配置,建议不要修改本类,直接在"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 数据源加载");
}

/**
* 数据源名称
*/
private String dbName = "main";

private String url;

private String username;

private String password;
/**
* 默认为 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); // 定义初始连接数
dataSource.setMinIdle(minIdle); // 最小空闲
dataSource.setMaxActive(maxActive); // 定义最大连接数
dataSource.setMaxWait(maxWait); // 获取连接等待超时的时间
dataSource.setRemoveAbandoned(removeAbandoned); // 超过时间限制是否回收
dataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout); // 超过时间限制多长

// 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
// 配置一个连接在池中最小生存的时间,单位是毫秒
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
// 用来检测连接是否有效的sql,要求是一个查询语句
dataSource.setValidationQuery(validationQuery);
// 申请连接的时候检测
dataSource.setTestWhileIdle(testWhileIdle);
// 申请连接时执行validationQuery检测连接是否有效,配置为true会降低性能
dataSource.setTestOnBorrow(testOnBorrow);
// 归还连接时执行validationQuery检测连接是否有效,配置为true会降低性能
dataSource.setTestOnReturn(testOnReturn);
// 打开PSCache,并且指定每个连接上PSCache的大小
dataSource.setPoolPreparedStatements(poolPreparedStatements);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
// 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
// 监控统计用的filter:stat
// 日志用的filter:log4j
// 防御SQL注入的filter:wall
try {
dataSource.setFilters(filters);
} catch (SQLException e) {
log.error("扩展插件失败.{}", e.getMessage());
}
return dataSource;
}

}

多数据源配置映射 yml配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* <p>多数据源配置</p>
* <p>多个数据源</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("加载多数据源配置信息 --> {}", "gt.maxzhao.boot.datasource");
}
/**
* 多个数据源
*/
private List<DruidProperties> datasource;
}

多数据源配置类

这里需要配置动态开启多数据源,如果不主动开启,配置了注解也不会生效。

这里也做了一个不必要的处理,如果多数据源中有处理失败或名称填写错误,默认使用主数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* <p>多数据源配置</p>
* <p>多数据源配置</p>
*
* @author maxzhao
* @date 2019-06-26 16:07
*/
@Slf4j
@Component
public class MultiSourceConfig {
@Autowired
private DruidProperties druidProperties;

@Autowired
private MultiDataSource multiDataSource;


/**
* 单数据源连接池配置
*/
@Bean
@ConditionalOnProperty(name = "gt.maxzhao.boot.multiDatasourceOpen", havingValue = "false")
public DruidDataSource singleDatasource() {
log.error("singleDatasource");
return druidProperties.config(new DruidDataSource());
}

/**
* 多数据源连接池配置
*/
@Bean
@ConditionalOnProperty(name = "gt.maxzhao.boot.multiDatasourceOpen", havingValue = "true")
public DynamicDataSource mutiDataSource() {
log.error("mutiDataSource");

//存储数据源别名与数据源的映射
HashMap<Object, Object> dbNameMap = new HashMap<>();
// 核心数据源
DruidDataSource mainDataSource = druidProperties.config();
// 这里添加 主要数据库,其它数据库挂了,默认使用主数据库
dbNameMap.put("main", mainDataSource);
// 其它数据源
// 当前多数据源是否存在
if (multiDataSource.getDatasource() != null) {
//过滤掉没有添加 dbName 的数据源,先加载娟全局配置,再次加载当前配置
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());

// 测试所有的数据源
try {
mainDataSource.init();
for (DruidDataSource druidDataSource : multiDataSourceList) {
druidDataSource.init();
}
} catch (SQLException sql) {
log.error("======================= 多数据源配置错误 ==========================");
sql.printStackTrace();
}
}
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(dbNameMap);
dynamicDataSource.setDefaultTargetDataSource(mainDataSource);
return dynamicDataSource;
}

}

DataSource 的 router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* <p>多数据源配置</p>
* <p>动态数据源</p>
* <p>多 datasource 的上下文</p>
*
* @author xiongneng
* @since 2017年3月5日 上午9:11:49
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* <p>多 datasource 的上下文</p>
* <p>每个线程独立的数据库连接名称</p>
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

/**
* @param dataSourceDbName 数据库别名
* @Description: 设置数据源别名
*/
public static void setDataSourceDbName(String dataSourceDbName) {
contextHolder.set(dataSourceDbName);
}

/**
* @Description: 获取数据源别名
*/
public static String getDataSourceDbName() {
return contextHolder.get();
}

/**
* @Description: 清除数据源别名
*/
public static void clearDataSourceDbName() {
contextHolder.remove();
}

/**
* 重写获取连接名称的方法
* @return 连接名称
*/
@Override
protected Object determineCurrentLookupKey() {
return getDataSourceDbName();
}

}

AOP配置

切点是自定义注解的包路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* <p>多数据源切换的 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("多数据源初始化 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;
//获取当点方法的注解
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("设置数据源为:" + datasource.name());
} else {
DynamicDataSource.setDataSourceDbName("main");
log.debug("设置数据源为:默认 --> main");
}
try {
return point.proceed();
} finally {
log.debug("清空数据源信息!");
DynamicDataSource.clearDataSourceDbName();
}
}

/**
* aop的顺序要早于spring的事务
*/
@Override
public int getOrder() {
return 1;
}
}

到这里构建结束

测试

model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 = "地区名称")
private String name;
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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

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

Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringRunner.class )
// 这里的 BasicApplication 是当前SpringBoot的启动类
@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");
}
}

EntityManager(doc) 方式

引入:

1
2
@Autowired
private EntityManager entityManager

使用:

后续更新。

本文地址: SpringBoot+JPA多数据源(注解方式)

gitee

推荐
IDEA好用的插件
JAVA自定义注解

本文地址: https://github.com/maxzhao-it/blog/post/40131/