SpringBoot -- 整合Druid實現多數據源動態切換

前言:博文詳述知識來源若依框架,可供大家一起探討學習和分享!

一、針對本文講解需求,我們需要先創建兩個數據庫進行學習,在這裏我使用的是master_test和slave_test兩個庫,兩個庫中創建同一張表t_user,字段結構如下:

master_test庫中t_user表:

slave_test庫中t_user表: 

 

二、項目創建,在這裏博主使用eclipse創建一個多模塊的maven項目,結構如下:

layduo 父級目錄
layduo-admin 後臺服務
layduo-common 通用工具
layduo-framework 框架核心
layduo-system 系統配置

接下就直接進入主題了,其他相關包依賴pom文件,可以參考git項目代碼(末尾提供)。

三、動態數據源配置,這裏使用的數據源是Druid,實現數據源之間的切換通過自定義註解@DataSource,配置Aop進行切換application-druid.yml的主從數據庫

layduo-admin添加項目全局配置文件和數據源配置文件(本文主要以yml文件形式講解)

application.yml (這裏主要關注server配置端口和訪問路徑、以及spring.profiles.active數據源文件引用就行,其他的可往後配置)

#項目相關配置
layduo:
   #名稱
   name: Layduo
   #版本
   version: 4.1.0
   #版本年份
   copyrightYear: 2019
   #實例演示開關
   demoEnabled: true
   #文件路徑( Windows配置D:/layduo/uploadPath,Linux配置 /home/layduo/uploadPath)
   profile: D:/layduo/uploadPath
   #獲取ip地址開關
   addressEnabled: true
   
#開發環境配置
server:
   #服務器HTTP端口,默認爲80
   port: 80
   #應用訪問路徑
   servlet:
      context-path: /
   tomcat:
      #tomcat的URI編碼
      uri-encoding: UTF-8
      #tomcat最大線程數,默認爲200
      max-threads: 800
      #tomcat啓動初始化的線程數,默認爲25
      min-spare-threads: 30
      
#日誌配置
logging:
   level:
      com.layduo: debug
      org.springframework: warn
      
#用戶配置
user:
   password:
      #密碼錯誤{maxRetryCount}次鎖定10分鐘
      maxRetryCount: 5
      
#spring配置
spring:
   #模板引擎
   thymeleaf:
      mode: HTML
      encoding: UTF-8
      #禁用緩存
      cache: false
   #國際資源信息
   messages:
      #國際化資源文件路徑
      basename: static/i18n/messages
   #格式化時間格式
   jackson:
      time-zone: GMT+8
      date-format: yyyy-MM-dd HH:mm:ss
   #引用數據源文件
   profiles:
      active: druid
   #文件上傳
   servlet:
      multipart:
        #單個文件大小
        max-file-size: 10MB
        #設置總上傳文件大小
        max-request-size: 20MB
   #服務模塊
   devtools:
      restart:
        #熱部署開關
        enabled: true
      
#mybatis配置
mybatis:
   #搜索指定包別名
   typeAliasesPackage: com.layduo.**.domain
   #配置mapper的掃描,找到所有mapper.xml映射文件
   mapperLocations: classpath*:mapper/**/*Mapper.xml
   #加載mybatis全局的配置文件
   configLocation: classpath:mybatis/mybatis-config.xml
   
#pagehelper分頁插件
pagehelper:
   helperDialect: mysql
   reasonable: true
   supportMethodsArguments: true
   params: count=countSql
   
#shiro
shiro:
   user:
      #登錄地址
      loginUrl: /login
      #權限認證失敗地址
      unauthorizedUrl: /unauth
      #首頁地址
      indexUrl: /index
      #驗證碼開關
      captchaEnabled: true
      #驗證碼類型: math數據計算、char字符檢驗
      captchaType: math
   cookie:
      #設置cookie的域名 默認爲空,即當前訪問的域名
      domain:
      #設置cookie的有效訪問路徑
      path: /
      #設置HttpOnly屬性
      httpOnly: true
      #設置cookie的過期時間,單位爲天
      maxAge: 30h
   session:
      #session超時時間,-1代表永不過期(默認爲30分鐘)
      expireTime: 30
      #同步session到數據庫的週期(默認1分鐘)
      dbSyncPeriod: 1
      #相隔多久檢查一次session的有效性,默認10分鐘
      validationInterval: 10
      #同一個用戶最大會話數,比如1的意思是同一個賬號允許最多同時一個人登錄(默認-1不限制)
      maxSession: 1
      #踢出之前登錄的/之後登錄的用戶,默認踢出之前登陸的用戶
      kickoutAfter: false
      
