title: SpringBoot+JPA多数据源(注解方式)
date: 2019-06-27
author: maxzhao
tags:
- JAVA
- 多数据源
- SpringBoot
- 自定义注解
- AOP
- MYSQL8
categories:
- SpringBoot
- JPA
- JAVA
First
- 项目中经常会遇到使用多个数据源的情况。
- 这里是基于 JPA 来配置多个数据源。
- 使用了 注解 + AOP 的方式实现。
- 如果多个数据源的表结构大不相同,不推荐使用,会产生冗余空表。
- 上面问题也可以通过分包扫描实现
- 基于 MySql 8.x
- alibaba Druid pool
优点
- 注解+AOP 简化切换工作
- 配置多数据源简单
缺点
- 不能简单的跟据参数动态切换数据源,也就是说,启动打那一刻,该方法执行连接的数据源就确定了。
- 如果其它数据源的表在主数据源中没有,则会自动在主数据源中添加。需要另外添加解决方案(扫描包的方式配置数据源)。这是JPA在初始化 Table Bean 的时候,必须要映射到对应数据库中的 Table。
构建
添加依赖
<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:
#不加此配置,获取不到当前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
添加注解类
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
配置类
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>多数据源配置</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
配置类
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>多数据源配置</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;
}
多数据源配置类
这里需要配置动态开启多数据源,如果不主动开启,配置了注解也不会生效。
这里也做了一个不必要的处理,如果多数据源中有处理失败或名称填写错误,默认使用主数据源。
/**
* <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
/**
* <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配置
切点是自定义注解的包路径
/**
* <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
@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
@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 是当前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");
}
}
本文地址:
SpringBoot+JPA多数据源(注解方式)