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

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。

构建

添加依赖


   org.springframework.boot
   spring-boot-starter-data-jpa


    mysql
    mysql-connector-java
    runtime


   com.alibaba
   druid-spring-boot-starter
   1.1.16


配置文件

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.*;

/**
 * 

多数据源标识

*

使用方式:必须用在方法上

* * @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;

/**
 * 

多数据源配置

*

数据库数据源配置

*

说明:这个类中包含了许多默认配置,建议不要修改本类,直接在"application.yml"中配置即可

* * @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;

/**
 * 

多数据源配置

*

多个数据源

* * @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 datasource; }

多数据源配置类

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

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

/**
 * 

多数据源配置

*

多数据源配置

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

/**
 * 

多数据源配置

*

动态数据源

*

多 datasource 的上下文

* * @author xiongneng * @since 2017年3月5日 上午9:11:49 */ public class DynamicDataSource extends AbstractRoutingDataSource { /** *

多 datasource 的上下文

*

每个线程独立的数据库连接名称

*/ private static final ThreadLocal contextHolder = new ThreadLocal(); /** * @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配置

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

/**
 * 

多数据源切换的 aop

* * @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 findAll() {
        return tempRepository.findAll();
    }

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

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

dao

@Repository("tempRepository")
public interface TempRepository extends JpaRepository {
}

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多数据源(注解方式)

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

你可能感兴趣的:(SpringBoot+JPA多数据源(注解方式))