#防止xss攻擊
xss: 
   #過濾開關
   enabled: true
   #排除鏈接(多個用逗號分隔)
   excludes: /system/notice/*
   #匹配鏈接
   urlPatterns: /system/*,/monitor/*,/tool/*
      

application-druid.yml

#數據源配置
spring:
   datasource:
      type: com.alibaba.druid.pool.DruidDataSource
      #如果配置多數據,數據驅動可以不用寫,自動識別
      driverClassName: com.mysql.cj.jdbc.Driver
      druid:
         #主數據源
         master:
            url: jdbc:mysql://localhost:3306/master_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
            username: root
            password: 123456
         #從數據源
         slave:
            #從數據源開關、默認關閉
            enabled: true
            url: jdbc:mysql://localhost:3306/slave_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
            username: root
            password: 123456
         #初始化連接數
         initialSize: 5
         #最小連接池數量
         minIdle: 10
         #最大連接池數量
         maxActive: 20
         #配置獲取連接等待超時的時間
         maxWait: 60000
         # 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
         timeBetweenEvictionRunsMillis: 60000
         # 配置一個連接在池中最小生存的時間,單位是毫秒
         minEvictableIdleTimeMillis: 300000
         # 配置一個連接在池中最大生存的時間,單位是毫秒
         maxEvictableIdleTimeMillis: 900000
         # 配置檢測連接是否有效
         validationQuery: SELECT 1 FROM DUAL
         testWhileIdle: true
         testOnBorrow: false
         testOnReturn: false
         webStatFilter: 
             enabled: true
         statViewServlet:
             enabled: true
             # 設置白名單,不填則允許所有訪問
             allow:
             url-pattern: /druid/*
             # 控制檯管理用戶名和密碼
             login-username: 
             login-password:
         filter:
             stat:
                 enabled: true
                 # 慢SQL記錄
                 log-slow-sql: true
                 slow-sql-millis: 1000
                 merge-sql: true
             wall:
                 config:
                     multi-statement-allow: true    

添加DruidProperties.java(Druid相關屬性配置)

package com.layduo.framework.properties;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import com.alibaba.druid.pool.DruidDataSource;

/**
 * druid相關屬性配置
 * 
 * @author layduo
 * @createTime 2019年11月5日 上午11:18:29
 */
@Configuration
public class DruidProperties {

	@Value("${spring.datasource.druid.initialSize}")
	private int initialSize;

	@Value("${spring.datasource.druid.minIdle}")
	private int minIdle;

	@Value("${spring.datasource.druid.maxActive}")
	private int maxActive;

	@Value("${spring.datasource.druid.maxWait}")
	private int maxWait;

	@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
	private int timeBetweenEvictionRunsMillis;

	@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
	private int minEvictableIdleTimeMillis;

	@Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
	private int maxEvictableIdleTimeMillis;

	@Value("${spring.datasource.druid.validationQuery}")
	private String validationQuery;

	@Value("${spring.datasource.druid.testWhileIdle}")
	private boolean testWhileIdle;

	@Value("${spring.datasource.druid.testOnBorrow}")
	private boolean testOnBorrow;

	@Value("${spring.datasource.druid.testOnReturn}")
	private boolean testOnReturn;

	public DruidDataSource dataSource(DruidDataSource datasource) {
		/** 配置初始化大小、最小、最大 */
		datasource.setInitialSize(initialSize);
		datasource.setMaxActive(maxActive);
		datasource.setMinIdle(minIdle);

		/** 配置獲取連接等待超時的時間 */
		datasource.setMaxWait(maxWait);

		/** 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 */
		datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

		/** 配置一個連接在池中最小、最大生存的時間,單位是毫秒 */
		datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
		datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

		/**
		 * 用來檢測連接是否有效的sql,要求是一個查詢語句,常用select
		 * 'x'。如果validationQuery爲null,testOnBorrow、testOnReturn、testWhileIdle都不會起作用。
		 */
		datasource.setValidationQuery(validationQuery);
		/**
		 * 建議配置爲true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
		 */
		datasource.setTestWhileIdle(testWhileIdle);
		/** 申請連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。 */
		datasource.setTestOnBorrow(testOnBorrow);
		/** 歸還連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。 */
		datasource.setTestOnReturn(testOnReturn);
		return datasource;
	}
}

多數據源配置類 -- DruidConfig.java

