Проекты Spring в целом имеют свое мнение: 80-90% случаев использования обрабатываются «по умолчанию», а код часто намного лаконичнее, чем требовалось бы в противном случае, из-за того, что Spring предпочитает конвенции конфигурации. Эти и другие «мнения» могут привести к значительно меньшему объему кода для написания и поддержки и, как следствие, к более целенаправленному воздействию.
В подавляющем большинстве случаев, когда Azure Storage используется из приложения, нет убедительных преимуществ использования более чем одной учетной записи Azure Storage. Но есть граничные случаи, и возможность использовать несколько учетных записей Azure Storage из одного приложения — даже если такая возможность может понадобиться только в 10% случаев — может стать невероятно полезным расширением наших суперспособностей в области хранения данных.
Эта статья является результатом сотрудничества с Ши ли Ченом.
- Все дело в ресурсах
- Становимся изобретательными
- Ресурсы Spring в Spring Cloud Azure
- Реализация AbstractResource
- Реализация ResourcePatternResolver
- Мнения
- Разработка расширенной версии Spring Cloud Azure Starter Storage Blob
- Расширение свойств Storage Blob
- Динамическая регистрация бобов Storage Blob
- Расширение AzureStorageBlobProtocolResolver
- Тестирование Spring Cloud Azure Starter Storage Blob Extend
- Заключение
- Ссылки и полезные ресурсы
Все дело в ресурсах
Spring Framework определяет интерфейс Resource
и предоставляет несколько реализаций, построенных на Resource
, чтобы облегчить разработчикам доступ к низкоуровневым ресурсам. Для того чтобы работать с определенным видом ресурсов, необходимы две вещи:
- реализация
Resource
. - реализация
ResourcePatternResolver
.
Приложение Spring оценивает ресурсы, используя один или несколько зарегистрированных резольверов. Когда тип ресурса определен, соответствующая реализация Resource
используется для доступа и/или манипулирования базовым ресурсом.
Если реализации, встроенные в Spring Framework, не удовлетворяют вашему сценарию использования, довольно просто добавить поддержку дополнительных типов ресурсов, определив собственные реализации интерфейсов AbstractResource
и ResourcePatternResolver
.
В этой статье мы представим Spring Resource, рассмотрим реализацию Spring Cloud Azure Resource
(особенно в отношении соображений и ограничений Azure Storage Account), а также рассмотрим, как расширить эту реализацию для решения тех частных случаев, когда было бы полезно получить доступ к нескольким Azure Storage Accounts из одного приложения Spring Boot.
Становимся изобретательными
Мы уже упоминали, что Spring Framework определяет несколько полезных реализаций ресурсов. На момент написания этой статьи они используются по умолчанию:
UrlResource
ClassPathResource
FileSystemResource
PathResource
ServletContextResource
InputStreamResource
ByteArrayResource
Как упоминалось ранее, каждый ресурс будет иметь соответствующий резольвер ресурсов.
Для того чтобы ваше приложение Spring Boot могло использовать пользовательский Resource
, необходимо выполнить следующие действия:
- Реализовать интерфейс
Resource
путем расширенияAbstractResource
. - Реализовать интерфейс
ResourcePatternResolver
для разрешения пользовательского типа ресурса - Зарегистрируйте реализацию
ResourcePatternResolver
как боб.
ПРИМЕЧАНИЕ: Ваш резольвер должен быть добавлен в набор резольверов загрузчика ресурсов по умолчанию с помощью метода org.springframework.core.io.DefaultResourceLoader#addProtocolResolver
, но этот код присутствует в AbstractAzureStorageProtocolResolver
; расширение этого класса для создания вашей реализации выполнит это от вашего имени, если вы не решите переопределить его метод setResourceLoader
.
ResourceLoader
пытается разрешить каждый Resource
, сравнивая его определенное местоположение/формат со всеми зарегистрированными преобразователями шаблонов протокола, пока не будет возвращен ненулевой ресурс. Если совпадение не найдено, Resource
будет оценен с помощью встроенных в Spring шаблонизаторов.
Ресурсы Spring в Spring Cloud Azure
Spring Cloud Azure предоставляет две реализации Spring resource и resource pattern resolver. В этой статье мы рассмотрим только реализацию ресурса Azure Storage Blob. Вы можете изучить исходный код Spring Cloud Azure Resources
на сайте Spring Cloud Azure и соответствующую документацию на сайте Resource Handling.
ПРИМЕЧАНИЕ: Для анализа и экспериментов мы используем Spring Cloud Azure Starter Storage Blob версии 4.2.0.
Реализация AbstractResource
Абстрактная реализация AzureStorageResource
для Spring Cloud Azure в первую очередь определяет формат протокола ресурса хранения Azure и учитывает уникальные атрибуты службы Azure Storage Account, например, имя контейнера и имя файла. Важно отметить, что AzureStorageResource
отделен от Azure Storage SDK.
Интерфейс Spring Framework WritableResource
представляет базовый API, на котором мы строим чтение из ресурса Azure Storage и запись в него.
abstract class AzureStorageResource extends AbstractResource implements WritableResource {
private boolean isAzureStorageResource(@NonNull String location) {
......
}
String getContainerName(String location) {
......
}
String getContentType(String location) {
......
}
String getFilename(String location) {
......
}
abstract StorageType getStorageType();
}
Ресурс StorageBlobResource
является реализацией Spring Cloud Azure Storage Blob абстрактного класса AbstractResource
.
Мы видим, что StorageBlobResource
использует BlobServiceClient
из Azure Storage Blob SDK для реализации всех абстрактных методов, полагаясь на клиент службы для взаимодействия со службой Azure Storage Blob.
public final class StorageBlobResource extends AzureStorageResource {
private final BlobServiceClient blobServiceClient;
private final BlobContainerClient blobContainerClient;
private final BlockBlobClient blockBlobClient;
public StorageBlobResource(BlobServiceClient blobServiceClient, String location, Boolean autoCreateFiles,
String snapshot, String versionId, String contentType) {
......
this.blobContainerClient = blobServiceClient.getBlobContainerClient(getContainerName(location));
BlobClient blobClient = blobContainerClient.getBlobClient(getFilename(location));
this.blockBlobClient = blobClient.getBlockBlobClient();
}
@Override
public OutputStream getOutputStream() throws IOException {
try {
......
return this.blockBlobClient.getBlobOutputStream(options);
} catch (BlobStorageException e) {
throw new IOException(MSG_FAIL_OPEN_OUTPUT, e);
}
}
......
@Override
StorageType getStorageType() {
return StorageType.BLOB;
}
}
Реализация ResourcePatternResolver
Spring Cloud Azure предоставляет абстрактную реализацию AbstractAzureStorageProtocolResolver
. Этот класс включает общую обработку протокола ресурсов хранения Azure, раскрывает специфические возможности службы Azure Storage Account и добавляет необходимую логику в загрузчик ресурсов по умолчанию. Как и AzureStorageResource
, AbstractAzureStorageProtocolResolver
также не связан с Azure Storage SDK.
public abstract class AbstractAzureStorageProtocolResolver implements ProtocolResolver, ResourcePatternResolver,
ResourceLoaderAware, BeanFactoryPostProcessor {
protected final AntPathMatcher matcher = new AntPathMatcher();
protected abstract StorageType getStorageType();
protected abstract Resource getStorageResource(String location, Boolean autoCreate);
protected ConfigurableListableBeanFactory beanFactory;
protected abstract Stream<StorageContainerItem> listStorageContainers(String containerPrefix);
protected abstract StorageContainerClient getStorageContainerClient(String name);
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
if (resourceLoader instanceof DefaultResourceLoader) {
((DefaultResourceLoader) resourceLoader).addProtocolResolver(this);
} else {
LOGGER.warn("Custom Protocol using azure-{}:// prefix will not be enabled.", getStorageType().getType());
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
if (AzureStorageUtils.isAzureStorageResource(location, getStorageType())) {
return getResource(location);
}
return null;
}
@Override
public Resource[] getResources(String pattern) throws IOException {
Resource[] resources = null;
if (AzureStorageUtils.isAzureStorageResource(pattern, getStorageType())) {
if (matcher.isPattern(AzureStorageUtils.stripProtocol(pattern, getStorageType()))) {
String containerPattern = AzureStorageUtils.getContainerName(pattern, getStorageType());
String filePattern = AzureStorageUtils.getFilename(pattern, getStorageType());
resources = resolveResources(containerPattern, filePattern);
} else {
return new Resource[] { getResource(pattern) };
}
}
if (null == resources) {
throw new IOException("Resources not found at " + pattern);
}
return resources;
}
@Override
public Resource getResource(String location) {
Resource resource = null;
if (AzureStorageUtils.isAzureStorageResource(location, getStorageType())) {
resource = getStorageResource(location, true);
}
if (null == resource) {
throw new IllegalArgumentException("Resource not found at " + location);
}
return resource;
}
/**
* Storage container item.
*/
protected static class StorageContainerItem {
private final String name;
......
}
protected static class StorageItem {
private final String container;
private final String name;
private final StorageType storageType;
......
}
protected interface StorageContainerClient {
......
}
}
Резольвер ресурсов AzureStorageBlobProtocolResolver
является реализацией Spring Cloud Azure Storage Blob ResourcePatternResolver
. Он инкапсулирует ресурсы в соответствии с шаблоном расположения или элемента хранения на основе BlobServiceClient
и возвращает связанный StorageBlobResource
.
public final class AzureStorageBlobProtocolResolver extends AbstractAzureStorageProtocolResolver {
private BlobServiceClient blobServiceClient;
@Override
protected StorageType getStorageType() {
return StorageType.BLOB;
}
@Override
protected Resource getStorageResource(String location, Boolean autoCreate) {
return new StorageBlobResource(getBlobServiceClient(), location, autoCreate);
}
private BlobServiceClient getBlobServiceClient() {
if (blobServiceClient == null) {
blobServiceClient = beanFactory.getBean(BlobServiceClient.class);
}
return blobServiceClient;
}
}
Мнения
Как уже упоминалось в начале этой заметки, возможности по умолчанию прекрасно выполняют требования в подавляющем большинстве случаев. Но в соответствии с принципами Spring, Spring Cloud Azure Starter Storage Blob был разработан для беспроблемного решения 80-90% случаев использования «из коробки», в то время как остальные (граничные) случаи могут быть реализованы с некоторыми дополнительными усилиями.
Как написано, ресурс Storage Blob поддерживает несколько операций с контейнерами, используя один и тот же аккаунт хранения. Важным моментом является то, что пути к блобам в разных контейнерах могут быть правильно разрешены в объекты StorageBlobResource
. Комбинируя предыдущий код для StorageBlobResource
, ресурс blob должен содержать клиента службы blob, и если blobServiceClient.getBlobContainerClient(getContainerName(location))
успешно возвращает BlobServiceClient
, ресурс blob может быть разрешен и получен.
Боб BlobServiceClient
представляет учетную запись Azure Storage Account в Azure Storage Blob SDK, что означает, что текущая реализация не поддерживает одновременную доступность с использованием нескольких Azure Storage Accounts.
Разработка расширенной версии Spring Cloud Azure Starter Storage Blob
Для тех редких случаев, когда может быть полезным одновременный доступ к нескольким учетным записям Azure Storage из одного приложения, существует способ сделать это. Чтобы продемонстрировать эту возможность, давайте создадим новую библиотеку под названием spring-cloud-azure-starter-storage-blob-extend
. Единственной внешней зависимостью для этой новой библиотеки является существующая spring-cloud-azure-starter-storage-blob
.
Расширение свойств Storage Blob
Хотя основной целью является поддержка нескольких учетных записей хранилищ, вторичной целью проектирования является использование структуры, аналогичной AzureStorageBlobProperties
, чтобы минимизировать кривую обучения и сохранить функции аутентификации Spring Cloud Azure 4.0.
public class ExtendAzureStorageBlobsProperties {
public static final String PREFIX = "spring.cloud.azure.storage.blobs";
private boolean enabled = true;
private final List<AzureStorageBlobProperties> configurations = new ArrayList<>();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<AzureStorageBlobProperties> getConfigurations() {
return configurations;
}
}
Динамическая регистрация бобов Storage Blob
Поскольку будет несколько конфигураций Storage Account, мы должны назвать бобы, соответствующие каждому Storage Account. Самый чистый подход — просто использовать имя учетной записи в качестве имени боба.
Теперь давайте динамически зарегистрируем эти бобы в контексте Spring.
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = { "spring.cloud.azure.storage.blobs.enabled"}, havingValue = "true")
public class ExtendStorageBlobsAutoConfiguration implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private Environment environment;
public static final String EXTEND_STORAGE_BLOB_PROPERTIES_BEAN_NAME = "extendAzureStorageBlobsProperties";
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
AzureGlobalProperties azureGlobalProperties =
Binder.get(environment)
.bind(AzureGlobalProperties.PREFIX, AzureGlobalProperties.class)
.orElse(new AzureGlobalProperties());
ExtendAzureStorageBlobsProperties blobsProperties =
Binder.get(environment)
.bind(ExtendAzureStorageBlobsProperties.PREFIX, ExtendAzureStorageBlobsProperties.class)
.orElseThrow(() -> new IllegalArgumentException("Can not bind the azure storage blobs properties."));
// merge properties
for (AzureStorageBlobProperties azureStorageBlobProperties : blobsProperties.getConfigurations()) {
AzureStorageBlobProperties transProperties = new AzureStorageBlobProperties();
AzureGlobalPropertiesUtils.loadProperties(azureGlobalProperties, transProperties);
copyAzureCommonPropertiesIgnoreTargetNull(transProperties, azureStorageBlobProperties);
}
DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory;
registryBeanExtendAzureStorageBlobsProperties(factory, blobsProperties);
blobsProperties.getConfigurations().forEach(blobProperties -> registryBlobBeans(factory, blobProperties));
}
private void registryBeanExtendAzureStorageBlobsProperties(DefaultListableBeanFactory beanFactory,
ExtendAzureStorageBlobsProperties blobsProperties) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExtendAzureStorageBlobsProperties.class,
() -> blobsProperties);
AbstractBeanDefinition rawBeanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
beanFactory.registerBeanDefinition(EXTEND_STORAGE_BLOB_PROPERTIES_BEAN_NAME, rawBeanDefinition);
}
private void registryBlobBeans(DefaultListableBeanFactory beanFactory, AzureStorageBlobProperties blobProperties) {
String accountName = getStorageAccountName(blobProperties);
Assert.hasText(accountName, "accountName can not be null or empty.");
registryBeanStaticConnectionStringProvider(beanFactory, blobProperties, accountName);
registryBeanBlobServiceClientBuilderFactory(beanFactory, blobProperties, accountName);
registryBeanBlobServiceClientBuilder(beanFactory, accountName);
registryBeanBlobServiceClient(beanFactory, accountName);
registryBeanBlobContainerClient(beanFactory, blobProperties, accountName);
registryBeanBlobClient(beanFactory, blobProperties, accountName);
}
private void registryBeanBlobServiceClientBuilder(DefaultListableBeanFactory beanFactory,
String accountName) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BlobServiceClientBuilder.class,
() -> {
BlobServiceClientBuilderFactory builderFactory =
beanFactory.getBean(accountName + BlobServiceClientBuilderFactory.class.getSimpleName(),
BlobServiceClientBuilderFactory.class);
return builderFactory.build();
});
AbstractBeanDefinition rawBeanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
beanFactory.registerBeanDefinition(
accountName + BlobServiceClientBuilder.class.getSimpleName(), rawBeanDefinition);
}
......
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
Расширение AzureStorageBlobProtocolResolver
Следующая задача заключается в том, чтобы сделать любой контейнер разрешаемым одним и тем же resolver’ом шаблонов ресурсов. Указав местоположение ресурса хранения блобов, например azure-blob-accountname://containername/test.txt, резолвер будет использовать его для поиска соответствующего боба BlobServiceClient
по имени Azure Storage Account и вернет ресурс хранения.
public class ExtendAzureStorageBlobProtocolResolver extends ExtendAbstractAzureStorageProtocolResolver {
private final Map<String, BlobServiceClient> blobServiceClientMap = new HashMap<>();
@Override
protected Resource getStorageResource(String location, Boolean autoCreate) {
return new ExtendStorageBlobResource(getBlobServiceClient(location), location, autoCreate);
}
private BlobServiceClient getBlobServiceClient(String locationPrefix) {
String storageAccount = ExtendAzureStorageUtils.getStorageAccountName(locationPrefix, getStorageType());
Assert.notNull(storageAccount, "storageAccount can not be null.");
String accountKey = storageAccount.toLowerCase(Locale.ROOT);
if (blobServiceClientMap.containsKey(accountKey)) {
return blobServiceClientMap.get(accountKey);
}
BlobServiceClient blobServiceClient = beanFactory.getBean(
accountKey + BlobServiceClient.class.getSimpleName(), BlobServiceClient.class);
Assert.notNull(blobServiceClient, "blobServiceClient can not be null.");
blobServiceClientMap.put(accountKey, blobServiceClient);
return blobServiceClient;
}
}
Снова необходимо добавить боб ExtendAzureStorageBlobProtocolResolver
в контекст Spring.
Тестирование Spring Cloud Azure Starter Storage Blob Extend
Вы можете использовать start.spring.io для создания проекта Spring Boot 2.6.7 или более поздней версии с поддержкой Azure Storage (или построить на основе этого примера блоба хранения, если вам так удобнее).
Добавьте зависимость extending starter в файл pom.xml:
<dependency>
<groupId>com.azure.spring.extend</groupId>
<artifactId>spring-cloud-azure-starter-storage-blob-extend</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
Удалите файл src/main/resources/application.properties или добавьте следующий конфигурационный файл application-extend.yml, который позволяет использовать несколько учетных записей хранилища:
application-extend.yml
spring:
cloud:
azure:
storage:
blob:
enabled: false
blobs:
enabled: true
configurations:
- account-name: ${FIRST_ACCOUNT}
container-name: ${FIRST_CONTAINER}
account-key: ${ACCOUNT_KEY_OF_FIRST_ACCOUNT}
- account-name: ${SECOND_ACCOUNT}
container-name: ${SECOND_CONTAINER}
account-key: ${ACCOUNT_KEY_OF_SECOND_ACCOUNT}
ПРИМЕЧАНИЕ: Вы должны предоставить значения для вышеуказанных переменных среды (перечисленных заглавными буквами) с активной информацией о ресурсе Azure Storage Account.
Добавьте класс com.azure.spring.extend.sample.storage.resource.extend.SampleDataInitializer
со следующим телом:
@Profile("extend")
@Component
public class SampleDataInitializer implements CommandLineRunner {
final static Logger logger = LoggerFactory.getLogger(SampleDataInitializer.class);
private final ConfigurableEnvironment env;
private final ExtendAzureStorageBlobProtocolResolver resolver;
private final ExtendAzureStorageBlobsProperties properties;
public SampleDataInitializer(ConfigurableEnvironment env, ExtendAzureStorageBlobProtocolResolver resolver,
ExtendAzureStorageBlobsProperties properties) {
this.env = env;
this.resolver = resolver;
this.properties = properties;
}
/**
* This is used to initialize some data for each Azure Storage Account Blob container.
*/
@Override
public void run(String... args) {
properties.getConfigurations().forEach(this::writeDataByStorageAccount);
}
private void writeDataByStorageAccount(AzureStorageBlobProperties blobProperties) {
String containerName = blobProperties.getContainerName();
if (!StringUtils.hasText(containerName) || blobProperties.getAccountName() == null) {
return;
}
String accountName = getStorageAccountName(blobProperties);
logger.info("Begin to initialize the {} container of the {} account", containerName, accountName);
long currentTimeMillis = System.currentTimeMillis();
String fileName = "fileName-" + currentTimeMillis;
String data = "data" + currentTimeMillis;
Resource storageBlobResource = resolver.getResource("azure-blob-" + accountName + "://" + containerName +"/" + fileName + ".txt");
try (OutputStream os = ((WritableResource) storageBlobResource).getOutputStream()) {
os.write(data.getBytes());
logger.info("Write data to container={}, fileName={}.txt", containerName, fileName);
} catch (IOException e) {
logger.error("Write data exception", e);
}
logger.info("End to initialize the {} container of the {} account", containerName, accountName);
}
}
Запустите образец с помощью следующей команды Maven:
mvn clean spring-boot:run -Dspring-boot.run.profiles=extend
Наконец, проверьте ожидаемый результат. В вашей консоли должно появиться следующее сообщение:
c.a.s.e.s.s.r.e.SampleDataInitializer : Begin to initialize the container first of the account firstaccount.
c.a.s.e.s.s.r.e.SampleDataInitializer : Write data to container=first, fileName=fileName-1656641340271.txt
c.a.s.e.s.s.r.e.SampleDataInitializer : End to initialize the container first of the account firstaccount.
c.a.s.e.s.s.r.e.SampleDataInitializer : Begin to initialize the container second of the account secondaccount.
c.a.s.e.s.s.r.e.SampleDataInitializer : Write data to container=second, fileName=fileName-1656641343572.txt
c.a.s.e.s.s.r.e.SampleDataInitializer : End to initialize the container second of the account secondaccount.
Весь код примера проекта опубликован в репозитории spring-cloud-azure-starter-storage-blob-extend-sample.
В этом расширенном приложении все еще можно вернуться к первоначальному использованию Spring Cloud Azure Starter Storage Blob с одной учетной записью хранения, добавив следующий файл конфигурации application-current.yml:
spring:
cloud:
azure:
storage:
blob:
account-name: ${FIRST_ACCOUNT}
container-name: ${FIRST_CONTAINER}
account-key: ${ACCOUNT_KEY_OF_FIRST_ACCOUNT}
current:
second-container: ${SECOND_CONTAINER}
ПРИМЕЧАНИЕ: Вы должны установить или заменить перечисленные значения, присвоенные переменным окружения, информацией об активном ресурсе Azure Storage Account.
Запустите образец с помощью следующей команды Maven:
mvn clean spring-boot:run -Dspring-boot.run.profiles=current
Чтобы убедиться в правильности работы с использованием одной учетной записи хранилища, сравните вывод терминала с приведенным здесь:
c.a.s.e.s.s.r.c.SampleDataInitializer : StorageApplication data initialization of 'first-container' begin ...
c.a.s.e.s.s.r.c.SampleDataInitializer : Write data to container=first-container, fileName=fileName1656641162614.txt
c.a.s.e.s.s.r.c.SampleDataInitializer : StorageApplication data initialization of 'first-container' end ...
c.a.s.e.s.s.r.c.SampleDataInitializer : StorageApplication data initialization of 'second-container' begin ...
c.a.s.e.s.s.r.c.SampleDataInitializer : Write data to container=second-container, fileName=fileName1656641165411.txt
c.a.s.e.s.s.r.c.SampleDataInitializer : StorageApplication data initialization of 'second-container' end ...
Заключение
Реализация определенного типа ресурсов и соответствующего шаблона резольвера относительно проста, во многом благодаря понятной документации, множеству встроенных реализаций, общему использованию в технологическом стеке Spring.
Один момент, который заслуживает внимания, это определение протокола для ресурса, например, Azure Storage Blob Resource. Мы должны отметить, используем ли мы azure-blob:// или azure-blob-[account-name]://, и спланировать возможности приложения соответствующим образом. Кроме того, поскольку идентификатор сетевого ресурса должен быть однозначно идентифицируемым, последний формат расположения может привести к гораздо более длинному имени и также раскрывает имя учетной записи хранилища. Эти компромиссы должны быть оценены в свете требований и профиля риска.
Ссылки и полезные ресурсы
- Последняя документация по ресурсам Spring
- Расширенный стартер для Spring Cloud Azure Starter Storage Blob и пример на GitHub