前言:博文詳述知識來源若依框架,可供大家一起探討學習和分享!
一、針對本文講解需求,我們需要先創建兩個數據庫進行學習,在這裏我使用的是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一下~