javascript
Spring Boot2.x-09 基于Spring Boot 2.1.2 + Mybatis使用自定义注解实现数据库切换
文章目錄
- 概述
- 場景說明:讀寫分離
- 操作步驟
- 工程結構
- Step1 自定義注解
- Step2 數據源定義
- Step3 配置文件配置數據源
- Step4 數據源實例化DatasourceConfig
- Step5 Mybatis中配置成動態數據源
- Step6 ThreadLocal管理當前線程使用的數據源連接
- Step7 切面
- Step 8 核心方法,重寫AbstractRoutingDataSource#determineCurrentLookupKey
- 測試
- 庫表數據
- Domain
- Dao
- Service
- Controller
- 啟動Spring Boot 工程
- 附
- 代碼
概述
之前總結過一篇基于Spring的 數據庫切換的文章:Spring-基于Spring使用自定義注解及Aspect實現數據庫切換 ,新的項目一般都直接采用SpringBoot開發了,那我們也用Spring Boot來整一版吧。
用到的東西包含: Spring Boot + Mybatis + Druid + MySql8 + lombok 等
鑒于我們是整合了Spring Boot +Mybatis , 不清楚如何整合的可以先看下
Spring Boot2.x-07Spring Boot2.1.2整合Mybatis
場景說明:讀寫分離
簡單說下適用場景【讀寫分離】:數據庫切換通常情況是用在項目中存在主從數據庫的情況,為了減輕主庫的壓力,因為主從是同步的,所以讀的操作我們直接取從庫的數據,主庫只負責寫的操作。從庫可以使多個,當然了主庫也可以是多個,看項目架構。 這個同多數據源還是有差別的,如何支持多數據源,后面單獨開篇介紹下。
廢話不多說,直接擼起來吧
操作步驟
核心還是重寫Spring的AbstractRoutingDataSource抽象類的determineCurrentLookupKey方法。
工程結構
Step1 自定義注解
這里我們先約定,自定義注解只能標注在方法上,如果需要也能標注在類上(因為后面的判斷會有Aspect判斷會所不同)請參考 Spring-基于Spring使用自定義注解及Aspect實現數據庫切換
package com.artisan.annotation;import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME;import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target;import com.artisan.config.DataSources;/*** * 自定義注解,用于切換數據源,默認MASTER_DB* @author yangshangwei**/@Documented @Retention(RUNTIME) @Target({ METHOD }) public @interface RouteDataSource {String value() default DataSources.MASTER_DB;}Step2 數據源定義
為了方便能夠注解引用,直接用接口吧
package com.artisan.config;/*** 數據源列表* @author yangshangwei**/ public interface DataSources {String MASTER_DB = "masterDB";String SLAVE_DB = "slaveDB";}Step3 配置文件配置數據源
我們這里采用application.yml ,注意前綴,后面要用。
# datasource Master 前綴為自定義的datasource-master spring:datasource-master:driver-class-name: com.mysql.cj.jdbc.Driver # JDBC連接Mysql6以上com.mysql.cj.jdbc.Driver (服務端為Mysql8)url: jdbc:mysql://localhost:3306/master?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=falseusername: rootpassword: root# datasource Replication 前綴為自定義的datasource-slavedatasource-slave: driver-class-name: com.mysql.cj.jdbc.Driver # JDBC連接Mysql6以上com.mysql.cj.jdbc.Driver (服務端為Mysql8)url: jdbc:mysql://localhost:3306/slave?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=falseusername: rootpassword: rootStep4 數據源實例化DatasourceConfig
通過@Configuration標注為配置類。被注解的類內部包含有一個或多個被@Bean注解的方法,這些方法將會被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext類進行掃描,并用于構建bean定義,初始化Spring容器。
application.yml中定義的前綴,別搞錯了。
package com.artisan.config;import javax.sql.DataSource;import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;import com.alibaba.druid.pool.DruidDataSource;@Configuration public class DatasourceConfig {//destroy-method="close":當數據庫連接不使用的時候,將該連接重新放到數據池中@Bean(name=DataSources.MASTER_DB,destroyMethod="close")@ConfigurationProperties(prefix = "spring.datasource-master")public DataSource dataSource() {// 創建數據源return DataSourceBuilder.create().type(DruidDataSource.class).build();}@Bean(name=DataSources.SLAVE_DB,destroyMethod="close")@ConfigurationProperties(prefix = "spring.datasource-slave")public DataSource dataSourceSlave() {// 創建數據源return DataSourceBuilder.create().type(DruidDataSource.class).build();}}Step5 Mybatis中配置成動態數據源
@Configuration 功能不多說了,如上。
@MapperScan 通過使用@MapperScan可以指定要掃描的Mapper類的包的路徑,當然了也可以在Mapper接口上聲明@Mapper , 當然是@MapperScan更方便了。
內部@Bean用到了DynamicDataSource 繼承自AbstractRoutingDataSource,就是我們剛開始說的核心
application.yml配置文件中新增mybatis的如下配置
# mybatis mybatis:# 映射文件的路徑 , 這個切換數據源的場景下不能配置 * 通配符,有多個 逗號隔開,繼續跟 classpath:mapper/XXX# mapper-locations: classpath:mapper/ArtisanMapper.xml # 在MybatisConfig.java#sqlSessionFactoryBean方法中通過sqlSessionFactoryBean設置classpath:mapper/*.xml ,不然每次都要改這個地方,不好維護。# 類型別名包配置,只能指定具體的包,多個配置可以使用英文逗號隔開type-aliases-package: com.artisan.domain# Mybatis SQL語句控制臺打印configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplStep6 ThreadLocal管理當前線程使用的數據源連接
package com.artisan.config;import lombok.extern.slf4j.Slf4j;/*** * 使用ThreadLocal管理當前線程使用的數據源連接* * @author yangshangwei**/ @Slf4j public class DatasourceContextHolder {public static final String DEFAULT_DATASOURCE = DataSources.MASTER_DB;private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();/*** 設置數據源* @param dbType*/public static void setDB(String dbType) {contextHolder.set(dbType);log.info("切換到數據源{}", dbType);}/*** 獲取數據源*/public static String getDB() {return contextHolder.get();}/*** 清除數據源*/public static void clearDB() {contextHolder.remove();} }Step7 切面
通過Aspect 來處理自定義注解的橫切邏輯。
package com.artisan.aspect;import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component;import com.artisan.annotation.RouteDataSource; import com.artisan.config.DatasourceContextHolder;import java.lang.reflect.Method;/*** 通過切面對自定義切庫注解的方法進行攔截,動態的選擇數據源* * @author yangshangwei**/@Slf4j @Aspect @Component public class DynamicDataSourceAspect {/*** 前置增強,方法執行前,通過JoinPoint訪問連接點上下文的信息* * @param joinPoint*/@Before("@annotation(com.artisan.annotation.RouteDataSource)")public void beforeSwitchDataSource(JoinPoint joinPoint) {// 獲取連接點的方法簽名對象MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();// 獲取方法Method method = methodSignature.getMethod();// 設置默認的數據源為Master,防止切庫出現異常執行失敗的情況String dataSource = DatasourceContextHolder.DEFAULT_DATASOURCE;// 判斷方法上是否標注了@RouteDataSourceif (method.isAnnotationPresent(RouteDataSource.class)) {RouteDataSource routeDataSource = method.getDeclaredAnnotation(RouteDataSource.class);// 獲取@RouteDataSource上的value的值dataSource = routeDataSource.value();}// 設置數據源DatasourceContextHolder.setDB(dataSource);log.info("setDB {}", dataSource);}/*** 后置增強,清空DatasourceContextHolder,防止threadLocal誤用帶來的內存泄露*/@After("@annotation(com.artisan.annotation.RouteDataSource)")public void afterSwitchDataSource() {// 方法執行完成后,清除threadlocal中持有的databaseDatasourceContextHolder.clearDB();log.info("清空DatasourceContextHolder...");}/**@Before("@annotation(com.artisan.annotation.RouteDataSource)")public void beforeSwitchDataSource(JoinPoint point) {// 獲得當前訪問的classClass<?> className = point.getTarget().getClass();// 獲得訪問的方法名String methodName = point.getSignature().getName();// 得到方法的參數的類型Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes();String dataSource = DatasourceContextHolder.DEFAULT_DATASOURCE;try {// 得到訪問的方法對象Method method = className.getMethod(methodName, argClass);// 判斷是否存在@DS注解if (method.isAnnotationPresent(RouteDataSource.class)) {RouteDataSource annotation = method.getAnnotation(RouteDataSource.class);// 取出注解中的數據源名dataSource = annotation.value();}} catch (Exception e) {log.error("routing datasource exception, " + methodName, e);}// 切換數據源DatasourceContextHolder.setDB(dataSource);}**/ }Step 8 核心方法,重寫AbstractRoutingDataSource#determineCurrentLookupKey
根據上面的AOP攔截,通過DatasourceContextHolder.getDB()動態的取出在切面里設置(DatasourceContextHolder.setDB(dataSource))的數據源即可。
package com.artisan.config;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import lombok.extern.slf4j.Slf4j;@Slf4j public class DynamicDataSource extends AbstractRoutingDataSource{@Overrideprotected Object determineCurrentLookupKey() {log.info("數據源為{}", DatasourceContextHolder.getDB());return DatasourceContextHolder.getDB();} }測試
庫表數據
Master:
-- ---------------------------- -- Table structure for artisan -- ---------------------------- DROP TABLE IF EXISTS `artisan`; CREATE TABLE `artisan` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`sex` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;-- ---------------------------- -- Records of artisan -- ---------------------------- INSERT INTO `artisan` VALUES ('1', 'master', '女'); INSERT INTO `artisan` VALUES ('2', 'master2', '男');Slave:
-- ---------------------------- -- Table structure for artisan -- ---------------------------- DROP TABLE IF EXISTS `artisan`; CREATE TABLE `artisan` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`sex` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;-- ---------------------------- -- Records of artisan -- ---------------------------- INSERT INTO `artisan` VALUES ('1', 'replication1', '女'); INSERT INTO `artisan` VALUES ('2', 'replication2', '男');Domain
package com.artisan.domain;import lombok.Data;@Data public class Artisan {private Long id ;private String name ;private String sex;}Dao
package com.artisan.dao;import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param;import com.artisan.domain.Artisan; /*** * @author yangshangwei* * 增加@Mapper這個注解之后,Spring 啟動時會自動掃描該接口,這樣就可以在需要使用時直接注入 Mapper 了* * MybatisConfig中標注了@MapperScan , 所以這里的@Mapper不加也行*/@Mapper public interface ArtisanMapper {Artisan selectArtisanById(@Param("id") int id );}對應的sql映射文件 ,當然了也可以使用@Select注解的方式,更簡便。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><!-- 當Mapper接口和XML文件關聯的時候, namespace的值就需要配置成接口的全限定名稱 --> <mapper namespace="com.artisan.dao.ArtisanMapper"><select id="selectArtisanById" resultType="Artisan"> select id , name ,sex from artisan where id= #{id}</select> </mapper>Service
接口及實現類
忽略這個方法名,忘改了。。。。事實上是根據Id獲取某個Artisan.
package com.artisan.service;import com.artisan.domain.Artisan;public interface ArtisanService {Artisan getArtisanListFromMaster(int id);Artisan getArtisanListFromSlave(int id); }實現類
通過自定義注解設置主從庫 ,默認是主庫,@RouteDataSource(DataSources.MASTER_DB)可以省略
package com.artisan.service.impl;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;import com.artisan.annotation.RouteDataSource; import com.artisan.config.DataSources; import com.artisan.dao.ArtisanMapper; import com.artisan.domain.Artisan; import com.artisan.service.ArtisanService;@Service public class ArtisanServiceImpl implements ArtisanService {@Autowiredprivate ArtisanMapper artisanMapper;@Override@RouteDataSource(DataSources.MASTER_DB)public Artisan getArtisanListFromMaster(int id) {return artisanMapper.selectArtisanById(id);}@Override@RouteDataSource(DataSources.SLAVE_DB)public Artisan getArtisanListFromSlave(int id) {return artisanMapper.selectArtisanById(id);}}Controller
package com.artisan.controller;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;import com.artisan.domain.Artisan; import com.artisan.service.ArtisanService;@RestController public class ArtisanController {@Autowiredprivate ArtisanService artisanService ;@GetMapping("/getDataFromMaster")public Artisan getDataFromMaster(int id) {return artisanService.getArtisanListFromMaster(id);}@GetMapping("/getDataFromRep")public Artisan getDataFromRep(int id) {return artisanService.getArtisanListFromSlave(id);}}啟動Spring Boot 工程
為了驗證功能,我們從主從庫均是查詢操作吧。
訪問主庫:
http://localhost:8080/getDataFromMaster?id=1
訪問從庫:
http://localhost:8080/getDataFromRep?id=2
附
為了方便用application.properties的童鞋,代碼如下,驗證通過
#master spring.datasource-master.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource-master.url=jdbc:mysql://localhost:3306/master?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false spring.datasource-master.username=root spring.datasource-master.password=root spring.datasource-master.type=com.alibaba.druid.pool.DruidDataSource#slave spring.datasource-slave.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource-slave.url=jdbc:mysql://localhost:3306/slave?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false spring.datasource-slave.username=root spring.datasource-slave.password=root spring.datasource-slave.type=com.alibaba.druid.pool.DruidDataSource#mybatis #mybatis.mapper-locations=classpath:mapper/ArtisanMapper.xml mybatis.type-aliases-package=com.artisan.domainpom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.2.RELEASE</version><relativePath /> <!-- lookup parent from repository --></parent><groupId>com.artisan</groupId><artifactId>RoutingDataSource</artifactId><version>0.0.1-SNAPSHOT</version><name>RoutingDataSource</name><description>Artisan </description><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>代碼
https://github.com/yangshangwei/RoutingDataSource
總結
以上是生活随笔為你收集整理的Spring Boot2.x-09 基于Spring Boot 2.1.2 + Mybatis使用自定义注解实现数据库切换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Boot2.x-08Spr
- 下一篇: Spring Boot2.x-10 基于