首页 > 解决方案 > 用户登录后如何使用 AbstractDataSource 切换 Schema

问题描述

我的问题:在用户登录后,在 StackOverFlow 之后坚持实施模式更改。

描述:我正在使用下面的课程。但是,我不知道如何使用它。我正在阅读每个教程,但我被卡住了。我期待的结果是:

1- Spring 使用默认 URL 进行初始化,以便用户可以登录。

2- 成功登录后,它会更改为基于UserDetails类的架构。

我正在关注 Stack Overflow 的解决方案:Change database schema during runtime based on logged in user

我正在使用的 Spring 版本是

> : Spring Boot ::        (v2.3.3.RELEASE)
    import com.google.common.cache.CacheBuilder;
    import com.google.common.cache.CacheLoader;
    import com.google.common.cache.LoadingCache;
    import java.sql.Connection;
    import java.sql.ConnectionBuilder;
    import java.sql.SQLException;
    import java.util.concurrent.TimeUnit;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.core.env.Environment;
    import org.springframework.jdbc.datasource.AbstractDataSource;
     
     
    public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
     
        @Autowired
        UsuarioProvider customUserDetails;
     
        @Autowired
        Environment env;
     
        private LoadingCache<String, DataSource> dataSources = createCache();
     
        public UserSchemaAwareRoutingDataSource() {
        }
     
        public UserSchemaAwareRoutingDataSource(UsuarioProvider customUserDetails, Environment env) {
            this.customUserDetails = customUserDetails;
            this.env = env;
        }
     
        private LoadingCache<String, DataSource> createCache() {
            return CacheBuilder.newBuilder()
                    .maximumSize(100)
                    .expireAfterWrite(10, TimeUnit.MINUTES)
                    .build(
                            new CacheLoader<String, DataSource>() {
                        public DataSource load(String key) throws Exception {
                            return buildDataSourceForSchema(key);
                        }
                    });
        }
     
        private DataSource buildDataSourceForSchema(String schema) {
            System.out.println("schema:" + schema);
            String url = "jdbc:mysql://REDACTED.com/" + schema;
            String username = env.getRequiredProperty("spring.datasource.username");
            String password = env.getRequiredProperty("spring.datasource.password");
     
            System.out.println("Flag A");
     
            DataSource build = (DataSource) DataSourceBuilder.create()
                    .driverClassName(env.getRequiredProperty("spring.datasource.driverClassName"))
                    .username(username)
                    .password(password)
                    .url(url)
                    .build();
     
            System.out.println("Flag B");
     
            return build;
        }
     
        @Override
        public Connection getConnection() throws SQLException {
            return determineTargetDataSource().getConnection();
        }
     
        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return determineTargetDataSource().getConnection(username, password);
        }
     
        private DataSource determineTargetDataSource() {
            try {
                Usuario usuario = customUserDetails.customUserDetails();
                //
                String db_schema = usuario.getTunnel().getDb_schema();
                //
     
                String schema = db_schema;
                return dataSources.get(schema);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return null;
        }
     
        @Override
        public ConnectionBuilder createConnectionBuilder() throws SQLException {
            return super.createConnectionBuilder();
        }
     
    }

参考: https ://spring.io/blog/2007/01/23/dynamic-datasource-routing/

如何使用 JDBC 在 Spring 中创建动态连接(数据源)

Spring Boot 配置和使用两个数据源

编辑(评论中需要其他信息):

我有 1 个数据库。该数据库有n许多模式。每个模式都属于一家公司。一位用户属于一家公司。登录逻辑如下:

- 用户输入用户名和密码。- 成功时,UserDetails将包含此用户的“模式”的名称。基本上,该用户属于哪个公司/架构。

我希望这尽可能地澄清。

