Spring Boot学习(二):mybatis + druid + 多数据源自动切换

目录

一、简介

二、环境准备

三、代码改造

四、注意事项

五、参考资料



一、简介

闲言碎语不多说:项目中要用到多数据源分别管理数据,主数据源存储正式数据,从数据源存储预加载的数据并完成预校验。

二、环境准备

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

扫描二维码关注公众号,回复: 2409555 查看本文章
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注解方法与调用方法不在同一个类中,否则切片不起作用

五、参考资料

猜你喜欢

转载自blog.csdn.net/xxfamly/article/details/81082415
今日推荐