首頁 > 軟體

SpringBoot多資料來源設定並通過註解實現動態切換資料來源

2022-08-01 14:01:54

1. 環境準備

1.1 資料庫準備

一個本地環境的MySQL資料庫,資料庫mydb,建立表t_user

CREATE TABLE `t_user` (
  `c_id` varchar(20) NOT NULL,
  `c_username` varchar(20) DEFAULT NULL,
  `c_password` varchar(20) DEFAULT NULL,
  `c_gender` tinyint(2) DEFAULT NULL,
  PRIMARY KEY (`c_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


INSERT INTO `mydb`.`t_user`(`c_id`, `c_username`, `c_password`, `c_gender`) VALUES ('1', '思思', '123', 1);

一個雲伺服器的MySQL資料庫,建立資料庫book_db,建立表t_userinfo。

CREATE TABLE `t_user_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '使用者id',
  `user_name` varchar(50) DEFAULT NULL COMMENT '使用者名稱',
  `password` varchar(255) DEFAULT NULL COMMENT '登入密碼',
  `areaObj` varchar(255) DEFAULT NULL COMMENT '所在學院',
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `sex` tinyint(255) DEFAULT NULL COMMENT '性別',
  `user_photo` varchar(255) DEFAULT NULL COMMENT '學生照片',
  `birthday` varchar(20) DEFAULT NULL COMMENT '出生日期',
  `telephone` varchar(20) DEFAULT NULL COMMENT '聯絡電話',
  `address` varchar(255) DEFAULT NULL COMMENT '家庭地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `book_db`.`t_user_info`(`id`, `user_name`, `password`, `areaObj`, `name`, `sex`, `user_photo`, `birthday`, `telephone`, `address`) VALUES (1, '張三', '123', '哈爾濱', '張三散', 1, '123', '02-16', '15756892458', '黑龍江省哈爾濱市');

建立資料庫chatroom,建立表admin。

CREATE TABLE `admin` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL COMMENT '登入賬號',
  `nickname` varchar(20) NOT NULL COMMENT '暱稱',
  `password` varchar(255) NOT NULL COMMENT '密碼',
  `user_profile` varchar(255) DEFAULT NULL COMMENT '管理員頭像',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

INSERT INTO `chatroom`.`admin`(`id`, `username`, `nickname`, `password`, `user_profile`) VALUES (1, 'admin', '系統管理員', '$2a$10$PyloUEVGuO0fUZdfeIaROOTluRmccl.Scifa8S7Os0Wt.s4bDkb', 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1784117537,3335593911&fm=26&gp=0.jpg');

1.2 專案建立

建立SpringBoot專案,整合MyBatis-Plus。pom.xml引入的依賴:

<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

		<!--mybatis plus-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.0.1</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!--druid-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>1.1.10</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.9</version>
		</dependency>

	</dependencies>

設定讀取resource資料夾下的mapper檔案

	<build>
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>

		</plugins>
	</build>

2. ThreadLocal類介紹

https://www.jb51.net/article/257530.htm

3. AbstractRoutingDataSource類介紹

Spring Boot提供了AbstractRoutingDataSource 根據使用者定義的規則選擇要使用的資料來源,這樣我們可以在每次資料庫操作前設定使用的資料來源,實現可動態路由的資料來源。它的抽象方法determineCurrentLookupKey() 決定使用哪個資料來源。

getConnection()獲取資料庫連線,根據查詢lookup key鍵對不同目標資料來源的呼叫,通常是通過(但不一定)某些執行緒繫結的事物上下文來實現。通過這我們知道可以實現:資料來源的動態切換,在程式執行時,把資料來源動態織入到程式中,靈活得進行資料來源切換,從而可以不依賴中介軟體,實現讀寫分離功能。

AbstractRoutingDataSource實現邏輯:

  • 繼承抽象類AbstractRoutingDataSource,並實現determineCurrentLookupKey()方法。自定義LookupKey的選擇規則。
  • 把設定的多個資料來源放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中(使用setDefaultTargetDataSource和setTargetDataSources方法),然後通過afterPropertiesSet()方法將資料來源分別進行復制到AbstractRoutingDataSource的resolvedDataSources屬性和resolvedDefaultDataSource屬性中。

呼叫AbstractRoutingDataSource的getConnection()的方法的時候,先呼叫determineTargetDataSource()方法返回DataSource在進行getConnection()。

determineTargetDataSource()方法通過呼叫determineCurrentLookupKey() 方法返回的lookupKey決定使用哪個資料來源。

4. 具體實現

4.1 定義資料來源列舉類

定義資料來源列舉類DataSourceTypeEnum

public enum DataSourceTypeEnum {
    /**
     * chatroom
     */
    CHATROOM("chatroom"),
    /**
     * book_db
     */
    BOOK_DB("book_db"),
    /**
     * mydb
     */
    MY_DB("mydb");

    private final String name;

    DataSourceTypeEnum(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

4.2 建立動態多資料來源類

定義一個動態多資料來源類DynamicDataSource用於管理不同執行緒間多個資料來源的選擇和切換,擴充套件 Spring 提供的 AbstractRoutingDataSource 抽象類,重寫 determineCurrentLookupKey 方法,其中的determineCurrentLookupKey() 方法用於決定使用哪個資料來源。

public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * ThreadLocal 用於提供執行緒區域性變數,在多執行緒環境可以保證各個執行緒裡的變數獨立於其它執行緒裡的變數。
     * 也就是說 ThreadLocal 可以為每個執行緒建立一個【單獨的變數副本】,相當於執行緒的 private static 型別變數。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 決定使用哪個資料來源之前需要把多個資料來源的資訊以及預設資料來源資訊設定好
     *
     * @param defaultTargetDataSource 預設資料來源
     * @param targetDataSources       目標資料來源
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * determineCurrentLookupKey決定使用哪個資料庫
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }

    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }
    
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

4.3 建立動態多資料來源設定類

DynamicDataSourceConfig類作為設定類,讀取組態檔的三個資料來源的設定,建立對應DataSource型別的Bean。

@Configuration
public class DynamicDataSourceConfig {

    @Bean(name="chatroom")
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource dataSource1(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name ="book_db")
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource dataSource2(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name="mydb")
    @ConfigurationProperties("spring.datasource.druid.third")
    public DataSource dataSource3(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name="dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>(5);
        targetDataSources.put(DataSourceTypeEnum.CHATROOM.getName(), dataSource1());
        targetDataSources.put(DataSourceTypeEnum.BOOK_DB.getName(), dataSource2());
        targetDataSources.put(DataSourceTypeEnum.MY_DB.getName(), dataSource3());
        return new DynamicDataSource(dataSource1(), targetDataSources);
    }
}

4.4 自定義註解用於指定資料來源

自定義註解@SpecifyDataSource用於在Service層方法上標記要使用哪個資料來源。這裡定義預設使用資料來源 DataSourceType.CHATROOM。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SpecifyDataSource {

    /**
     * @return
     */
    DataSourceTypeEnum value() default DataSourceTypeEnum.CHATROOM;
}

4.5 AOP實現動態切換資料來源

定義資料來源介面類DataSourceAspect,用於實現有SpecifyDataSource註解標註的方法前切換註解指定的資料來源。

@Aspect
@Component
@Order(value = 1)
public class DataSourceAspect {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Pointcut("@annotation(top.javahai.datasource.annotation.SpecifyDataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        SpecifyDataSource ds = method.getAnnotation(SpecifyDataSource.class);
        if (ds == null) {
            DynamicDataSource.setDataSource(DataSourceType.CHATROOM.getName());
            logger.info("set datasource is " + DataSourceType.CHATROOM);
        } else {
            DynamicDataSource.setDataSource(ds.value().getName());
            logger.info("set datasource is " + ds.value().getName());
        }

        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
            logger.info("clean datasource");
        }
    }
}

5. 測試使用

5.1 設定資料來源

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
# 資料來源1
spring.datasource.druid.first.url=jdbc:mysql://158.156.444.68:3306/chatroom?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.druid.first.username=root
spring.datasource.druid.first.password=123456
# 資料來源2
spring.datasource.druid.second.url=jdbc:mysql://158.156.444.68:3306/book_db?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.druid.second.username=root
spring.datasource.druid.second.password=123456

#資料來源3
spring.datasource.druid.third.url=jdbc:mysql:///mydb?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.druid.third.username=root
spring.datasource.druid.third.password=123456

mybatis-plus.mapper-locations=classpath:mapper/*.xml


#輸出sql執行紀錄檔
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

5.2 建立實體類

建立實體類Admin

public class Admin implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 登入賬號
     */
    private String username;

    /**
     * 暱稱
     */
    private String nickname;

    /**
     * 密碼
     */
    private String password;

    /**
     * 管理員頭像
     */
    private String userProfile;

//省略getter/setter方法

建立實體類TUser

public class TUser implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "c_id", type = IdType.AUTO)
    private Integer cId;

    private String cUsername;

    private String cPassword;

    private Integer cGender;
}

