Попытка реализовать мультиарендность в существующем приложении Springboot. Я продолжаю получать данные из той же базы даJAVA

Программисты JAVA общаются здесь
Ответить
Anonymous
 Попытка реализовать мультиарендность в существующем приложении Springboot. Я продолжаю получать данные из той же базы да

Сообщение Anonymous »

У меня есть большой микросервис, в котором я пытаюсь реализовать мультиарендность (база данных на одного клиента) — концепцию, которую я применяю впервые. До сих пор я следил за некоторыми руководствами и видео здесь и там. Вот что у меня есть на данный момент.
Интерфейс TenantResolver.

Код: Выделить всё

@FunctionalInterface
public interface TenantResolver{
String resolveTenant(T object);
}
Класс, реализующий этот интерфейс. Я использую этот класс для разрешения tenantId из заголовка http, и это работает так, как ожидалось.

Код: Выделить всё

@Component
public class HttpHeaderTenantResolver implements TenantResolver {
@Override
public String resolveTenant(HttpServletRequest request) {
return request.getHeader("X-tenantId");
}
}
Затем у меня есть TenantContext для хранения 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);
}
}
Пока все хорошо. Затем идут сложные части. Сначала позвольте мне поделиться частью моих свойств application.

Код: Выделить всё

.
.
.
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
Все, что после "### Multitenancy demo ###", было добавлено мной. Я попробовал поиграться с несколькими свойствами. Но, как вы можете видеть, я планирую использовать 2 арендаторов, и у каждого будет своя собственная БД.
Я создал класс 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;
}
}
Это построено правильно. Но такие методы, как getConnection и т. д., похоже, никогда не вызываются. Я никогда не вижу информации о журнале, которую я добавляю в эти методы. (Однажды я видел их в одном из многих изменений, которые я внес в этот проект, но я этого не помню, потому что поток был упорядочен неправильно, а результат все равно был тот же.)
Я тестирую это, используя фиктивный контроллер, который выполняет запрос 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
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «JAVA»