Перейти к основному содержимому

Руководство по Spring AbstractRoutingDatasource

· 4 мин. чтения

1. Обзор

В этой быстрой статье мы рассмотрим Spring AbstractRoutingDatasource как способ динамического определения фактического источника данных на основе текущего контекста .

В результате мы увидим, что можем убрать логику поиска DataSource из кода доступа к данным.

2. Зависимости Maven

Начнем с объявления spring-context, spring-jdbc, spring-test и h2 в качестве зависимостей в pom.xml :

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.8.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.195</version>
<scope>test</scope>
</dependency>
</dependencies>

Последнюю версию зависимостей можно найти здесь .

Если вы используете Spring Boot, мы можем использовать стартеры для Spring Data и Test:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.195</version>
<scope>test</scope>
</dependency>
</dependencies>

3. Контекст источника данных

AbstractRoutingDatasource требует информации, чтобы знать, к какому фактическому источнику данных следует маршрутизировать. Эта информация обычно называется контекстом.

Хотя Context , используемый с AbstractRoutingDatasource , может быть любым объектом, для их определения используется перечисление . В нашем примере мы будем использовать понятие ClientDatabase в качестве нашего контекста со следующей реализацией:

public enum ClientDatabase {
CLIENT_A, CLIENT_B
}

Стоит отметить, что на практике контекст может быть любым, имеющим смысл для рассматриваемой области.

Например, другой распространенный вариант использования включает использование понятия среды для определения контекста. В таком сценарии контекст может быть перечислением, содержащим PRODUCTION , DEVELOPMENT и TESTING .

4. Держатель контекста

Реализация держателя контекста представляет собой контейнер, в котором хранится текущий контекст в виде ссылки ThreadLocal .

Помимо хранения ссылки, он должен содержать статические методы для ее установки, получения и очистки. AbstractRoutingDatasource запросит у ContextHolder контекст, а затем будет использовать контекст для поиска фактического источника данных .

Критически важно использовать здесь ThreadLocal , чтобы контекст был привязан к выполняющемуся в данный момент потоку .

Очень важно использовать этот подход, чтобы поведение было надежным, когда логика доступа к данным охватывает несколько источников данных и использует транзакции:

public class ClientDatabaseContextHolder {

private static ThreadLocal<ClientDatabase> CONTEXT
= new ThreadLocal<>();

public static void set(ClientDatabase clientDatabase) {
Assert.notNull(clientDatabase, "clientDatabase cannot be null");
CONTEXT.set(clientDatabase);
}

public static ClientDatabase getClientDatabase() {
return CONTEXT.get();
}

public static void clear() {
CONTEXT.remove();
}
}

5. Маршрутизатор источника данных

Мы определяем наш ClientDataSourceRouter для расширения Spring AbstractRoutingDataSource . Мы реализуем необходимый метод defineCurrentLookupKey для запроса нашего ClientDatabaseContextHolder и возврата соответствующего ключа.

Реализация AbstractRoutingDataSource выполняет остальную работу за нас и прозрачно возвращает соответствующий DataSource:

public class ClientDataSourceRouter
extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return ClientDatabaseContextHolder.getClientDatabase();
}
}

6. Конфигурация

Нам нужна Карта контекстов для объектов DataSource для настройки нашего AbstractRoutingDataSource . Мы также можем указать источник данных по умолчанию для использования, если контекст не установлен.

Используемые нами DataSource могут поступать откуда угодно, но обычно они либо создаются во время выполнения, либо просматриваются с помощью JNDI:

@Configuration
public class RoutingTestConfiguration {

@Bean
public ClientService clientService() {
return new ClientService(new ClientDao(clientDatasource()));
}

@Bean
public DataSource clientDatasource() {
Map<Object, Object> targetDataSources = new HashMap<>();
DataSource clientADatasource = clientADatasource();
DataSource clientBDatasource = clientBDatasource();
targetDataSources.put(ClientDatabase.CLIENT_A,
clientADatasource);
targetDataSources.put(ClientDatabase.CLIENT_B,
clientBDatasource);

ClientDataSourceRouter clientRoutingDatasource
= new ClientDataSourceRouter();
clientRoutingDatasource.setTargetDataSources(targetDataSources);
clientRoutingDatasource.setDefaultTargetDataSource(clientADatasource);
return clientRoutingDatasource;
}

// ...
}

При использовании Spring Boot вы можете настроить источники данных в файле application.properties (т.е. ClientA и ClientB ): `` ** **

#database details for CLIENT_A
client-a.datasource.name=CLIENT_A
client-a.datasource.script=SOME_SCRIPT.sql

#database details for CLIENT_B
client-b.datasource.name=CLIENT_B
client-b.datasource.script=SOME_SCRIPT.sql

Затем вы можете создать POJO, которые будут содержать свойства ваших источников данных :

@Component
@ConfigurationProperties(prefix = "client-a.datasource")
public class ClientADetails {

private String name;
private String script;

// Getters & Setters
}

и используйте их для создания bean-компонентов источника данных:

@Autowired
private ClientADetails clientADetails;
@Autowired
private ClientBDetails clientBDetails;

private DataSource clientADatasource() {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.setName(clientADetails.getName())
.addScript(clientADetails.getScript())
.build();
}

private DataSource clientBDatasource() {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.setName(clientBDetails.getName())
.addScript(clientBDetails.getScript())
.build();
}

7. Использование

При использовании нашего AbstractRoutingDataSource мы сначала устанавливаем контекст, а затем выполняем нашу операцию. Мы используем сервисный уровень, который принимает контекст в качестве параметра и устанавливает его перед делегированием коду доступа к данным и очищает контекст после вызова.

В качестве альтернативы ручной очистке контекста в сервисном методе логика очистки может быть реализована с помощью AOP point cut.

Важно помнить, что контекст привязан к потоку, особенно если логика доступа к данным будет охватывать несколько источников данных и транзакций:

public class ClientService {

private ClientDao clientDao;

// standard constructors

public String getClientName(ClientDatabase clientDb) {
ClientDatabaseContextHolder.set(clientDb);
String clientName = this.clientDao.getClientName();
ClientDatabaseContextHolder.clear();
return clientName;
}
}

8. Заключение

В этом руководстве мы рассмотрели пример использования Spring AbstractRoutingDataSource . Мы реализовали решение, используя понятие Client — где у каждого клиента есть свой DataSource .

И, как всегда, примеры можно найти на GitHub .