SpringBoot利用自定义注解实现多数据源

Posted atwood-pan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot利用自定义注解实现多数据源相关的知识,希望对你有一定的参考价值。

自定义多数据源

SpringBoot利用自定义注解实现多数据源,前置知识:注解、Aop、SpringBoot整合Mybaits

1、搭建工程

创建一个SpringBoot工程,并引入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--    解析多数据源注解    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.0</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.18</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

2、定义多数据源注解

/**
 * 1、定义多数据源注解
 * @author ss_419
 * TODO 这个注解将来可以加在service类上或者方法上,通过value属性来指定类或者方法应该使用那个数据源
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE, ElementType.METHOD)
public @interface DataSource 
    String value() default DataSourceType.DEFAULT_DS_NAME;

3、创建一个多数据上下文对象

这个类用来存储当前线程所使用的数据源名称


/**
 * TODO 这个类用来存储当前线程所使用的数据源名称
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/5/21 09:21
 */
public class DynamicDataSourceContextHolder 
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    public static void setDataSourceType(String dataSourceType) 
        CONTEXT_HOLDER.set(dataSourceType);
    

    public static String getDataSourceType() 
        return CONTEXT_HOLDER.get();
    
    public static void clearDataSourceType() 
        CONTEXT_HOLDER.remove();
    



4、配置aop

  • @annotation(org.pp.dd.annotation.DataSource) 如果有@DataSource注解就给拦截下来

  • @within(org.pp.dd.annotation.DataSource) 表示类上有@DataSource注解就将类中的方法给拦截下来

/**
 * TODO
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/5/21 09:42
 */
@Component
@Aspect
@Order(11)
public class DataSourceAspect 

    /**
     * 定义切点
     *
     * @annotation(org.pp.dd.annotation.DataSource) 如果有@DataSource注解就给拦截下来
     * @within(org.pp.dd.annotation.DataSource) 表示类上有@DataSource注解就将类中的方法给拦截下来
     */
    @Pointcut("@annotation(org.pp.dd.annotation.DataSource) || @within(org.pp.dd.annotation.DataSource)")
    public void pc() 

    
    /**
     * 环绕通知
     *
     * @param pjp
     * @return
     */
    @Around("pc()")
    public Object around(ProceedingJoinPoint pjp) 
        // 获取方法上的有效注解
        DataSource dataSource = getDataSource(pjp);
        if (dataSource != null) 
            // 获取注解中数据源的名称
            String value = dataSource.value();
            DynamicDataSourceContextHolder.setDataSourceType(value);
        
        try 
            return pjp.proceed();
         catch (Throwable e) 
            throw new RuntimeException(e);
         finally 
            DynamicDataSourceContextHolder.clearDataSourceType();
        

    

    private DataSource getDataSource(ProceedingJoinPoint pjp) 
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        // 获取方法上的注解
        DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);

        if (annotation != null) 
            // 说明方法上有注解
            return annotation;
        


        return (DataSource) AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    

5、读取参数DruidProperties


/**
 * TODO 读取数据源
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/5/21 10:20
 */
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties 
    private String type;
    private String driverClassName;
    private Map<String, Map<String ,String>> ds;
    private Integer initialSize;
    private Integer minIdle;
    private Integer maxActive;
    private Integer maxWait;

    /**
     * 在这个方法中设置公共属性
     * @param dataSource
     * @return
     */
    public DataSource dataSource(DruidDataSource dataSource)
        dataSource.setInitialSize(initialSize);
        dataSource.setMinIdle(minIdle);
        dataSource.setMaxActive(maxActive);
        dataSource.setMaxWait(maxWait);
        return dataSource;
    

    public String getType() 
        return type;
    

    public void setType(String type) 
        this.type = type;
    

    public String getDriverClassName() 
        return driverClassName;
    

    public void setDriverClassName(String driverClassName) 
        this.driverClassName = driverClassName;
    

    public Map<String, Map<String, String>> getDs() 
        return ds;
    

    public void setDs(Map<String, Map<String, String>> ds) 
        this.ds = ds;
    

    public Integer getInitialSize() 
        return initialSize;
    

    public void setInitialSize(Integer initialSize) 
        this.initialSize = initialSize;
    

    public Integer getMinIdle() 
        return minIdle;
    

    public void setMinIdle(Integer minIdle) 
        this.minIdle = minIdle;
    

    public Integer getMaxActive() 
        return maxActive;
    

    public void setMaxActive(Integer maxActive) 
        this.maxActive = maxActive;
    

    public Integer getMaxWait() 
        return maxWait;
    

    public void setMaxWait(Integer maxWait) 
        this.maxWait = maxWait;
    