编辑2:

    @Component
    public class UsuarioProvider {
    
        @Bean
        @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) // or just @RequestScope
        public Usuario customUserDetails() {
            return (Usuario) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        }
    
    }
    public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
    
        @Autowired
        private UsuarioProvider usuarioProvider;
    
        @Autowired // This references the primary datasource, because no qualifier is given
        private DataSource companyDependentDataSource;
    
        @Autowired
        @Qualifier(value = "loginDataSource") 
        private DataSource loginDataSource;
    
        @Autowired
        Environment env;
    
        private LoadingCache<String, DataSource> dataSources = createCache();
    
        public UserSchemaAwareRoutingDataSource() {
        }
    
    
    
        private LoadingCache<String, DataSource> createCache() {
            return CacheBuilder.newBuilder()
                    .maximumSize(100)
                    .expireAfterWrite(10, TimeUnit.MINUTES)
                    .build(
                            new CacheLoader<String, DataSource>() {
                        public DataSource load(String key) throws Exception {
                            return buildDataSourceForSchema(key);
                        }
                    });
        }
    
        private DataSource buildDataSourceForSchema(String schema) {
            System.out.println("schema:" + schema);
            String url = "jdbc:mysql://REDACTED.com/" + schema;
            String username = env.getRequiredProperty("spring.datasource.username");
            String password = env.getRequiredProperty("spring.datasource.password");
    
            System.out.println("Flag A");
    
            DataSource build = (DataSource) DataSourceBuilder.create()
                    .driverClassName(env.getRequiredProperty("spring.datasource.driverClassName"))
                    .username(username)
                    .password(password)
                    .url(url)
                    .build();
    
            System.out.println("Flag B");
    
            return build;
        }
    
        @Override
        public Connection getConnection() throws SQLException {
            return determineTargetDataSource().getConnection();
        }
    
        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return determineTargetDataSource().getConnection(username, password);
        }
    
        private DataSource determineTargetDataSource() {
            try {
                System.out.println("Flag G");
                Usuario usuario = usuarioProvider.customUserDetails(); // request scoped answer!
                String db_schema = usuario.getTunnel().getDb_schema();
                return dataSources.get(db_schema);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return null;
        }
    
        @Override
        public ConnectionBuilder createConnectionBuilder() throws SQLException {
            return super.createConnectionBuilder();
        }
    
    }

我需要把这@Configuration门课放在首位吗?我无法让 Spring Boot 了解此设置。我对如何告诉 Spring Boot loginDataSource;url 是什么感到有些困惑。我使用application.properties默认值登录。

标签: javaspringspring-bootspring-mvc

解决方案


您的设置结合了两个不同数据源的经典情况。这是Baeldung-Blog-Post 如何配置 Spring Data JPA

首先要注意的是,他们正在使用@Primary. 这在帮助你的同时也阻碍了你。您只能拥有一个特定类型的主 bean。这给某些人带来了麻烦,因为他们试图通过将测试 spring bean 设为主要来“覆盖”spring bean。这导致有两个具有相同类型的主 bean。因此,在设置测试时要小心。

但是,如果您主要指的是一个 DataSource 而仅在少数情况下指代另一个,它也会使事情变得容易。这似乎是你的情况,所以让我们采用它。

您的 DataSource 配置可能看起来像

@Configuration
public class DataSourceConfiguration {
    @Bean(name="loginDataSource")
    public DataSource loginDataSource(Environment env) {
        String url = env.getRequiredProperty("spring.logindatasource.url");
        return DataSourceBuilder.create()
            .driverClassName(env.getRequiredProperty("spring.logindatasource.driverClassName"))
            [...]
            .url(url)
            .build();
    }
    
    @Bean(name="companyDependentDataSource")
    @Primary // use with caution, I'd recommend to use name based autowiring. See @Qualifier
    public DataSource companyDependentDataSource(Environment env) {
        return new UserSchemaAwareRoutingDataSource(); // Autowiring is done afterwards by Spring
    }
}

这两个数据源现在可以在您的存储库/DAO 中使用,或者您如何构建您的程序

@Autowired // This references the primary datasource, because no qualifier is given. UserSchemaAwareRoutingDataSource is its implementation
// @Qualifier("companyDependentDataSource") if @Primary is omitted
private DataSource companyDependentDataSource;

