java实现多租户_spring boot 项目中多租户的实现

项目背景

项目中SaaS服务需要提供多租户基础功能,通过访问域名区分不同的客户,进而隔离数据源,即一个租户一个数据库。

AbstractRoutingDataSource

spring中对切换数据源提供了动态设置方法,通过determineCurrentLookupKey()设置值切换对应数据源。

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 源码的介绍:

af82a23f44c5

基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,但是注意无法动态的增加数据源,只能在项目启动时加载。

实现逻辑

定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。

启动时加载多个数据源,并配置到AbstractRoutingDataSource的defaultTargetDataSource和targetDataSources中。

对所有controller方法做aop,根据当前域名或者前端设置值修改本地线程动态数据源名称。

新建类 DynamicDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**

* 动态获取DataSource

*

* @author plsy

*/

public class DynamicDataSource extends AbstractRoutingDataSource {

@Override

protected Object determineCurrentLookupKey() {

return DynamicDataSourceContextHolder.getDataSource();

}

}

通过基于本地线程的上下文管理来切换数据源

import org.apache.log4j.Logger;

import java.util.LinkedList;

/**

* 动态数据源上下文管理

*

* @author plsy

*/

public class DynamicDataSourceContextHolder {

static Logger logger = Logger.getLogger(DynamicDataSourceContextHolder.class);

/**

* 存放当前线程使用的数据源类型信息

*/

private static final ThreadLocal contextHolder = new ThreadLocal();

/**

* 数据源使用顺序标识

*/

public static LinkedList dataSourceIds = new LinkedList<>();

/**

* 设置数据源

*

* @param dataSourceName

*/

public static void setDataSource(String dataSourceName) {

contextHolder.set(dataSourceName);

dataSourceIds.add(dataSourceName);

}

/**

* 获取数据源

*/

public static String getDataSource() {

if (contextHolder.get() == null) {

logger.debug("数据源标识为空,使用默认的数据源");

} else {

logger.debug("使用数据源:" + contextHolder.get() + " 如果数据源不存在将使用默认数据源.");

}

return contextHolder.get();

}

/**

* 清除数据源

*/

public static void clearDataSource() {

contextHolder.remove();

dataSourceIds.clear();

}

/**

* 返回上一次使用的数据源

*

* @return

*/

public static void returnDataSource() {

dataSourceIds.removeLast();

setDataSource(dataSourceIds.getLast());

}

}

初始化数据源

package com.gsoft.cos3.datasource;

import com.zaxxer.hikari.util.DriverDataSource;

import org.apache.log4j.Logger;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.cloud.context.config.annotation.RefreshScope;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import java.util.Properties;

@Configuration

public class DataSourceConfig {

private Logger logger = Logger.getLogger(DataSourceConfig.class);

/**

* 自定义数据源

*/

public Map customDataSources = new HashMap<>();

@Value("${spring.datasource.url}")

private String url;

@Value("${spring.datasource.username}")

private String username;

@Value("${spring.datasource.password}")

private String password;

@Value("${spring.datasource.driver-class-name}")

private String driver;

private static final List custNames = new ArrayList<>();

@Primary

@RefreshScope

@Bean(name = "datasource")

public DynamicDataSource dynamicDataSource() {

logger.info("=====初始化动态数据源=====");

Properties properties = new Properties();

DataSource dataSource = new DriverDataSource(url, driver, properties, username, password);

DynamicDataSource dynamicDataSource = new DynamicDataSource();

// 默认数据源

dynamicDataSource.setDefaultTargetDataSource(dataSource);

// 配置多数据源

Map dsMap = new HashMap();

Map customDataSources = new HashMap<>();

Connection connection = null;

try {

connection = dataSource.getConnection();

PreparedStatement preparedStatement = connection.prepareStatement("select C_CODE,C_DSURL,C_DSDRIVERCLASSNAME,C_DSUSERNAME,C_DSPASSWORD from COS_SAAS_CUSTOMER");

ResultSet resultSet = preparedStatement.executeQuery();

while (resultSet.next()) {

String name = resultSet.getString(1);

custNames.add(name);

dsMap.put("url", resultSet.getString(2));

dsMap.put("driver", resultSet.getString(3));

dsMap.put("username", resultSet.getString(4));

dsMap.put("password", resultSet.getString(5));

DataSource ds = buildDataSource(dsMap);

customDataSources.put(name, ds);

}

} catch (SQLException e) {

e.printStackTrace();

}

dynamicDataSource.setTargetDataSources(customDataSources);

this.customDataSources = customDataSources;

logger.info("已加载租户库数据源" + custNames);

return dynamicDataSource;

}

@Primary

@Bean(name = "jdbcTemplate")

public JdbcTemplate jdbcTemplate(@Qualifier("datasource") DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

public static DataSource buildDataSource(Map dataSourceMap) {

String driverClassName = dataSourceMap.get("driver").toString();

String url = dataSourceMap.get("url").toString();

String username = dataSourceMap.get("username").toString();

String password = dataSourceMap.get("password").toString();

// hikari配置参数 目前使用默认设置

Properties properties = new Properties();

DriverDataSource driverDataSource = new DriverDataSource(url, driverClassName, properties, username, password);

return driverDataSource;

}

}

AOP

import com.gsoft.cos3.util.Assert;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.aspectj.lang.annotation.Pointcut;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**

* DataSourceAspect

*

* @author plsy

*/

@Component

@Aspect

@Order(-1)

public class DataSourceAspect {

Logger logger = LoggerFactory.getLogger(getClass());

@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")

private void cutController() {

}

@Before("cutController()")

public void before(JoinPoint joinPoint) {

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = attributes.getRequest();

// 租户标识

String sign = request.getHeader("Site-info");

if (Assert.isNotEmpty(sign)) {

DynamicDataSourceContextHolder.setDataSource(sign);

} else {

DynamicDataSourceContextHolder.clearDataSource();

}

}

}

前端设置切换数据源标识header,切面取值设置到DynamicDataSourceContextHolder中,租户标识与数据源初始化时设置的name相同。


版权声明:本文为weixin_42466857原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。