建立實體類TUserinfo

@TableName(value = "t_user_info")
public class TUserinfo implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * user_name
     */
    private String userName;

    /**
     * 登入密碼
     */
    private String password;

    /**
     * 所在學院
     */
    @TableField("areaObj")
    private String areaObj;

    /**
     * 姓名
     */
    private String name;

    /**
     * 性別
     */
    private Integer sex;

    /**
     * 學生照片
     */
    private String userPhoto;

    /**
     * 出生日期
     */
    private String birthday;

    /**
     * 聯絡電話
     */
    private String telephone;

    /**
     * 家庭地址
     */
    private String address;
}

建立UserVO用於測試

public class UserVO {

    private List<Admin> adminList;
    private List<TUserinfo> tUserinfos;
    private List<TUser> tUsers;
}

5.3 服務層程式碼

@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {

    public List<Admin> getAll(){
        return this.list(null);
    }
}
@Service
public class TUserinfoServiceImpl extends ServiceImpl<TUserinfoMapper, TUserinfo> implements ITUserinfoService {

    @SpecifyDataSource(value = DataSourceTypeEnum.BOOK_DB)
    public List<TUserinfo> selectAll(){
        return this.list(null);
    }

}
@Service
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements ITUserService {

    @SpecifyDataSource(value = DataSourceTypeEnum.MY_DB)
    public List<TUser> selectAll(){
        return this.list(null);
    }
}
public interface AdminMapper extends BaseMapper<Admin> {

}

public interface TUserinfoMapper extends BaseMapper<TUserinfo> {

}

public interface TUserMapper extends BaseMapper<TUser> {

}

5.4 控制層程式碼

建立介面/test/list用於測試

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private AdminServiceImpl adminService;

    @Autowired
    private TUserinfoServiceImpl userinfoService;

    @Autowired
    private TUserServiceImpl userService;

    @GetMapping("/list")
    public UserVO list(){
        List<Admin> adminList= adminService.getAll();
        List<TUserinfo> tUserinfos = userinfoService.selectAll();
        List<TUser> tUsers = userService.selectAll();
        UserVO userVO = new UserVO();
        userVO.setAdminList(adminList);
        userVO.settUserinfos(tUserinfos);
        userVO.settUsers(tUsers);
        return userVO;
    }
}

瀏覽器請求/test/list

檢視控制檯輸出,檢視資料來源的切換紀錄檔

完整Demo程式碼地址:https://github.com/JustCoding-Hai/learn-everyday/tree/master/learn-multi_data_source

到此這篇關於SpringBoot多資料來源設定並通過註解實現動態切換資料來源的文章就介紹到這了,更多相關SpringBoot 動態切換資料來源內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com