package com.layduo.framework.config;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.layduo.framework.datasource.DynamicDataSource;
import com.layduo.framework.properties.DruidProperties;
import com.layduo.common.enums.DataSourceType;

/**
* @author layduo
* @createTime 2019年11月5日 上午11:17:44
* @ConditionalOnProperty控制Configuration是否生效
* value 數組,獲取對應property名稱的值,與name不可同時使用  
* prefix property名稱的前綴,可有可無
* name 數組,property完整名稱或部分名稱(可與prefix組合使用,組成完整的property名稱),與value不可同時使用  
* havingValue 可與name組合使用,比較獲取到的屬性值與havingValue給定的值是否相同,相同才加載配置  
* matchIfMissing 缺少該property時是否可以加載。如果爲true,沒有該property也會正常加載;反之報錯  
* relaxedNames 是否可以鬆散匹配,至今不知道怎麼使用的  
*/
@Configuration
public class DruidConfig {

	@Bean
	@ConfigurationProperties("spring.datasource.druid.master")
	public DataSource masterDataSource(DruidProperties druidProperties) {
		DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
		return druidProperties.dataSource(dataSource);
	}
	
	@Bean
	@ConfigurationProperties("spring.datasource.druid.slave")
	@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
	public DataSource slaveDataSource(DruidProperties druidProperties) {
		DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
		return druidProperties.dataSource(dataSource);
	}
	
	@Bean(name = "dynamicDataSource")
	@Primary
	public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
		Map<Object, Object> targetDataSources = new HashMap<>();
		targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
		targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource);
		return new DynamicDataSource(masterDataSource, targetDataSources);
	}
	
	/**
	 * 去除監控頁面底部廣告
	 * @param properties
	 * @return
	 */
	@SuppressWarnings({"rawtypes", "unchecked"})
	@Bean
	@ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
	public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
		//獲取web監控頁面的參數
		DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
		//提取common.js的配置路徑
		String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
		String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
		final String filePath = "support/http/resources/js/common.js";
		//創建filter進行過濾
		Filter filter = new Filter() {
			
			@Override
			public void init(FilterConfig filterConfig) throws ServletException {
				
			}
			
			@Override
			public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
					throws IOException, ServletException {
				chain.doFilter(request, response);
				//重置緩衝區,響應頭不會被重置
				response.resetBuffer();
				//獲取common.js
				String text = Utils.readFromResource(filePath);
				//正則替換banner, 除去底部廣告信息
				text = text.replaceAll("<a.*?banner\"></a><br/>", "");
				text = text.replaceAll("powered.*?shrek.wang</a>", "");
				response.getWriter().write(text);		
			}
			
			@Override
			public void destroy() {
				
			}
		};
		
		FilterRegistrationBean registrationBean = new FilterRegistrationBean();
		registrationBean.setFilter(filter);
		registrationBean.addUrlPatterns(commonJsPattern);
		return registrationBean;
	}
}

動態數據源切換類 -- DynamicDataSource

package com.layduo.framework.datasource;

import java.util.Map;

import javax.sql.DataSource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import com.layduo.common.config.datasource.DynamicDataSourceContextHolder;

/**
 * 動態數據源
 * 
 * @author layduo
 * @createTime 2019年11月5日 上午11:31:43
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

	public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
		super.setDefaultTargetDataSource(defaultTargetDataSource);
		super.setTargetDataSources(targetDataSources);
		super.afterPropertiesSet();
	}

	@Override
	protected Object determineCurrentLookupKey() {
		return DynamicDataSourceContextHolder.getDataSourceType();
	}

}

動態數據源切換處理類 -- DynamicDataSourceContextHolder.java

package com.layduo.common.config.datasource;
/**
 * 動態數據源處理類
* @author layduo
* @createTime 2019年11月5日 上午11:36:14
*/

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DynamicDataSourceContextHolder {

	public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

	/**
	 * 使用ThreadLocal維護變量,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,
	 * 所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
	 */
	private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

	/**
	 * 設置數據源的變量
	 */
	public static void setDataSourceType(String dataSourceType) {
		log.info("切換到{}數據源", dataSourceType);
		CONTEXT_HOLDER.set(dataSourceType);
	}

	/**
	 * 獲取數據源的變量
	 */
	public static String getDataSourceType() {
		return CONTEXT_HOLDER.get();
	}

	/**
	 * 清空數據源變量
	 */
	public static void clearDataSourceType() {
		CONTEXT_HOLDER.remove();
	}
}

