一 Mybatis
1、Maven依賴
在ruoyi父項目的pom文件中有一個分頁插件的依賴
<!-- pagehelper 分頁插件 -->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${pagehelper.boot.version}</version>
</dependency>
pagehelper分頁插件依賴中已經包含了Mybatis
2、Mybatis配置詳解
在ruoyi-admin模塊的application.yml中的配置
# MyBatis配置
mybatis:# 搜索指定包別名typeAliasesPackage: com.ruoyi.**.domain# 配置mapper的掃描,找到所有的mapper.xml映射文件mapperLocations: classpath*:mapper/**/*Mapper.xml# 加載全局的配置文件configLocation: classpath:mybatis/mybatis-config.xml
Spring Boot提供了一個啟動器的類MybatisAutoConfiguration,其中有一個配置MybatisProperties。從MybatisProperties中我們可以看到是從配置文件yml中讀取mybatis下的配置信息以及一些內置的配置項
其中配置項Configuration與我們ruoyi-admin模塊resources下的mybatis中的mybatis-config.xml是一樣的
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><!-- 全局參數 --><settings><!-- 使全局的映射器啟用或禁用緩存 --><setting name="cacheEnabled" value="true" /><!-- 允許JDBC 支持自動生成主鍵 --><setting name="useGeneratedKeys" value="true" /><!-- 配置默認的執行器.SIMPLE就是普通執行器;REUSE執行器會重用預處理語句(prepared statements);BATCH執行器將重用語句并執行批量更新 --><setting name="defaultExecutorType" value="SIMPLE" /><!-- 指定 MyBatis 所用日志的具體實現 --><setting name="logImpl" value="SLF4J" /><!-- 使用駝峰命名法轉換字段 --><!-- <setting name="mapUnderscoreToCamelCase" value="true"/> --></settings></configuration>
除此之外還有一個自定義的MybatisConfig配置類,其中注冊了Bean SqlSessionFactory把MybatisAutoConfiguration中的SqlSessionFactory覆蓋掉了。這里主要是為了一些配置項的通配符支持,如:設置搜索指定包名的配置項typeAliasesPackage,默認的typeAliasesPackage當有多個包需要掃描時,需要配置多個包,而重寫后可以通過通配符來設置。
package com.ruoyi.framework.config;import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import javax.sql.DataSource;
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import com.ruoyi.common.utils.StringUtils;/*** Mybatis支持*匹配掃描包* * @author ruoyi*/
@Configuration
public class MyBatisConfig
{@Autowiredprivate Environment env;static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";public static String setTypeAliasesPackage(String typeAliasesPackage){ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);List<String> allResult = new ArrayList<String>();try{for (String aliasesPackage : typeAliasesPackage.split(",")){List<String> result = new ArrayList<String>();aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX+ ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;Resource[] resources = resolver.getResources(aliasesPackage);if (resources != null && resources.length > 0){MetadataReader metadataReader = null;for (Resource resource : resources){if (resource.isReadable()){metadataReader = metadataReaderFactory.getMetadataReader(resource);try{result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());}catch (ClassNotFoundException e){e.printStackTrace();}}}}if (result.size() > 0){HashSet<String> hashResult = new HashSet<String>(result);allResult.addAll(hashResult);}}if (allResult.size() > 0){typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));}else{throw new RuntimeException("mybatis typeAliasesPackage 路徑掃描錯誤,參數typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");}}catch (IOException e){e.printStackTrace();}return typeAliasesPackage;}public Resource[] resolveMapperLocations(String[] mapperLocations){ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();List<Resource> resources = new ArrayList<Resource>();if (mapperLocations != null){for (String mapperLocation : mapperLocations){try{Resource[] mappers = resourceResolver.getResources(mapperLocation);resources.addAll(Arrays.asList(mappers));}catch (IOException e){// ignore}}}return resources.toArray(new Resource[resources.size()]);}@Beanpublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");String mapperLocations = env.getProperty("mybatis.mapperLocations");String configLocation = env.getProperty("mybatis.configLocation");typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);VFS.addImplClass(SpringBootVFS.class);final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();sessionFactory.setDataSource(dataSource);sessionFactory.setTypeAliasesPackage(typeAliasesPackage);sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));return sessionFactory.getObject();}
}
注意: 如果后續需要加mybatis的其它配置的話不僅要在yml中進行配置還需要在MybatisConfig中進行對應新增否則不會生效。
3、Mybatis使用
在rouyi-framework模塊com.ruoyi.framework.config下有一個ApplicationConfig,這里指定了要掃描的Mapper類的包的路徑 @MapperScan(“com.ruoyi.**.mapper”)。否則需要在每個mapper中加上@Mapper
package com.ruoyi.framework.config;import java.util.TimeZone;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;/*** 程序注解配置** @author ruoyi*/
@Configuration
// 表示通過aop框架暴露該代理對象,AopContext能夠訪問
@EnableAspectJAutoProxy(exposeProxy = true)
// 指定要掃描的Mapper類的包的路徑
@MapperScan("com.ruoyi.**.mapper")
public class ApplicationConfig
{/*** 時區配置*/@Beanpublic Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization(){return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());}
}
在xml中id名要和mapper中方法名一致
實體類屬性與數據庫字段映射,調用時也是用id名以及一些mybatis的基礎使用
<?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 namespace="com.ruoyi.system.mapper.SysConfigMapper"><resultMap type="SysConfig" id="SysConfigResult"><id property="configId" column="config_id" /><result property="configName" column="config_name" /><result property="configKey" column="config_key" /><result property="configValue" column="config_value" /><result property="configType" column="config_type" /><result property="createBy" column="create_by" /><result property="createTime" column="create_time" /><result property="updateBy" column="update_by" /><result property="updateTime" column="update_time" /></resultMap><sql id="selectConfigVo">select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark from sys_config</sql><!-- 查詢條件 --><sql id="sqlwhereSearch"><where><if test="configId !=null">and config_id = #{configId}</if><if test="configKey !=null and configKey != ''">and config_key = #{configKey}</if></where></sql><select id="selectConfig" parameterType="SysConfig" resultMap="SysConfigResult"><!-- 上面定義好的查詢和條件 --><include refid="selectConfigVo"/><include refid="sqlwhereSearch"/></select><select id="selectConfigList" parameterType="SysConfig" resultMap="SysConfigResult"><include refid="selectConfigVo"/><where><if test="configName != null and configName != ''">AND config_name like concat('%', #{configName}, '%')</if><if test="configType != null and configType != ''">AND config_type = #{configType}</if><if test="configKey != null and configKey != ''">AND config_key like concat('%', #{configKey}, '%')</if><if test="params.beginTime != null and params.beginTime != ''"><!-- 開始時間檢索 -->and date_format(create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')</if><if test="params.endTime != null and params.endTime != ''"><!-- 結束時間檢索 -->and date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')</if></where></select><select id="selectConfigById" parameterType="Long" resultMap="SysConfigResult"><include refid="selectConfigVo"/>where config_id = #{configId}</select><select id="checkConfigKeyUnique" parameterType="String" resultMap="SysConfigResult"><include refid="selectConfigVo"/>where config_key = #{configKey} limit 1</select><insert id="insertConfig" parameterType="SysConfig">insert into sys_config (<if test="configName != null and configName != '' ">config_name,</if><if test="configKey != null and configKey != '' ">config_key,</if><if test="configValue != null and configValue != '' ">config_value,</if><if test="configType != null and configType != '' ">config_type,</if><if test="createBy != null and createBy != ''">create_by,</if><if test="remark != null and remark != ''">remark,</if>create_time)values(<if test="configName != null and configName != ''">#{configName},</if><if test="configKey != null and configKey != ''">#{configKey},</if><if test="configValue != null and configValue != ''">#{configValue},</if><if test="configType != null and configType != ''">#{configType},</if><if test="createBy != null and createBy != ''">#{createBy},</if><if test="remark != null and remark != ''">#{remark},</if>sysdate())</insert><update id="updateConfig" parameterType="SysConfig">update sys_config <set><if test="configName != null and configName != ''">config_name = #{configName},</if><if test="configKey != null and configKey != ''">config_key = #{configKey},</if><if test="configValue != null and configValue != ''">config_value = #{configValue},</if><if test="configType != null and configType != ''">config_type = #{configType},</if><if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if><if test="remark != null">remark = #{remark},</if>update_time = sysdate()</set>where config_id = #{configId}</update><delete id="deleteConfigById" parameterType="Long">delete from sys_config where config_id = #{configId}</delete><delete id="deleteConfigByIds" parameterType="Long">delete from sys_config where config_id in <foreach item="configId" collection="array" open="(" separator="," close=")">#{configId}</foreach></delete></mapper>
二 分頁
1、分頁配置
ruoyi這里使用的是pagehelper分頁插件,Maven依賴如下:
<properties>
<pagehelper.boot.version>1.4.7</pagehelper.boot.version>
</properties><!-- pagehelper 分頁插件 -->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${pagehelper.boot.version}</version>
</dependency>
yml配置
# PageHelper分頁插件
pagehelper:# 設置數據庫方言,這里設置為mysqlhelperDialect: mysql# 支持通過方法參數來傳遞分頁參數supportMethodsArguments: true# params配置是用于指定分頁插件的參數名,count=countSql表示使用countSql作為count查詢的參數params: count=countSql
其它參數介紹見Mybatis官方文檔
2、分頁插件的使用及代碼詳解
這里使用了分頁查詢的第二種方法,其它方法及使用方式同見官方文檔
//第二種,Mapper接口方式的調用,推薦這種使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);
官方文檔展示使用方法
在你需要進行分頁的 MyBatis 查詢方法前調用 PageHelper.startPage 靜態方法即可,緊跟在這個方法后的第一個MyBatis 查詢方法會被進行分頁。
// 示例
//獲取第1頁,10條內容,默認查詢總數count
PageHelper.startPage(1, 10);
//緊跟著的第一個select方法會被分頁
List<User> list = userMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分頁時,實際返回的結果list類型是Page<E>,如果想取出分頁信息,需要強制轉換為Page<E>
assertEquals(182, ((Page) list).getTotal());
ruoyi中的使用
拿一個分頁查詢方法舉例
/*** 獲取參數配置列表*/
@PreAuthorize("@ss.hasPermi('system:config:list')")
@GetMapping("/list")
public TableDataInfo list(SysConfig config)
{// 在分頁查詢方法前調用startPage()startPage();List<SysConfig> list = configService.selectConfigList(config);return getDataTable(list);
}
前端請求時需要加上pageNum和pageSize參數
接口使用的startPage()進行了封裝,點到BaseController
/*** 設置請求分頁數據*/
protected void startPage()
{PageUtils.startPage();
}
再點到PageUtils中
package com.ruoyi.common.utils;import com.github.pagehelper.PageHelper;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.utils.sql.SqlUtil;/*** 分頁工具類* * @author ruoyi*/
public class PageUtils extends PageHelper
{/*** 設置請求分頁數據*/public static void startPage(){// 獲取前端分頁參數PageDomain pageDomain = TableSupport.buildPageRequest();Integer pageNum = pageDomain.getPageNum();Integer pageSize = pageDomain.getPageSize();// 對排序字段進行SQL注入防護轉義String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());Boolean reasonable = pageDomain.getReasonable();// 這里給PageHelper.startPage傳了頁數,頁大小,排序方式以及是否分頁參數合理化PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);}/*** 清理分頁的線程變量*/public static void clearPage(){PageHelper.clearPage();}
}
再來查看這個PageDomain
package com.ruoyi.common.core.page;import com.ruoyi.common.utils.StringUtils;/*** 分頁數據* * @author ruoyi*/
public class PageDomain
{/** 當前記錄起始索引 */private Integer pageNum;/** 每頁顯示記錄數 */private Integer pageSize;/** 排序列 */private String orderByColumn;/** 排序的方向desc或者asc */private String isAsc = "asc";/** 分頁參數合理化 */private Boolean reasonable = true;public String getOrderBy(){if (StringUtils.isEmpty(orderByColumn)){return "";}// 將駝峰修改為符合數據表字段的下劃線格式并拼接上排序方向return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc;}public Integer getPageNum(){return pageNum;}public void setPageNum(Integer pageNum){this.pageNum = pageNum;}public Integer getPageSize(){return pageSize;}public void setPageSize(Integer pageSize){this.pageSize = pageSize;}public String getOrderByColumn(){return orderByColumn;}public void setOrderByColumn(String orderByColumn){this.orderByColumn = orderByColumn;}public String getIsAsc(){return isAsc;}public void setIsAsc(String isAsc){if (StringUtils.isNotEmpty(isAsc)){// 兼容前端排序類型if ("ascending".equals(isAsc)){isAsc = "asc";}else if ("descending".equals(isAsc)){isAsc = "desc";}this.isAsc = isAsc;}}public Boolean getReasonable(){if (StringUtils.isNull(reasonable)){return Boolean.TRUE;}return reasonable;}public void setReasonable(Boolean reasonable){this.reasonable = reasonable;}
}
注意點(此處注意點內容來自ruoyi官方文檔):
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(User user)
{// 此方法配合前端完成自動分頁startPage();List<User> list = userService.selectUserList(user);return getDataTable(list);
}
- 常見坑點1:selectPostById莫名其妙的分頁。例如下面這段代碼
startPage();
List<User> list;
if(user != null){list = userService.selectUserList(user);
} else {list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);
原因分析:這種情況下由于user存在null的情況,就會導致pageHelper生產了一個分頁參數,但是沒有被消費,這個參數就會一直保留在這個線程上。 當這個線程再次被使用時,就可能導致不該分頁的方法去消費這個分頁參數,這就產生了莫名其妙的分頁。
上面這個代碼,應該寫成下面這個樣子才能保證安全。
List<User> list;
if(user != null){startPage();list = userService.selectUserList(user);
} else {list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);
- 常見坑點2:添加了startPage方法。也沒有正常分頁。例如下面這段代碼
startPage();
Post post = postService.selectPostById(1L);
List<User> list = userService.selectUserList(user);
return getDataTable(list);
原因分析:只對該語句以后的第一個查詢(Select)語句得到的數據進行分頁。
上面這個代碼,應該寫成下面這個樣子才能正常分頁。
Post post = postService.selectPostById(1L);
startPage();
List<User> list = userService.selectUserList(user);
return getDataTable(list);
提示
項目分頁插件默認是Mysql語法,如果項目改為其他數據庫需修改配置application.yml文件中的屬性helperDialect: 你的數據庫
注意
只要你可以保證在PageHelper方法調用后緊跟MyBatis查詢方法,這就是安全的。因為PageHelper在finally代碼段中自動清除了ThreadLocal存儲的對象。 如果代碼在進入Executor前發生異常,就會導致線程不可用,這屬于人為的Bug(例如接口方法和XML中的不匹配,導致找不到MappedStatement時),這種情況由于線程不可用,也不會導致ThreadLocal參數被錯誤的使用。
最后再來看下解析前端分頁參數的TableSupport,其解析前端傳來的分頁,排序參數并返回分頁對象PageDomain
package com.ruoyi.common.core.page;import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.ServletUtils;/*** 表格數據處理* * @author ruoyi*/
public class TableSupport
{/*** 當前記錄起始索引*/public static final String PAGE_NUM = "pageNum";/*** 每頁顯示記錄數*/public static final String PAGE_SIZE = "pageSize";/*** 排序列*/public static final String ORDER_BY_COLUMN = "orderByColumn";/*** 排序的方向 "desc" 或者 "asc".*/public static final String IS_ASC = "isAsc";/*** 分頁參數合理化*/public static final String REASONABLE = "reasonable";/*** 封裝分頁對象*/public static PageDomain getPageDomain(){PageDomain pageDomain = new PageDomain();pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));return pageDomain;}public static PageDomain buildPageRequest(){return getPageDomain();}
}
三 數據源
1、數據源配置詳解
引入Maven依賴
<properties><druid.version>1.2.23</druid.version>
</properties><dependencies><!-- 阿里數據庫連接池 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency>
</dependencies>
我們先看配置文件application.yml
再看數據源配置文件application-druid.yml
alibb/druid官網地址
# 數據源配置
spring:datasource:# 指定連接池類型type: com.alibaba.druid.pool.DruidDataSource# 指定數據庫驅動driverClassName: com.mysql.cj.jdbc.Driverdruid:# 主庫數據源master:url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: password# 從庫數據源slave:# 從數據源開關/默認關閉enabled: falseurl: username: password: # 初始連接數initialSize: 5# 最小連接池數量minIdle: 10# 最大連接池數量maxActive: 20# 配置獲取連接等待超時的時間maxWait: 60000# 配置連接超時時間connectTimeout: 30000# 配置網絡超時時間socketTimeout: 60000# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一個連接在池中最小生存的時間,單位是毫秒minEvictableIdleTimeMillis: 300000# 配置一個連接在池中最大生存的時間,單位是毫秒maxEvictableIdleTimeMillis: 900000# 配置檢測連接是否有效validationQuery: SELECT 1 FROM DUAL# 申請連接的時候檢測,建議配置為true,不影響性能,并且保證安全性testWhileIdle: true# 獲取連接時執行檢測,建議關閉,影響性能testOnBorrow: false# 歸還連接時執行檢測,建議關閉,影響性能testOnReturn: false# 打開監控過濾器webStatFilter:enabled: true# 開啟后臺管理頁面statViewServlet:enabled: true# 設置白名單,不填則允許所有訪問allow:url-pattern: /druid/*# 控制臺管理用戶名和密碼login-username: ruoyilogin-password: 123456filter:stat:enabled: true# 慢SQL記錄log-slow-sql: trueslow-sql-millis: 1000merge-sql: truewall:config:multi-statement-allow: true
2、數據源代碼詳解
我們先查看ruoyi-framework模塊com.ruoyi.framework.config下的DruidConfig
package com.ruoyi.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.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.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.DruidProperties;
import com.ruoyi.framework.datasource.DynamicDataSource;/*** druid 配置多數據源* * @author ruoyi*/
@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")// spring.datasource.druid.slave.enabled=true時才創建該Bean@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定義名為"dynamicDataSource"的Bean,@Primary注解表示這是主數據源@Bean(name = "dynamicDataSource")@Primarypublic DynamicDataSource dataSource(DataSource masterDataSource){// 創建目標數據源Map,將主數據源(masterDataSource)放入,鍵為"MASTER"Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);// 調用setDataSource方法嘗試添加從數據源(slaveDataSource),鍵為"SLAVE"setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");// 返回一個新的DynamicDataSource實例,包含默認數據源和所有目標數據源return new DynamicDataSource(masterDataSource, targetDataSources);}/*** 設置數據源* * @param targetDataSources 備選數據源集合* @param sourceName 數據源名稱* @param beanName bean名稱*/public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName){try{DataSource dataSource = SpringUtils.getBean(beanName);targetDataSources.put(sourceName, dataSource);}catch (Exception e){}}/*** 去除監控頁面底部的廣告*/@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(){@Overridepublic void init(javax.servlet.FilterConfig filterConfig) throws ServletException{}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException{chain.doFilter(request, response);// 重置緩沖區,響應頭不會被重置response.resetBuffer();// 獲取common.jsString text = Utils.readFromResource(filePath);// 正則替換banner, 除去底部的廣告信息text = text.replaceAll("<a.*?banner\"></a><br/>", "");text = text.replaceAll("powered.*?shrek.wang</a>", "");response.getWriter().write(text);}@Overridepublic void destroy(){}};FilterRegistrationBean registrationBean = new FilterRegistrationBean();registrationBean.setFilter(filter);registrationBean.addUrlPatterns(commonJsPattern);return registrationBean;}
}
注意:Spring Boot 2.X 版本不再支持配置繼承,多數據源的話每個數據源的所有配置都需要單獨配置,否則配置不會生效
里面有一個去除druid后臺管理頁面底層廣告的方法,這里大致描述其流程
1.使用@Bean創建一個FilterRegistrationBean,當Druid監控頁面啟用時生效
2.定義一個匿名Filter,在請求common.js文件時進行攔截
3.在Filter中讀取common.js文件內容,使用正則表達式刪除廣告相關的HTML代碼
4.將處理后的內容寫入響應,從而實現去除廣告的效果
我們再看druid的配置屬性,獲取yml文件中druid的配置set進數據源并返回
package com.ruoyi.framework.config.properties;import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource;/*** druid 配置屬性* * @author ruoyi*/
@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.connectTimeout}")private int connectTimeout;@Value("${spring.datasource.druid.socketTimeout}")private int socketTimeout;@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.setConnectTimeout(connectTimeout);/** 配置網絡超時時間,等待數據庫操作完成的網絡超時時間,單位是毫秒 */datasource.setSocketTimeout(socketTimeout);/** 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 */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;}
}
3、從庫數據源使用
首先在application-druid.yml中進行配置,配置從庫數據源slave,將enabled設置為true并配置數據源鏈接
# 數據源配置
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverdruid:# 主庫數據源master:url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: password# 從庫數據源slave:# 從數據源開關/默認關閉enabled: trueurl: jdbc:mysql://localhost:3306/ry-vue1useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: password# 初始連接數initialSize: 5# 最小連接池數量minIdle: 10# 最大連接池數量maxActive: 20# 配置獲取連接等待超時的時間maxWait: 60000# 配置連接超時時間connectTimeout: 30000# 配置網絡超時時間socketTimeout: 60000# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一個連接在池中最小生存的時間,單位是毫秒minEvictableIdleTimeMillis: 300000# 配置一個連接在池中最大生存的時間,單位是毫秒maxEvictableIdleTimeMillis: 900000# 配置檢測連接是否有效validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsewebStatFilter:enabled: truestatViewServlet:enabled: true# 設置白名單,不填則允許所有訪問allow:url-pattern: /druid/*# 控制臺管理用戶名和密碼login-username: ruoyilogin-password: 123456filter:stat:enabled: true# 慢SQL記錄log-slow-sql: trueslow-sql-millis: 1000merge-sql: truewall:config:multi-statement-allow: true
在需要被切換數據源的Service或Mapper方法上添加@DataSource注解,使用方法如下:
@DataSource(value = DataSourceType.MASTER)
public List<...> select(...)
{return mapper.select(...);
}
其中value用來表示數據源名稱,除MASTER和SLAVE其他均需要進行配置。
4、多數據源
4.1 新增相同數據源
在application-druid.yml中新增slave2
# 從庫數據源slave2:# 從數據源開關/默認關閉enabled: trueurl: jdbc:mysql://localhost:3306/ry-vue2useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: password
在枚舉類DataSouceType中新增數據庫類型
package com.ruoyi.common.enums;/*** 數據源* * @author ruoyi*/
public enum DataSourceType
{/*** 主庫*/MASTER,/*** 從庫*/SLAVE,/*** 從庫2*/SLAVE2
}
在類DruidConfig中新增
@Bean
@ConfigurationProperties("spring.datasource.druid.slave2")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave2", name = "enabled", havingValue = "true")
public DataSource slaveDataSource2(DruidProperties druidProperties)
{DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return druidProperties.dataSource(dataSource);
}@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource)
{Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");setDataSource(targetDataSources, DataSourceType.SLAVE2.name(), "slaveDataSource2");return new DynamicDataSource(masterDataSource, targetDataSources);
}
4.2 新增不同數據源
如新增Oracle數據庫(此部分摘自ruoyi官方文檔)
新增Maven依賴
<!--oracle驅動-->
<dependency><groupId>com.oracle</groupId><artifactId>ojdbc6</artifactId><version>11.2.0.3</version>
</dependency>
數據源配置
# 數據源配置
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedruid:# 主庫數據源master:url: jdbc:mysql://127.0.0.1:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: passwordvalidationQuery: select 1# 從庫數據源slave:# 從數據源開關/默認關閉enabled: trueurl: jdbc:oracle:thin:@127.0.0.1:1521:oracleusername: rootpassword: passwordvalidationQuery: select 1 from dual
注意
對于不同數據源造成的驅動問題,可以刪除driverClassName,默認會自動識別驅動
如需要對不同數據源分頁需要操作application.yml中的pagehelper配置中刪除helperDialect: mysql會自動識別數據源,新增autoRuntimeDialect=true表示運行時獲取數據源
4.3 多數據源代碼原理
首先看ruoyi-framework模塊com.ruoyi.framework.aspectj下的DataSourceAspect類,處理多數據源切換的切面
package com.ruoyi.framework.aspectj;import java.util.Objects;
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.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder;/*** 多數據源處理* * @author ruoyi*/
@Aspect
// 確保在所有切面中第一個執行
@Order(1)
@Component
public class DataSourceAspect
{protected Logger logger = LoggerFactory.getLogger(getClass());// 如果在方法或類上有@DataSource注解@Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"+ "|| @within(com.ruoyi.common.annotation.DataSource)")public void dsPointCut(){}// 定義環繞通知 目標方法執行前后都可以執行自定義邏輯@Around("dsPointCut()")public Object around(ProceedingJoinPoint point) throws Throwable{// 獲取數據源DataSource dataSource = getDataSource(point);if (StringUtils.isNotNull(dataSource)){ // 將獲取的數據源set進多數據源上下文中DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());}try{ // 執行目標方法return point.proceed();}finally{// 銷毀數據源 在執行方法之后DynamicDataSourceContextHolder.clearDataSourceType();}}/*** 獲取需要切換的數據源*/public DataSource getDataSource(ProceedingJoinPoint point){MethodSignature signature = (MethodSignature) point.getSignature();DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);if (Objects.nonNull(dataSource)){return dataSource;}return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);}
}
其中 DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); 只是將獲取的數據源set進上下文中,而具體設置數據源是由DynamicDataSource來實現的
package com.ruoyi.framework.datasource;import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;/*** 動態數據源* * @author ruoyi*/
public class DynamicDataSource extends AbstractRoutingDataSource
{public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){super.setDefaultTargetDataSource(defaultTargetDataSource);super.setTargetDataSources(targetDataSources);super.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey(){return DynamicDataSourceContextHolder.getDataSourceType();}
}
DynamicDataSource在DruidConfig中被注冊到容器中,去切換數據源
@Bean(name = "dynamicDataSource")@Primarypublic DynamicDataSource dataSource(DataSource masterDataSource){// 創建目標數據源Map,將主數據源(masterDataSource)放入,鍵為"MASTER"Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);// 調用setDataSource方法嘗試添加從數據源(slaveDataSource),鍵為"SLAVE"setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");// 返回一個新的DynamicDataSource實例,包含默認數據源和所有目標數據源return new DynamicDataSource(masterDataSource, targetDataSources);}