Spring Boot-在多租户应用程序的单个请求中访问多个数据源

时间:2019-03-16 14:10:17

标签: java spring-boot spring-data-jpa multi-tenant

此应用程序从请求网址中提取子域,并将其用作tenantId,以选择要连接的数据源。

public class TenantDetectionFilter extends GenericFilterBean {

    private final MultiTenantManager multiTenantManager;

    public TenantDetectionFilter(MultiTenantManager multiTenantManager) {
        this.multiTenantManager = multiTenantManager;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
        try {
            mapSubDomainToDataSource(getSubDomainFromDomain(servletRequest.getServerName()));
        } catch (Exception e) {
            //sending error
            return;
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    private void mapSubDomainToDataSource(String subDomain) throws Exception {
        multiTenantManager.setCurrentTenant(subDomain);
    }

    private String getSubDomainFromDomain(@NotNull String domain) {
        //logic to extract the sub-domain
    }
}

MultiTenantManager使用提取的子域将应用程序映射到相关的MySql数据库。


@Configuration
public class MultiTenantManager {

    public static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
    private final Map<Object, Object> tenantDataSources = new ConcurrentHashMap<>();
    private static final String DB_CONNECTOR_DRIVER = "com.mysql.cj.jdbc.Driver";
    private AbstractRoutingDataSource multiTenantDataSource;

    @Bean
    public DataSource dataSource() {
        multiTenantDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                return currentTenant.get();
            }
        };
        multiTenantDataSource.setTargetDataSources(tenantDataSources);
        multiTenantDataSource.setDefaultTargetDataSource(defaultDataSource());
        multiTenantDataSource.afterPropertiesSet();
        populateTenantsProd();

        return multiTenantDataSource;
    }

    public void addTenant(String tenantId, String url, String username, String password) throws SQLException {
        DataSource dataSource = DataSourceBuilder.create()
                                                 .driverClassName(DB_CONNECTOR_DRIVER)
                                                 .url(url)
                                                 .username(username)
                                                 .password(password)
                                                 .build();

        try (Connection c = dataSource.getConnection()) {
            tenantDataSources.put(tenantId, dataSource);
            multiTenantDataSource.afterPropertiesSet();
        }
    }

    public void setCurrentTenant(String tenantId) throws Exception {
        currentTenant.set(tenantId);
    }

    private DriverManagerDataSource defaultDataSource() {
        DriverManagerDataSource defaultDataSource = new DriverManagerDataSource();
        defaultDataSource.setDriverClassName(DB_CONNECTOR_DRIVER);
        defaultDataSource.setUrl("jdbc:mysql://localhost:3306/defaultDB");
        defaultDataSource.setUsername("username");
        defaultDataSource.setPassword("password)");
        return defaultDataSource;
    }

    private void populateTenantsProd() {
        try {
            addTenant("tenantId_1", "jdbc:mysql://localhost:3306/mysql_db_1", "username", "password");
            addTenant("tenantId_2", "jdbc:mysql://localhost:3306/mysql_db_2", "username", "password");
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
}

到目前为止,一切正常。但是,此方案增加了一个新要求,即也要使用 MongoDB 数据库。因此,当收到请求时,逻辑代码应该能够将选定的(使用tenantId Mysql 数据库用于其业务逻辑,此外,逻辑代码还应该能够使用选定的(使用{{1 }})MongoDB数据库,用于保存一些元数据。

我将 Spring Data Jpa 休眠结合使用。

  1. 从域 ex:tenant_1
  2. 中提取子域
  3. 解析该tenantId的MongoDB和Mysql数据库名称
  4. 在业务逻辑中同时使用两个数据库

能否请您解释一下如何在Spring Boot中实现此目标。

1 个答案:

答案 0 :(得分:0)

我必须重写getDb(String dbName)才能完成这项工作。

public class MongoFactory extends SimpleMongoDbFactory
{

    private static final String DB_PREFIX = "prefix_";

    public MongoFactory(MongoClientURI uri)
    {
        super(uri);
    }

    public MongoFactory()
    {
        super(new MongoClientURI("mongodb://localhost/test"));
    }

    @Override
    public MongoDatabase getDb(String dbName) throws DataAccessException
    {
        if(Objects.nonNull(currentTenant.get())){
            return super.getDb(DB_PREFIX+currentTenant.get());
        }
        return super.getDb();
    }
}

MultiTenantManager中,

@Bean(name = "mongoDbFactory")
public MongoDbFactory mongoDbFactory()throws Exception
{
    return new MongoFactory();
}

@Bean
public MongoTemplate mongoTemplate(@Qualifier("mongoDbFactory") MongoDbFactory dbFactory) throws Exception
{
    return new MongoTemplate(dbFactory);
}