多數據源枚舉,若添加從庫有多個,可依次按以下方式添加

package com.layduo.common.enums;

/**
 * 數據源
 * 
 * @author layduo
 * @createTime 2019年11月5日 下午2:15:50
 */
public enum DataSourceType {

	/**
	 * 主庫
	 */
	MASTER,

	/**
	 * 從庫
	 */
	SLAVE
}

自定義多數據源切換註解@DataSource

package com.layduo.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.layduo.common.enums.DataSourceType;

/**
 * 自定義多數據源切換註解
 * 
 * @author layduo
 * @createTime 2019年11月5日 下午3:36:32
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {

	/**
	 * 切換數據源名稱
	 */
	public DataSourceType value() default DataSourceType.MASTER;
}

通過Aop對多數據源切面處理 -- DataSourceAspect.java

package com.layduo.framework.aspectj;

import java.lang.reflect.Method;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.layduo.common.annotation.DataSource;
import com.layduo.common.config.datasource.DynamicDataSourceContextHolder;
import com.layduo.common.utils.StringUtils;

/**
 * 多數據源切面處理
 * 
 * @author layduo
 * @createTime 2019年11月5日 下午3:41:17
 */
@Aspect
@Order(1) // 優先級
@Component
public class DataSourceAspect {

	protected Logger logger = LoggerFactory.getLogger(getClass());

	/**
	 * 通過自定義註解@DataSource定義切點
	 */
	@Pointcut("@annotation(com.layduo.common.annotation.DataSource)"
			+ "|| @within(com.layduo.common.annotation.DataSource)")
	public void dsPointCut() {
	}

	/**
	 * 切點環繞
	 * @param point
	 * @return
	 * @throws Throwable
	 */
	@Around("dsPointCut()")
	public Object around(ProceedingJoinPoint point) throws Throwable {
		DataSource dataSource = getDataSource(point);
		if (StringUtils.isNotNull(dataSource)) {
			DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
		}

		try {
			return point.proceed();
		} finally {
			DynamicDataSourceContextHolder.clearDataSourceType();
		}
	}

	/**
	 * 獲取需要切換的數據源
	 */
	public DataSource getDataSource(ProceedingJoinPoint point) {
		MethodSignature signature = (MethodSignature) point.getSignature();
		Class<? extends Object> targetClass = point.getTarget().getClass();
		DataSource targetDataSource = targetClass.getAnnotation(DataSource.class);
		if (StringUtils.isNotNull(targetDataSource)) {
			return targetDataSource;
		} else {
			Method method = signature.getMethod();
			DataSource dataSource = method.getAnnotation(DataSource.class);
			return dataSource;
		}
	}
}

配置啓動類註解信息,去除框架自動數據源配置

package com.layduo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

/**
 * 項目啓動程序
 * 
 * @author layduo
 *
 */
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class LayduoApplication {
	public static void main(String[] args) {
		SpringApplication.run(LayduoApplication.class, args);
		System.out.println("(♥◠‿◠)ノ゙  項目啓動成功   ლ(´ڡ`ლ)゙  \n");
	}
}

利用@DataSource註解進行動態數據源切換,將該註解作用於sevice或mapper上

@DataSource(value = DataSourceType.SLAVE)
public List<SysUser> selectUserList(SysUser user) {
	return userMapper.selectUserList(user);
}

對於特殊情況可以通過DynamicDataSourceContextHolder手動實現數據源切換

public List<SysUser> selectUserList(SysUser user) {
	DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE.name());
	List<SysUser> userList = userMapper.selectUserList(user);
	DynamicDataSourceContextHolder.clearDataSourceType();
	return userList;
}

最後在這裏做一下簡單的測試,編寫DemoController.java

package com.layduo.web.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.layduo.common.annotation.DataSource;
import com.layduo.common.enums.DataSourceType;

/**
 * @author layduo
 * @createTime 2019年11月5日 下午3:21:19
 */
@Controller
public class DemoController {

	@RequestMapping("/sayHelloByMaster")
	@ResponseBody
	@DataSource(value = DataSourceType.MASTER)
	public String sayHelloByMaster() {
		return "Master say Hello World for you!";
	}
	
	@RequestMapping("/sayHelloBySlave")
	@ResponseBody
	@DataSource(value = DataSourceType.SLAVE)
	public String sayHelloBySlave() {
		return "Slave say Hello World for you!";
	}
}

項目源碼已上傳github: https://github.com/builthuLin/layduo.git 有需要的自己fork一下~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章