目录
一、线程安全的数据源切换类(DataSourceSwitch.java)
二、多数据源类(MultiDataSource.java)
三、SpringBoot的数据源配置
四、使用过滤器在线程访问数据前设置线程数据源
在实际场景中,会遇到不同用户拥有不同的数据源,这些数据源信息配置在数据库表里面,需要我们根据用户切换成相应的数据源。在本文中,会介绍如何在SpringBoot + Mybatis中根据用户切换数据源的配置。
将ThreadLocal封装成设置、保存、获取和清空当前线程所属用户的数据源的工具类,具体代码如下:
import javax.sql.DataSource;
/**
* 当前线程数据源工具类
*
* @author hrc
* @date 2019年1月29日
*/
public class DataSourceSwitch {
/**
* 保存数据源线程安全容器
*/
private static final ThreadLocal dataSourceThreadLocal = new ThreadLocal();
/**
* 设置数据源
* @param dataSource 数据源
*/
public static void setDataSource (DataSource dataSource) {
dataSourceThreadLocal.set(dataSource);
}
/**
* 获取数据源
* @return
*/
public static DataSource getDataSource(){
return (DataSource) dataSourceThreadLocal.get();
}
/**
* 清空数据源
*/
public static void clearDataSource(){
dataSourceThreadLocal.remove();
}
}
多数据源类实现了DataSource接口,并且该类一个单例模式。在mybatis的sqlSessionFactory在创建一个sqlSession的时候会调用DataSource里的DataSource里的方法,并从中获取数据库连接。所以我们要实现多数据源,就只需要把多数据源的getDataSource()方法写成获取当前线程的数据源,并且把DataSource接口的方法改成getDataSource().xxx实现就行了。多数据源的具体实现代码如下:
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import javax.sql.DataSource;
/**
* 多数据源类
*
* @author hrc
* @date 2019年1月29日
*/
public class MultiDataSource implements DataSource {
private static MultiDataSource multiDataSource = null;
private DataSource dataSource = null;
private MultiDataSource() {};
public static MultiDataSource getInstance() {
if(multiDataSource == null) {
synchronized (MultiDataSource.class) {
if (multiDataSource == null) {
multiDataSource = new MultiDataSource();
}
}
}
return multiDataSource;
}
public DataSource getDataSource() {
DataSource dataSource = DataSourceSwitch.getDataSource();
if (dataSource == null) {
dataSource = this.dataSource;
}
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return getDataSource().getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
getDataSource().setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
getDataSource().setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return getDataSource().getLoginTimeout();
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return getDataSource().getParentLogger();
}
@Override
public T unwrap(Class iface) throws SQLException {
return getDataSource().unwrap(iface);
}
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
return getDataSource().isWrapperFor(iface);
}
@Override
public Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return getDataSource().getConnection(username, password);
}
}
在SpringBoot中手动配置数据源的具体代码如下:
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import com.alibaba.druid.pool.DruidDataSource;
import com.cheng.common.util.JdbcConfigUtil;
/**
* 数据源配置
*
* @author hrc
* @date 2018年10月9日
*/
@Configuration
public class DataSourceConfig {
@Autowired
private Environment env;
/**
* 为多数据源创建默认的数据源
* @return
*/
public DataSource dataSource() {
MultiDataSource multiDataSource = MultiDataSource.getInstance();
String driverClassName = JdbcConfigUtil.getValue("jdbc.default.driverClassName");
String url = JdbcConfigUtil.getValue("jdbc.default.url");
String username = JdbcConfigUtil.getValue("jdbc.default.username");
String password = JdbcConfigUtil.getValue("jdbc.default.password");
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
multiDataSource.setDataSource(dataSource);
return multiDataSource;
}
@Primary
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(this.dataSource());
sessionFactory.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations")));
return sessionFactory.getObject();
}
@Bean("transactionManager")
public DataSourceTransactionManager transactionManager() throws Exception {
return new DataSourceTransactionManager(this.dataSource());
}
}
该多数源的设计时需要在线程访问数据之前将当前线程的数据源设置成当前用户相应的数据源,所以这就需要用到过滤器(Filter)了。使用过滤器对相应的请求进行拦截,在其到达controller层之前,调用DataSourceSwitch.setDataSource(dataSource)来设置当前线程的数据源。我写的一个应用多数据源的过滤器例子,代码如下:
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.fastjson.JSON;
import com.cheng.common.datasourse.DataSourceSwitch;
import com.cheng.common.util.JdbcConfigUtil;
import com.cheng.common.util.ResMapBuilder;
import com.cheng.common.util.Util;
import com.cheng.system.dto.User;
import com.cheng.system.service.UserService;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 基础过滤器
*
* @author hrc
* @date 2018年12月7日
*/
public class BaseFilter implements Filter {
private UserService userService;
private final static Set EXCLUDE_PATTERN = new HashSet();
private final static HashMap DATA_SOURCE_MAP = new HashMap(50);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
EXCLUDE_PATTERN.add("hanaAuth.action");
/*
*初始化时,将数据源加载到缓存中
*/
ServletContext sc = filterConfig.getServletContext();
WebApplicationContext cxt = WebApplicationContextUtils.getWebApplicationContext(sc);
if (cxt != null && cxt.getBean(UserService.class) != null && userService == null) {
userService = (UserService) cxt.getBean(UserService.class);
}
List userList = userService.getAcctInfos("HANA");
if (!Util.isEmpty(userList)) {
String username = "";
String password = "";
for (User user : userList) {
username = user.getAcctCode();
password = user.getAcctPwd();
DataSource dataSource = configDataSource(username, password);
DATA_SOURCE_MAP.put(username, dataSource);
}
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 允许ajax跨域的参数设置
String origin = httpServletRequest.getHeader("Origin");
httpServletResponse.setHeader("Access-Control-Allow-Origin", origin);
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "X-Requested-With, accept, content-type");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH");
String path = httpServletRequest.getRequestURI();
if (isExcludePattern(path)) {
filterChain.doFilter(request, response);
return;
}
// 进行登录拦截
User user = null;
Object obj = httpServletRequest.getSession().getAttribute("user");
if (obj instanceof User) {
user = (User) obj;
}
if (user == null) {
httpServletResponse.setContentType("application/json; charset=utf-8");
httpServletResponse.setHeader("cache-control", "no-cache");
String msg = JSON.toJSONString(new ResMapBuilder().code(403).status(0).msg("未检测到登录状态,请登录").build());
PrintWriter out = httpServletResponse.getWriter();
out.println(msg);
out.flush();
out.close();
return;
}
String uamCode = user.getUamCode();
String username = user.getAcctCode();
if (DATA_SOURCE_MAP.keySet().contains(username)) {
DataSourceSwitch.setDataSource(DATA_SOURCE_MAP.get(username));
} else {
User acctInfo = userService.getAcctInfo(uamCode, "HANA");
String password = acctInfo.getAcctPwd();
DataSource dataSource = configDataSource(username, password);
DataSourceSwitch.setDataSource(dataSource);
DATA_SOURCE_MAP.put(username, dataSource);
}
filterChain.doFilter(request, response);
// 清楚当前线程的数据源
DataSourceSwitch.clearDataSource();
}
@Override
public void destroy() {
}
/**
* 是否是过滤的URL
*
* @param url
* @return
*/
private boolean isExcludePattern(String url) {
boolean isExcludePattern = false;
if (Util.isEmpty(url)) {
return isExcludePattern;
}
for (String str : EXCLUDE_PATTERN) {
if (url.endsWith(str)) {
isExcludePattern = true;
break;
}
}
return isExcludePattern;
}
/**
* 配置数据源
* @param username 用户名
* @param password 密码
* @return
*/
private DataSource configDataSource(String username, String password) {
/*
* 数据源配置参数
*/
String driverClassName = JdbcConfigUtil.getValue("jdbc.driverClassName");
String url = JdbcConfigUtil.getValue("jdbc.url");
String initialSize = JdbcConfigUtil.getValue("jdbc.initialSize");
String minIdle = JdbcConfigUtil.getValue("jdbc.minIdle");
String maxActive = JdbcConfigUtil.getValue("jdbc.maxActive");
String maxWait = JdbcConfigUtil.getValue("jdbc.maxWait");
String validationQuery = JdbcConfigUtil.getValue("jdbc.validationQuery");
String testOnBorrow = JdbcConfigUtil.getValue("jdbc.testOnBorrow");
/*
* 创建数据源对象
*/
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setInitialSize(Util.isEmpty(initialSize) ? 5 : Integer.valueOf(initialSize));
dataSource.setMinIdle(Util.isEmpty(minIdle) ? 5 : Integer.valueOf(minIdle));
dataSource.setMaxActive(Util.isEmpty(maxActive) ? 20 : Integer.valueOf(maxActive));
dataSource.setMaxWait(Util.isEmpty(maxWait) ? 60000 : Long.valueOf(maxWait));
if (!Util.isEmpty(validationQuery)) {
dataSource.setValidationQuery(validationQuery);
dataSource.setTestOnBorrow(Util.isEmpty(testOnBorrow) ? false : Boolean.valueOf(testOnBorrow));
}
return dataSource;
}
}