@Autowired
@Qualifier(name="loginDataSource") // reference by bean name
private DataSource loginDataSource

这是一个示例,如何使用DataSource名称引用来配置 Spring Data JPA:

@Configuration
@EnableJpaRepositories(
    basePackages = "<your entity package>", 
    entityManagerFactoryRef = "companyEntityManagerFactory", 
    transactionManagerRef = "companyTransactionManager"
)
public class CompanyPersistenceConfiguration {

    @Autowired
    @Qualifier("companyDependentDataSource")
    private DataSource companyDependentDataSource;
    
    @Bean(name="companyEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean companyEntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(companyDependentDataSource);
        // ... see Baeldung Blog Post
        return emf;
    }

    @Bean(name="companyTransactionManager")
    public PlatformTransactionManager companyTransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(companyEntityManagerFactory().getObject());
        return tm;
    }
}

正如您提到的我的 SO-answer中所述,有一个重要的假设

用于当前用户的当前模式名称可通过 Spring JSR-330 Provider 访问,例如private javax.inject.Provider<User> user; String schema = user.get().getSchema();. 理想情况下,这是一个基于 ThreadLocal 的代理。

这是使UserSchemaAwareRoutingDataSource实现成为可能的技巧。Spring bean 大多是单例的,因此是无状态的。这也适用于 DataSources 的正常使用。它们被视为无状态的单例,并且对它们的引用在整个程序中传递。因此,我们需要找到一种方法来提供单个实例,该实例companyDependentDataSource在用户基础上表现不同,无论如何。为了获得这种行为,我建议使用请求范围的 bean。

在 Web 应用程序中,您可以使用@Scope(REQUEST_SCOPE)来创建此类对象。还有一个Bealdung Post谈论那个话题。像往常一样,@Bean带注释的方法驻留在@Confiugration带注释的类中。

@Configuration
public class UsuarioConfiguration {
    @Bean
    @Scope(value = WebApplicationContext.SCOPE_REQUEST,
     proxyMode = ScopedProxyMode.TARGET_CLASS) // or just @RequestScope
    public Usuario usario() {
        // based on your edit2 
        return (Usuario) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}

现在,您可以将此请求范围对象与单例DataSource 中的提供程序一起使用,以根据登录用户的行为有所不同:

@Autowired
private Usario usario; // this is now a request-scoped proxy which will create the corresponding bean (see UsuarioConfiguration.usario()

private DataSource determineTargetDataSource() {
    try {
        String db_schema = this.usuario.getTunnel().getDb_schema();
        return dataSources.get(db_schema);
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    return null;
}

我希望这可以帮助您理解 Spring 的请求范围概念。

所以你的登录过程看起来像

  1. 用户输入用户名和密码
  2. 一个普通的 spring bean,通过name引用 userDataSource ,正在检查登录并将用户信息放入 session/securitycontext/cookie/....
  3. 成功后,在下一个请求期间,companyDependentDataSource能够检索正确设置的Usario对象
  4. 您现在可以使用此数据源来执行用户特定的操作。

要验证您DataSource是否正常工作,您可以创建一个小型 Spring MVC 端点

@RestController
public class DataSourceVerificationController {
    @Autowired
    private Usario usario;
    
    @Autowired
    @Qualifier("companyDependentDataSource") // omit this annotation if you use @Primary
    private DataSource companyDependentDataSource;

    @GetRequest("/test")
    public String test() throws Exception {
        String schema = usario.getTunnel().getDb_schema()
    
        Connection con = companyDependentDataSource.getConnection();
        Statement stmt = con.createStatement();
        ResultSet rs = stmt.executeQuery("select name from Employee"); // just a random guess
        rs.next();
        String name = rs.getString("name")
        rs.close();
        stmt.close();
        con.close();
        
        return "name = '" + name + "', schema = '" + schema + "'";
    }
}

使用您喜欢的浏览器进入您的登录页面,进行有效登录,然后调用 http://localhost:8080/test


推荐阅读