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。

构建

添加依赖

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353