6、加载数据源LoadDataSource

/**
 * TODO 加载数据源
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/5/21 10:30
 */
@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource 

    @Autowired
    DruidProperties druidProperties;

    public Map<String, DataSource> loadAllDataSource() 
        Map<String, DataSource> map = new HashMap<>();
        Map<String, Map<String, String>> ds = druidProperties.getDs();

        try 
            Set<String> keySet = ds.keySet();
            for (String key : keySet) 
                map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
            
         catch (Exception e) 
            throw new RuntimeException(e);
        

        return map;
    

7、定义数据源管理器

当系统需要调用数据源的时候,数据源以key-value存起来,当需要数据源时调用determineCurrentLookupKey()方法来获取数据源。
由于本人实力原因,解答不了大家这里的疑惑。大致功能 通过修改本地线程的值,来实现数据源的切换。

/**
 * TODO 设置数据源
 * 当系统需要调用数据源的时候,数据源以key-value存起来,当需要数据源时调用determineCurrentLookupKey()方法
 * @author ss_419
 * @version 1.0
 * @date 2023/5/21 10:47
 */
@Component
public class DynamicDataSource extends AbstractRoutingDataSource 


    public DynamicDataSource(LoadDataSource loadDataSource) 
        //1、设置所有的数据源
        Map<String, DataSource> allDs = loadDataSource.loadAllDataSource();
        super.setTargetDataSources(new HashMap<>(allDs));
        //2、设置默认数据源
        super.setDefaultTargetDataSource(allDs.get(DataSourceType.DEFAULT_DS_NAME));

        super.afterPropertiesSet();
    

    /**
     * 这个方法用来返回数据源名称,当系统需要获取数据源的时候会自动调用该方法获取数据源名称
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() 
        return DynamicDataSourceContextHolder.getDataSourceType();
    

定一个用于存储数据库类型的接口,这个接口类似于枚举类:

/**
 * TODO
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/5/21 10:54
 */
public interface DataSourceType 
    String DEFAULT_DS_NAME = "master";
    String DS_SESSION_KEY = "ds_session_key";

8、测试

创建User实体:

/**
 * TODO
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/5/21 11:15
 */
public class User 
    private Integer id;
    private String username;
    private String password;

    @Override
    public String toString() 
        return "User" +
                "id=" + id +
                ", username=\'" + username + \'\\\'\' +
                ", password=\'" + password + \'\\\'\' +
                \'\';
    

    public Integer getId() 
        return id;
    

    public void setId(Integer id) 
        this.id = id;
    

    public String getUsername() 
        return username;
    

    public void setUsername(String username) 
        this.username = username;
    

    public String getPassword() 
        return password;
    

    public void setPassword(String password) 
        this.password = password;
    


创建UserService:

@Service
// 在类上加注解的效果,会使该类的所有方法都切入到新的数据源中
//@DataSource
public class UserService 

    @Autowired
    UserMapper userMapper;
    // 在方法上加注解的效果,只会让指定的方法切入到另一个数据源中
    //@DataSource("slave")
    public List<User> findUsers()
        return userMapper.findAllUsers();
    

创建UserMapper:

@Mapper
public interface UserMapper 


    @Select("SELECT * FROM user")
    List<User> findAllUsers();

测试类:

@SpringBootTest
class DynamicDatasourcesApplicationTests 

    @Autowired
    UserService userService;

    @Test
    void contextLoads() 

        List<User> users = userService.findUsers();
        users.stream()
                .forEach(user -> System.out.println(user));
    


默认选择主库的数据源:

执行结果如下:

在Service上加上注解,指定数据源为从库:

执行结果如下:

实现SpringBoot项目的多数据源配置的两种方式(dynamic-datasource-spring-boot-starter和自定义注解的方式)

