我这里是为了实现一个业务场景。Spring如何配置多数据源并整合Druid的方式网上已经一大片了。
但是那种方式适应场景是采用主从模式的数据库。
(做法)我们需要事先在配置文件配置好服务需要用到的多个数据源信息,并创建自定义注解,这里注解一般声明在Service层的方法上较为合理。然后不同的Service根据注解的value属性区分方法所用到的数据源
这里,我实现的是在项目运行时,根据参数传递或其他途径获取到数据库连接配置信息,进行动态的连接创建,切换,和销毁
pom依赖:
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Druid 数据源配置:
server:
port: 8080
spring:
profiles:
active: dev
datasource:
druid:
name: druid-default-datasource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/qb?characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: root
initial-size: 5
max-active: 30
min-idle: 5
filter:
stat:
enabled: true
log-slow-sql: true
merge-sql: true
slow-sql-millis: 30000 #单位毫秒
connection-stack-trace-enable: true
config:
enabled: true
slf4j:
enabled: true
query-timeout: 1800
max-wait: 60000
validation-query: select 1 from dual
pool-prepared-statements: true #mysql5.5以上或者oracle建议开启
testWhileIdle: true
filters: stat,wall,config,slf4j
break-after-acquire-failure: true
time-between-eviction-runs-millis: 300000
connection-error-retry-attempts: 3
stat-view-servlet:
enabled: true #默认为true,可以不显示设置
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: 123
deny: 192.168.0.191
web-stat-filter:
enabled: true #默认为true,可以不显示设置
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
session-stat-enable: 'true'
profile-enable: 'true'
#-1标识采用数据库默认事务隔离级别
default-transaction-isolation: 2
aop-patterns: com.landa.service.*
aop:
proxy-target-class: true
#proxy-target-class属性值决定是基于接口的还是基于类的代理被创建。如果proxy-target-class 属性值被设置为true,那么基于类的代理将起作用(这时需要cglib库)。如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用。
#即使你未声明 proxy-target-class="true" ,但运行类没有继承接口,spring也会自动使用CGLIB代理。
#高版本spring自动根据运行类选择 JDK 或 CGLIB 代理
#mybatis配置
mybatis:
configuration:
lazy-loading-enabled: true
aggressive-lazy-loading: true
cache-enabled: true
call-setters-on-nulls: true
jdbc-type-for-null: NULL
mapper-locations: class:mybatis/mapper/**/*.xml
#PageHelper分页插件配置
pagehelper:
helper-dialect: mysql
reasonable: 'true'
# 用于控制默认不带 count 查询的方法中,是否执行 count 查询,这里设置为true后,total会为-1
auto-dialect: 'true'
page-size-zero: 'true'
default-count: 'false'
close-conn: 'true'
auto-runtime-dialect: 'false'
offset-as-page-num: 'false'
row-bounds-with-count: 'true'
上面是我写demo时yml的完整配置。
yml中的框架插件配置都可以用java配置来替代的,这里为了直观便将所有配置放在一起方便大家理解。
这时候,基础的东西已经配置好了
声明自定义注解:
package com.landa.annotation;
import java.lang.annotation.*;
/**
* @Author: save
* @Date: 2020/5/27 10:16
* @Version: 1.0
**/
@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
}
数据源初始化配置
package com.landa.common;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
/**
* @Author: save
* @Date: 2020/5/27 12:02
*/
@Configuration
public class DynamicDataSourceConfig {
/**
* @Author: save
* @Description: 构建默认数据源
* @Date: 2020/5/27 14:22
* @return: javax.sql.DataSource
**/
@Bean(name = "defaultDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource defaultDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 核心动态数据源
* @return 数据源实例
*/
@Bean
public DataSource dynamicDataSource() {
DynamicDataSourceHandler dataSource = new DynamicDataSourceHandler();
//dataSource.setDefaultTargetDataSource(defaultDataSource());
dataSource.setTargetDataSources(new HashMap<>());
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
//此处设置为了解决找不到mapper文件的问题
//sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().
// getResources("classpath:mybatis/mapper/**/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
/**
* 事务管理
* @return 事务管理实例
*/
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}
创建动态数据源管理类(继承AbstractRoutingDataSource数据源扩展类)
package com.landa.common;
import com.landa.util.SpringUtil;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
/**
* 继承数据源扩展类
* @author Administrator
*
*/
public class DynamicDataSourceHandler extends AbstractRoutingDataSource {
//初始化主数据源到ThreadLocal中
private static final ThreadLocal<DataSource> DATASOURCE = ThreadLocal.withInitial(() -> (DataSource) SpringUtil.getBean("defaultDataSource"));
@Override
protected Object determineCurrentLookupKey() {
return null;
}
@Override
protected DataSource determineTargetDataSource() {
return getDataSource();
}
public static DataSource getDataSource() {
return DynamicDataSourceHandler.DATASOURCE.get();
}
public static void setDataSource(DataSource dataSource) {
DynamicDataSourceHandler.DATASOURCE.set(dataSource);
}
public static void clear() {
DynamicDataSourceHandler.DATASOURCE.remove();
}
}
自定义切面类
package com.landa.aspect;
import com.landa.annotation.DataSource;
import com.landa.common.DynamicDataSourceHandler;
import com.landa.util.DruidUtil;
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.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
/**
* @Author: save
* @Date: 2020/5/27 10:19
* @Version: 1.0
**/
@Aspect
@Order(-1)
@Component
public class DataSourceAspect {
/**
* @Aspect:作用是把当前类标识为一个切面供容器读取
* @Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
* @Around:环绕增强,相当于MethodInterceptor
* @AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
* @Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
* @AfterThrowing:异常抛出增强,相当于ThrowsAdvice
* @After: final增强,不管是抛出异常或者正常退出都会执行
**/
@Autowired
private DruidUtil druidUtil;
@Before("execution(* com.landa.service.*.*(..))")
public void beforeServiceMethodExecute(JoinPoint point){
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Method targetMethod = methodSignature.getMethod();
Parameter[] parameters= targetMethod.getParameters();
Object[] args=point.getArgs();
if(null==parameters||parameters.length==0){
return;
}
for (int i = 0; i < parameters.length; i++) {
if(parameters[i].isAnnotationPresent(DataSource.class)){
String dataSourceId=args[i].toString();
if(null==dataSourceId||dataSourceId.length()==0){
throw new RuntimeException("数据源切换失败");
}
druidUtil.switchDB(dataSourceId);
}
}
}
/**
* @Author: save
* @Description: 方法执行完成,清除数据源
* @Date: 2020/5/27 11:13
* @Param: point【接入点】
* @return: void
**/
@After("execution(* com.landa.service.*.*(..))")
public void afterServiceMethodExecute(JoinPoint point){
DynamicDataSourceHandler.clear();
}
}
注意:我这里的@DataSource注解设置的作用域是在方法参数上。因为实际业务中数据源Id由其他服务发送接口请求时传过来,然后数据源服务定义切面切入需要切库的Service(这里切入点可以视自己的实际业务而定)。切面中切入到目标类的方法,判断方法参数中是否存在由@DataSource注解标注的参数。如果存在,则拿到该参数值(也就是数据源id)去数据源主库查询数据源配置信息(如连接数据库需要的url,driver,username,password等等),
下面假设DataSourceModel为获取到的数据库配置信息
package com.landa.model;
import lombok.Data;
/**
* @Author: save
* @Date: 2020/5/27 11:38
* @Version: 1.0
**/
@Data
public class DataSourceModel {
private String username;
private String password;
private String url;
private String driverClassName;
public DataSourceModel(String username, String password, String url, String driverClassName) {
this.username = username;
this.password = password;
this.url = url;
this.driverClassName = driverClassName;
}
}
创建数据源的简单demo
package com.landa.util;
import com.alibaba.druid.pool.DruidDataSource;
import com.landa.common.DynamicDataSourceHandler;
import com.landa.model.DataSourceModel;
import org.springframework.stereotype.Component;
/**
* @Description:
* @Author: save
* @Date: 2020/5/27 14:05
* @Version: 1.0
**/
@Component
public class DruidUtil {
public void switchDB(String dataSourceId){
//该Model的数据实际中应该是传参过来或去数据库查表
DataSourceModel dataSourceModel=new DataSourceModel("root","root","jdbc:mysql://localhost:3306/qb_qbbs?characterEncoding=utf8&serverTimezone=GMT%2B8","com.mysql.cj.jdbc.Driver");
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(dataSourceModel.getDriverClassName());
dataSource.setUrl(dataSourceModel.getUrl());
dataSource.setUsername(dataSourceModel.getUsername());
dataSource.setPassword(dataSourceModel.getPassword());
DynamicDataSourceHandler.setDataSource(dataSource);
}
}
再回到切面类DataSourceAspect中看Before,After
druidUtil.switchDB(datasourceId)方法根据切点拿到的数据源id,创建数据库连接,并通过
进行切库操作。
然后在方法执行完成
after中执行数据源清除操作。