目录
一、简介
闲言碎语不多说:项目中要用到多数据源分别管理数据,主数据源存储正式数据,从数据源存储预加载的数据并完成预校验。
二、环境准备
eclipse + maven + Spring Boot + mybatis + oracle
三、代码改造
pom.xml文件中添加依赖
<!--alibaba连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.5</version>
</dependency>
application.properties文件中主从数据源配置
#====================================================================================
# 数据源配置
#====================================================================================
#数据源基础配置信息==============
datasource.base.poolPreparedStatements = false
datasource.base.type=com.alibaba.druid.pool.DruidDataSource
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
datasource.base.timeBetweenEvictionRunsMillis = 60000
#配置一个连接在池中最小生存的时间,单位是毫秒
datasource.base.minEvictableIdleTimeMillis = 30000
#检测查询
datasource.base.validationQuery = select 'x' FROM DUAL
datasource.base.testWhileIdle = true
datasource.base.testOnBorrow = false
datasource.base.testOnReturn = false
datasource.base.maxPoolPreparedStatementPerConnectionSize = 20
#配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
datasource.base.filters = stat,wall,slf4j
#主数据源==============================
#数据库地址
datasource.master.jdbcUrl=jdbc:oracle:thin:@192.168.10.111:1521:orcl
#数据库用户名
datasource.master.username=user1
#数据库密码
datasource.master.password=user1
#数据库驱动器
datasource.master.driver-class-name=oracle.jdbc.OracleDriver
#最大空闲连接: 连接池中容许保持空闲状态的最大连接数量,超过的空闲连接将被释放, 如果设置为负数表示不限制
datasource.master.max-idle=10
#最小空闲连接: 连接池中容许保持空闲状态的最小连接数量, 低于这个数量将创建新的连接, 如果设置为0 则不创建
datasource.master.min-idle=10
#最大等待时间: 当没有可用连接时, 连接池等待连接被归还的最大时间( 以毫秒计数), 超过时间则抛出异常, 如果设置为-1 表示无限等待
datasource.master.max-wait=10000
#连接池最大使用连接数量
datasource.master.max-active=300
#初始化连接: 连接池启动时创建的初始化连接数量
datasource.master.initial-size=50
#其它数据源==============================
#数据库地址
datasource.slave.jdbcUrl=jdbc:oracle:thin:@192.168.10.112:1521:orcl
#数据库用户名
datasource.slave.username=user2
#数据库密码
datasource.slave.password=user2
#数据库驱动器
datasource.slave.driver-class-name=oracle.jdbc.OracleDriver
#最小空闲连接: 连接池中容许保持空闲状态的最小连接数量, 低于这个数量将创建新的连接, 如果设置为0 则不创建
datasource.slave.min-idle=10
#最大等待时间: 当没有可用连接时, 连接池等待连接被归还的最大时间( 以毫秒计数), 超过时间则抛出异常, 如果设置为-1 表示无限等待
datasource.slave.max-wait=10000
#连接池最大使用连接数量
datasource.slave.max-active=300
#初始化连接: 连接池启动时创建的初始化连接数量
datasource.slave.initial-size=5
定义主从数据源枚举类DataSourceType.java
package service.db.datasource;
public enum DataSourceType {
// 主库
Master("master"),
// 从库
Slave("slave");
private String dSName;
private DataSourceType(String dSName) {
this.dSName = dSName;
}
public String getDsName() {
return dSName;
}
public void setDsName(String dSName) {
this.dSName = dSName;
}
}
读取application.properties中的配置信息,初始化主从数据源DynamicDataSourceRegister.java
package service.db.datasource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import com.alibaba.druid.pool.DruidDataSource;
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 数据源公有属性-------------------------
private String type;
private long timeBetweenEvictionRunsMillis;
private long minEvictableIdleTimeMillis;
private String validationQuery;
private boolean testWhileIdle;
private boolean testOnBorrow;
private boolean testOnReturn;
private boolean poolPreparedStatements;
private int maxPoolPreparedStatementPerConnectionSize;
private String filters;
private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";
private ConversionService conversionService = new DefaultConversionService();
private PropertyValues dataSourcePropertyValues;
// 默认数据源
private DataSource defaultDataSource;
private Map<String, DataSource> slaveDataSources = new HashMap<String, DataSource>();
/**
* 加载多数据源配置
*/
@Override
public void setEnvironment(Environment environment) {
// 加载基本配置
initBaseProperties(environment);
// 加载主数据源配置
initDefaultDataSource(environment);
// 加载从数据源配置
initslaveDataSources(environment);
}
/**
* 读取数据源基本配置.
*
* @param env
*/
private void initBaseProperties(Environment env) {
logger.info("读取数据源基本配置");
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "datasource.base.");
if (propertyResolver.containsProperty("type")) {
type = propertyResolver.getProperty("type");
}
if (propertyResolver.containsProperty("timeBetweenEvictionRunsMillis")) {
timeBetweenEvictionRunsMillis = Long.parseLong(propertyResolver.getProperty("timeBetweenEvictionRunsMillis"));
}
if (propertyResolver.containsProperty("minEvictableIdleTimeMillis")) {
minEvictableIdleTimeMillis = Long.parseLong(propertyResolver.getProperty("minEvictableIdleTimeMillis"));
}
if (propertyResolver.containsProperty("validationQuery")) {
validationQuery = propertyResolver.getProperty("validationQuery");
}
if (propertyResolver.containsProperty("testWhileIdle")) {
testWhileIdle = Boolean.parseBoolean(propertyResolver.getProperty("testWhileIdle"));
}
if (propertyResolver.containsProperty("testOnBorrow")) {
testOnBorrow = Boolean.parseBoolean(propertyResolver.getProperty("testOnBorrow"));
}
if (propertyResolver.containsProperty("testOnReturn")) {
testOnReturn = Boolean.parseBoolean(propertyResolver.getProperty("testOnReturn"));
}
if (propertyResolver.containsProperty("poolPreparedStatements")) {
poolPreparedStatements = Boolean.parseBoolean(propertyResolver.getProperty("poolPreparedStatements"));
}
if (propertyResolver.containsProperty("maxPoolPreparedStatementPerConnectionSize")) {
maxPoolPreparedStatementPerConnectionSize = Integer.parseInt(propertyResolver.getProperty("maxPoolPreparedStatementPerConnectionSize"));
}
if (propertyResolver.containsProperty("filters")) {
filters = propertyResolver.getProperty("filters");
}
}
/**
* @方法: initDefaultDataSource
* @描述: 初始化主数据源
* @返回: void
*/
private void initDefaultDataSource(Environment env) {
logger.info("读取主数据源配置");
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "datasource.master.");
Map<String, Object> dsMap = new HashMap<String, Object>();
dsMap.put("driverClassName", propertyResolver.getProperty("driver-class-name"));
dsMap.put("url", propertyResolver.getProperty("jdbcUrl"));
dsMap.put("username", propertyResolver.getProperty("username"));
dsMap.put("password", propertyResolver.getProperty("password"));
dsMap.put("initialSize", propertyResolver.getProperty("initial-size"));
dsMap.put("minIdle", propertyResolver.getProperty("min-idle"));
dsMap.put("maxActive", propertyResolver.getProperty("max-active"));
dsMap.put("maxWait", propertyResolver.getProperty("max-wait"));
logger.info("创建默认(主)数据源");
// 创建数据源
defaultDataSource = buildDataSource(dsMap);
dataBinder(defaultDataSource, env);
}
/**
* @方法: initslaveDataSources
* @描述: 初始化从数据源
* @返回: void
*/
private void initslaveDataSources(Environment env) {
logger.info("读取从数据源配置");
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "datasource.slave.");
Map<String, Object> dsMap = new HashMap<String, Object>();
dsMap.put("driverClassName", propertyResolver.getProperty("driver-class-name"));
dsMap.put("url", propertyResolver.getProperty("jdbcUrl"));
dsMap.put("username", propertyResolver.getProperty("username"));
dsMap.put("password", propertyResolver.getProperty("password"));
dsMap.put("initialSize", propertyResolver.getProperty("initial-size"));
dsMap.put("minIdle", propertyResolver.getProperty("min-idle"));
dsMap.put("maxActive", propertyResolver.getProperty("max-active"));
dsMap.put("maxWait", propertyResolver.getProperty("max-wait"));
logger.info("创建从数据源");
DataSource slaveDataSource = buildDataSource(dsMap);
slaveDataSources.put(DataSourceType.Slave.getDsName(), slaveDataSource);
dataBinder(slaveDataSource, env);
}
/**
* 创建datasource.
*
* @param dsMap
* @return
*/
@SuppressWarnings("unchecked")
public DataSource buildDataSource(Map<String, Object> dsMap) {
if (type == null) {
type = (String) DATASOURCE_TYPE_DEFAULT;// 默认DataSource
}
Class<? extends DataSource> dataSourceType;
try {
dataSourceType = (Class<? extends DataSource>) Class.forName(type);
String driverClassName = dsMap.get("driverClassName").toString();
String url = dsMap.get("url").toString();
String username = dsMap.get("username").toString();
String password = dsMap.get("password").toString();
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driverClassName);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
druidDataSource.setInitialSize(Integer.parseInt(dsMap.get("initialSize").toString()));
druidDataSource.setMinIdle(Integer.parseInt(dsMap.get("minIdle").toString()));
druidDataSource.setMaxActive(Integer.parseInt(dsMap.get("maxActive").toString()));
druidDataSource.setMaxWait(Integer.parseInt(dsMap.get("maxWait").toString()));
druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
druidDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
druidDataSource.setValidationQuery(validationQuery);
druidDataSource.setTestWhileIdle(testWhileIdle);
druidDataSource.setTestOnBorrow(testOnBorrow);
druidDataSource.setTestOnReturn(testOnReturn);
druidDataSource.setPoolPreparedStatements(poolPreparedStatements);
druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
druidDataSource.setFilters(filters);
druidDataSource.init();
} catch (SQLException e) {
e.printStackTrace();
}
return druidDataSource;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* 为DataSource绑定更多数据
*
* @param dataSource
* @param env
*/
private void dataBinder(DataSource dataSource, Environment env) {
RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource);
dataBinder.setConversionService(conversionService);
dataBinder.setIgnoreNestedProperties(false);// false
dataBinder.setIgnoreInvalidFields(false);// false
dataBinder.setIgnoreUnknownFields(true);// true
if (dataSourcePropertyValues == null) {
Map<String, Object> rpr = new RelaxedPropertyResolver(env, "datasource.base").getSubProperties(".");
Map<String, Object> values = new HashMap<>(rpr);
// 排除已经设置的属性
values.remove("type");
values.remove("driverClassName");
values.remove("url");
values.remove("username");
values.remove("password");
dataSourcePropertyValues = new MutablePropertyValues(values);
}
dataBinder.bind(dataSourcePropertyValues);
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
// 将主数据源添加到更多数据源中
targetDataSources.put(DataSourceType.Master.getDsName(), defaultDataSource);
DynamicDataSourceContextHolder.dsNames.add(DataSourceType.Master.getDsName());
// 添加更多数据源
targetDataSources.putAll(slaveDataSources);
for (String key : slaveDataSources.keySet()) {
DynamicDataSourceContextHolder.dsNames.add(key);
}
// 创建DynamicDataSource
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
// 添加属性:AbstractRoutingDataSource.defaultTargetDataSource
mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
mpv.addPropertyValue("targetDataSources", targetDataSources);
registry.registerBeanDefinition("dataSource", beanDefinition);
}
}
路由类DynamicDataSourceContextHolder.java
package service.db.datasource;
import java.util.ArrayList;
import java.util.List;
public class DynamicDataSourceContextHolder {
/*
* 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/*
* 管理所有的数据源id; 主要是为了判断数据源是否存在;
*/
public static List<String> dsNames = new ArrayList<String>();
/**
* @方法: setDsName
* @描述: 设置当前的数据源
* @返回: void
*/
public static void setDsName(String dsName) {
contextHolder.set(dsName);
}
public static String getDsName() {
return contextHolder.get();
}
public static void clearDsName() {
contextHolder.remove();
}
/**
* 判断指定DataSrouce当前是否存在
*/
public static boolean containsDataSource(String dsName) {
return dsNames.contains(dsName);
}
}
自定义注解类TargetDataSource.java
package service.db.datasource;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
DataSourceType value();
}
定义切面类DynamicDataSourceAspect.java,配合自定义注解使用,达到动态切换数据源的终极目标。这里有个切面知识点,不清楚的朋友百度之。
package service.db.datasource;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Order(-10)//保证该AOP在@Transactional之前执行
@Component
public class DynamicDataSourceAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/*
* @Before("@annotation(ds)") 的意思是:
*
* @Before:在方法执行之前进行执行:
*
* @annotation(targetDataSource): 会拦截注解targetDataSource的方法,否则不拦截;
*/
@Before("@annotation(targetDataSource)")
public void changeDataSource(JoinPoint point,
TargetDataSource targetDataSource) throws Throwable {
// 获取当前的指定的数据源;
String dsName = targetDataSource.value().getDsName();
// 如果不在我们注入的所有的数据源范围之内,那么输出警告信息,系统自动使用默认的数据源。
if (!DynamicDataSourceContextHolder.containsDataSource(dsName)) {
logger.error("目标数据源[{" + targetDataSource.value() + "}]不存在,使用默认数据源 > " + point.getSignature());
} else {
logger.info("切换到数据源[{" + targetDataSource.value() + "}]>" + point.getSignature());
// 找到的话,那么设置到动态数据源上下文中。
DynamicDataSourceContextHolder.setDsName(targetDataSource.value().getDsName());
}
}
@After("@annotation(targetDataSource)")
public void restoreDataSource(JoinPoint point,
TargetDataSource targetDataSource) {
logger.info("回收当前数据源[{" + targetDataSource.value() + "}]>" + point.getSignature());
// 方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。
DynamicDataSourceContextHolder.clearDsName();
}
}
启动类调整ServerApplication.java
主要加入两句话,第一个是关闭spring boot自带数据源,第二个是导入自定义的数据源注册类
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
@Import({DynamicDataSourceRegister.class})
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
import service.db.datasource.DynamicDataSourceRegister;
@Configuration
@EnableCaching
@EnableWebMvc
@EnableScheduling
@EnableAutoConfiguration
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
@Import({DynamicDataSourceRegister.class})
public class ServerApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
application.web(true);
return application.sources(ServerApplication.class);
}
@Bean
public FilterRegistrationBean resourceFilter() {
FilterRegistrationBean filter = new FilterRegistrationBean();
filter.setFilter(new ResourceUrlEncodingFilter());
filter.addUrlPatterns("/*");
return filter;
}
}
以下是调用类,展示如何使用以上的一系列改造。
单独定义接口BusiDataService.java,使用注解的方法必须是接口公开方法。
package service.db;
/**
* @类名: BusiDataService
* @描述: 业务数据处理服务
*/
public interface BusiDataService {
/**
* @方法: selectOneFromTemp
* @描述: 从临时库查询1条业务数据
* @返回: String
*/
public String selectOneFromTemp();
}
编写接口实现类BusiDataServiceImpl.java
package service.db.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import service.db.BusiDataService;
import service.db.datasource.DataSourceType;
import service.db.datasource.TargetDataSource;
@Service
public class BusiDataServiceImpl implements BusiDataService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
@TargetDataSource(DataSourceType.Slave)
public String selectOneFromTemp() {
String ret = "从临时库查询数据";
logger.debug(ret);
return ret;
}
}
编写调用接口TestDataSource.java
package service.db;
/**
* @类名: TestDataSource
* @描述: 测试接口
*/
public interface TestDataSource {
public void test();
}
编写调用实现类TestDataSourceImpl.java
package service.db.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import service.db.BusiDataService;
import service.db.TestDataSource;
/**
* @类名: TestDataSourceImpl
* @描述: 测试实现类
*/
@Service
public class TestDataSourceImpl implements TestDataSource {
@Autowired
BusiDataService busiDataService;
@Override
public void test() {
//调用查询方法
busiDataService.selectOneFromTemp();
}
}
四、注意事项
1.@TargetDataSource必须应用于接口方法上,否则切片不起作用
2.@TargetDataSource注解方法与调用方法不在同一个类中,否则切片不起作用