您好,我是码农飞哥,感谢您阅读本文,欢迎一键三连哦
💪🏻 1. Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。 Python从入门到精通
❤️ 2. Python爬虫专栏,系统性的学习爬虫的知识点。9.9元买不了吃亏,买不了上当 。python爬虫入门进阶
❤️ 3. Ceph实战,从原理到实战应有尽有。 Ceph实战
❤️ 4. Java高并发编程入门,打卡学习Java高并发。 Java高并发编程入门
😁 5. 社区逛一逛,周周有福利,周周有惊喜。码农飞哥社区,飞跃计划
全网同名【码农飞哥】欢迎关注,个人VX: wei158556

文章目录

1. 简介

最近项目需要配置多数据源,本项目采用的技术是SpringBoot+mybatis-plus+Druid。为了图个方便直接想直接集成dynamic-datasource-spring-boot-starter进行多数据源配置。

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。
其支持 Jdk 1.7+, SpringBoot 1.4.x 1.5.x 2.x.x。

其官方文档的地址是:https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611

该官方文档分为免费部分和付费部分。付费部分也仅仅只需要29块,29块也不多,就算原作者的支持,个人觉得这29块花的值。
强烈建议使用最新版本,可以在版本记录里查找最新版本

前提

这里默认你已经集成并配置好了mybatis-plus。

集成(第一种实现方式)

仅仅只看基础部分的集成手册是远远不够的。网上好多博客也仅仅只是介绍了基础部分的内容,最终还是达不到想要的效果。本文的集成亲测有效,欢迎读者老爷们阅读。
这里再次强烈建议采用最新版本的dynamic-datasource-spring-boot-starter,具体的版本记录请点击

1. 添加依赖

   <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

2. 添加数据源配置

在application.yml文件中将单数据源配置成多数据源,数据源配置的语法结构如下所示:

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx) # 内置加密,使用请查看详细文档
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver
       #......省略
       #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2

此处我的配置实例是:

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master  :
          url: jdbc:mysql://127.0.0.1:23306/db1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave:
          url: jdbc:mysql://127.0.0.1:23306/db2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver

3. 使用 @DS 切换数据源。

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。

注解结果
没有@DS使用默认数据源
@DS(“dsName”)dsName可以为组名也可以为具体某个库的名称
官方文档里配置到这里就结束了,实际上还远远不够。

4. 排除掉DruidDataSourceAutoConfigure

在启动类中需要排除掉DruidDataSourceAutoConfigure.class,就是取消Druid的数据源的自动配置类。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class)
@MapperScan(basePackages = "com.jay.multidatasource.mapper")
public class MultidatasourceApplication 
    public static void main(String[] args) 
        SpringApplication.run(MultidatasourceApplication.class, args);
    

原理解释(第二种实现方式)

多数据源的配置本质上就是加载多个数据源,并设置默认数据源,给每个数据源设置不同的键值对,当需要切换数据源时就传入目标数据源的键,然后重新设置数据源。下面就做一个简单的演示,就是不使用dynamic-datasource-spring-boot-starter。

1. 定义数据源配置

在application.yml文件中将单数据源配置成多数据源

spring:
  datasource:
    druid:
      db1:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:23306/db1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true
        username: root
        password: 123456
      db2:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:23306/db2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true
        username: root
        password: 123456
      test-on-borrow: true

2. 定义全局的数据源构造类DynamicDataSourceContextHolder

这个类的作用就是管理每个数据源的键,设置当前数据源的键,获取当前数据源的键。

public class DynamicDataSourceContextHolder 

    private static ThreadLocal<Object> CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceKey.DCS.getName());

    public static List<Object> dataSourceKeys = new ArrayList<Object>();

    public static void setDataSourceKey(String key)
        CONTEXT_HOLDER.set(key);
    

    public static Object getDataSourceKey()
        return CONTEXT_HOLDER.get();
    

    public static void clearDataSourceKey()
        CONTEXT_HOLDER.remove();
    

    public static Boolean containDataSourceKey(String key)
        return dataSourceKeys.contains(key);
    


2. 自定义DynamicRoutingDataSource

