Интерфейс TenantResolver.
Код: Выделить всё
@FunctionalInterface
public interface TenantResolver{
String resolveTenant(T object);
}
Код: Выделить всё
@Component
public class HttpHeaderTenantResolver implements TenantResolver {
@Override
public String resolveTenant(HttpServletRequest request) {
return request.getHeader("X-tenantId");
}
}
Код: Выделить всё
public class TenantContext {
private static final Logger LOGGER = LoggerFactory.getLogger(TenantContext.class);
private static final ThreadLocal CURRENT_TENANT = new ThreadLocal();
public static void setTenantId(String tenant) {
LOGGER.info("Setting current tenant to {}", tenant);
CURRENT_TENANT.set(tenant);
}
public static String getTenantId() {
return CURRENT_TENANT.get();
}
public static void clear(){
CURRENT_TENANT.remove();
}
}
Код: Выделить всё
@Component
public class TenantInterceptor implements HandlerInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(TenantInterceptor.class);
private final HttpHeaderTenantResolver tenantResolver;
public TenantInterceptor(HttpHeaderTenantResolver tenantResolver) {
this.tenantResolver = tenantResolver;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
LOGGER.info("Inside preHandle() of TenantInterceptor");
String tenantId = tenantResolver.resolveTenant(request);
if (tenantId.isEmpty() || tenantId.isBlank()) {
tenantId = "tenant1";
}
TenantContext.setTenantId(tenantId);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
TenantContext.clear();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
TenantContext.clear();
}
}
Код: Выделить всё
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private TenantInterceptor tenantInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(tenantInterceptor);
}
}
Код: Выделить всё
.
.
.
server.compression.enabled=true
.
.
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=....
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show.sql=true
spring.jpa.hib.ddl.auto=none
spring.jpa.prop.hib.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true
spring.jpa.properties.hibernate.format_sql=false
.
.
.
### Multitenancy demo ###
#spring.datasource.initialization-mode=never
#spring.jpa.defer-datasource-initialization=true
spring.main.allow-bean-definition-overriding=true
spring.sql.init.mode=never
# tenant1
tenants.tenant.tenant1.url=...
tenants.tenant.tenant1.username=root
tenants.tenant.tenant1.password=root
tenants.tenant.tenant1.driverClassName=org.postgresql.Driver
# tenant2
tenants.tenant.tenant2.url=...
tenants.tenant.tenant2.username=root
tenants.tenant.tenant2.password=root
tenants.tenant.tenant2.driverClassName=org.postgresql.Driver
Я создал класс TenantProperties для чтения информации о БД моих арендаторов из приложения. файл свойств.
Код: Выделить всё
@Component
@ConfigurationProperties(prefix="tenants")
public class TenantProperties {
private Map tenant = new HashMap();
public Map getTenant(){
return tenant;
}
public void setTenant(Map tenant){
this.tenant = tenant;
}
public static class DataSourceProperties {
private String url;
private String username;
private String password;
private String driverClassName;
public String getUrl(){
return url;
}
public String getUsername(){
return username;
}
public String getPassword(){
return password;
}
public String getDriverClassName(){
return driverClassName;
}
public void setUrl(String url){
this.url = url;
}
public void setUsername(String username){
this.username = username;
}
public void setPassword(String password){
this.password = password;
}
public void setDriverClassName(String driverClassName){
this.driverClassName = driverClassName;
}
}
}
Код: Выделить всё
@Configuration
public class MultiTenantConfiguration {
@Autowired
private TenantProperties tenantProperties;
@Bean
@Primary
public DataSource dataSource(){
Map resolvedDatasources = new HashMap();
tenantProperties.getTenant().forEach((tenantId, dsProps) -> {
DataSource dataSource = DataSourceBuilder.create()
.url(dsProps.getUrl())
.username(dsProps.getUsername())
.password(dsProps.getPassword())
.driverClassName(dsProps.getDriverClassName())
.build();
resolvedDatasources.put(tenantId, dataSource);
});
AbstractRoutingDataSource multiTenantDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getTenantId();
}
};
multiTenantDataSource.setDefaultTargetDataSource(resolvedDatasources.values().iterator().next());
multiTenantDataSource.setTargetDataSources(resolvedDatasources);
multiTenantDataSource.afterPropertiesSet();
return multiTenantDataSource;
}
// @Bean
// @Primary
// LocalSessionFactoryBean sessionFactory(DataSource dataSource) throws SQLException {
// LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
// sessionFactory.setDataSource(dataSource);
// sessionFactory.setPackagesToScan("org.example.*");
// Properties hibernateProperties = new Properties();
// hibernateProperties.put("hibernate.multiTenancy", "DATABASE");
// //hibernateProperties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
// hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
// hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
// sessionFactory.setHibernateProperties(hibernateProperties);
// Properties props = sessionFactory.getHibernateProperties();
// System.out.println("--------Hibernate properties");
// props.forEach((k, v) -> System.out.println(k + ": " + v));
// return sessionFactory;
// }
//
// @Bean
// @Primary
// public PlatformTransactionManager transactionManager(SessionFactory sessionFactory){
// HibernateTransactionManager transactionManager = new HibernateTransactionManager();
// transactionManager.setSessionFactory(sessionFactory);
// return transactionManager;
// }
@Bean
@Primary
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, MultiTenantConnectionProvider connectionProvider, CurrentTenantIdentifierResolver tenantIdentifierResolver){
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(dataSource);
factory.setPackagesToScan("org.example.*");
factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Properties jpaProperties = new Properties();
jpaProperties.put("hibernate.multiTenancy", "DATABASE");
jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
jpaProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
jpaProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, connectionProvider);
factory.setJpaProperties(jpaProperties);
return factory;
}
@Bean
@Primary
public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory.getObject());
return transactionManager;
}
}
Теперь о тех частях, которые меня поставили в тупик. Следуя моим инструкциям, я создал TenantIdentifierResolver.
Код: Выделить всё
@Component
@Primary
public class TenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
if (TenantContext.getTenantId() == null) {
return "tenant1";
}
return TenantContext.getTenantId();
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
Но resolveCurrentTenantIdentifier никогда не вызывается, когда я вызываю конечную точку контроллера.
/>Затем у меня есть провайдер подключения, который, по моему мнению, является корнем моей проблемы.
Код: Выделить всё
@Component
@Primary
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(MultiTenantConnectionProviderImpl.class);
@Autowired
private DataSource dataSource;
@PostConstruct
public void init() {
System.out.println("!!!!!!!ConnectionProvider constructed");
System.out.println("Also check ds is abstract???" + (dataSource instanceof AbstractRoutingDataSource));
}
@Override
public Connection getAnyConnection() throws SQLException {
LOGGER.info("Getting default connection for all");
return dataSource.getConnection();
}
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
connection.close();
}
@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
LOGGER.info("Connecting to DB for tenant: {}", tenantIdentifier);
return dataSource.getConnection();
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
LOGGER.info("Releasing DB connection for tenant: {}", tenantIdentifier);
connection.close();
}
@Override
public boolean supportsAggressiveRelease() {
return false;
}
@Override
public @UnknownKeyFor @NonNull @Initialized boolean isUnwrappableAs(@UnknownKeyFor @NonNull @Initialized Class unwrapType) {
return false;
}
@Override
public T unwrap(@UnknownKeyFor @NonNull @Initialized Class unwrapType) {
return null;
}
}
Я тестирую это, используя фиктивный контроллер, который выполняет запрос GET, чтобы получить количество записей в таблице (с помощью Postman). Очевидно, что значения должны быть разными, но это не так. Я получаю только значение, установленное в моих свойствах Spring.datasource. Свойства клиента, похоже, не используются.
Проект содержит еще два файла конфигурации. Я оставляю их ниже на случай, если они могут оказаться актуальными.
Код: Выделить всё
@Configuration
@EnableJpaRepositories("com.example.repository")
public class JpaConfig {
}
Код: Выделить всё
@Configuration
@PropertySource("classpath:application.properties")
public class PropertiesConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
Java 17,
SpringBoot версии 3.1.9,
Hibernate версии 6.3.1.Final.< /p>
Будем очень благодарны за любую помощь и/или объяснение.
Подробнее здесь: https://stackoverflow.com/questions/793 ... n-i-keep-g
Мобильная версия