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 .