/**
 * 该类继承自 AbstractRoutingDataSource 类,
 * 在访问数据库时会调用该类的 determineCurrentLookupKey() 方法获取数据库实例的 key
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource 
    @Override
    protected Object determineCurrentLookupKey() 
        logger.info("current datasource is : ", DynamicDataSourceContextHolder.getDataSourceKey());
        return DynamicDataSourceContextHolder.getDataSourceKey();
    

3. 定义数据源配置类

该类的作用就是初始化数据源DataSource实例,以及初始化SqlSessionFactory实例。这里需要注意的是必须使用MybatisSqlSessionFactoryBean来获取会话工厂SqlSessionFactory,不然的话,baseMapper中的生成动态SQL的方法就不能使用了。

@Configuration
public class DataSourceConfigurer 

    /**
     * 配置数据源
     *
     * @return
     */
    @Bean(name = "db1")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.db1")
    public DataSource db1() 
        return DruidDataSourceBuilder.create().build();
    

    /**
     * 配置数据源
     *
     * @return
     */
    @Bean(name = "db2")
    @ConfigurationProperties(prefix = "spring.datasource.druid.db2")
    public DataSource db2() 
        return DruidDataSourceBuilder.create().build();
    

    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() 
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();

        Map<Object, Object> dataSourceMap = new HashMap<Object, Object>(2);
        dataSourceMap.put("db1", db1());
        dataSourceMap.put("db2", db2());

        dynamicRoutingDataSource.setDefaultTargetDataSource(dcs());
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);

        DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());

        return dynamicRoutingDataSource;
    

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception 
        //MybatisPlus使用的是MybatisSqlSessionFactory
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        //此处设置为了解决找不到mapper文件的问题
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    

    @Bean
    public SqlSessionTemplate sqlSessionTemplate() throws Exception 
        return new SqlSessionTemplate(sqlSessionFactory());
    


    /**
     * 事务
     *
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager() 
        return new DataSourceTransactionManager(dynamicDataSource());
    

4. 自定义注解TargetDataSource

该注解只是作用在方法上,这里默认的数据源是db1.

@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource 

    String value() default "db1";

5. 定义切面DynamicDataSourceAspect

切面顾名思义就是拦击标注TargetDataSource注解的方法,并且根据注解指定的数据源的key切换数据源。

@Aspect
@Component
public class DynamicDataSourceAspect 

    private Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    @Before("@annotation(targetDataSource))")
    public void switchDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) 
        if (!DynamicDataSourceContextHolder.containDataSourceKey(targetDataSource.value().getName())) 
            logger.error("DataSource [] doesn't exist, use default DataSource []", targetDataSource.value());
         else 
            DynamicDataSourceContextHolder.setDataSourceKey(targetDataSource.value().getName());
            logger.info("Switch DataSource to [] in Method []", DynamicDataSourceContextHolder.getDataSourceKey(), joinPoint.getSignature());
        
    

    @After("@annotation(targetDataSource))")
    public void restoreDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) 
        DynamicDataSourceContextHolder.clearDataSourceKey();
        logger.info("Restore DataSource to [] in Method []", DynamicDataSourceContextHolder.getDataSourceKey(), joinPoint.getSignature());
    

6. 使用注解

没有添加注解的方法使用的是默认数据源,当需要使用非默认数据源时,则需要在方法上添加 @TargetDataSource("db2") 注解。需要注意的是,该注解最好添加到xxxMapper类的方法上。

  @TargetDataSource("db2")
  ClassVO getClassStudent(@Param("open_id") String openId);

总结

本文详细介绍了两种数据源配置的方式

以上是关于SpringBoot利用自定义注解实现多数据源的主要内容,如果未能解决你的问题,请参考以下文章

springboot项目自定义注解实现的多数据源切换

自定义validation注解:解决动态多字段联动校验问题

实现SpringBoot项目的多数据源配置的两种方式(dynamic-datasource-spring-boot-starter和自定义注解的方式)

实现SpringBoot项目的多数据源配置的两种方式(dynamic-datasource-spring-boot-starter和自定义注解的方式)

SpringBoot+Redis实现多端登录+token自动续期和定期刷新+自定义注解和拦截器实现鉴权(角色和权限校验)

SpringBoot+Redis实现多端登录+token自动续期和定期刷新+自定义注解和拦截器实现鉴权(角色和权限校验)