{"id":229292,"date":"2022-08-17T06:31:49","date_gmt":"2022-08-17T13:31:49","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/java\/?p=229292"},"modified":"2022-08-17T06:36:24","modified_gmt":"2022-08-17T13:36:24","slug":"a-concise-guide-to-using-multiple-azure-storage-accounts-from-a-single-spring-boot-application","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/java\/a-concise-guide-to-using-multiple-azure-storage-accounts-from-a-single-spring-boot-application\/","title":{"rendered":"A Concise Guide to Using Multiple Azure Storage Accounts from a Single Spring Boot Application"},"content":{"rendered":"<p>Spring projects in general are opinionated: 80-90% of use cases are handled &#8220;by default&#8221;, and code is often much more concise than would be required otherwise due to Spring&#8217;s preference of convention over configuration. These and other &#8220;opinions&#8221; can result in dramatically less code to write and maintain and as a result, more focused impact.<\/p>\n<p>In the vast majority of cases where Azure Storage is used from an application, there is no compelling advantage to using more than a single Azure storage account. But there are edge cases, and having the ability to use multiple Azure Storage accounts from a single app &#8211; even if we might only need that capability around 10% of the time &#8211; could provide an incredibly useful extension of our storage superpowers.<\/p>\n<p>This article is the result of a collaboration with Shi Li Chen.<\/p>\n<h2>It&#8217;s all about resources<\/h2>\n<p>The Spring Framework defines the\u00a0<code>Resource<\/code>\u00a0interface and provides several implementations built upon\u00a0<code>Resource<\/code>\u00a0to facilitate developer access to low-level resources. In order to handle a particular kind of resource, two things are required:<\/p>\n<ul>\n<li>A\u00a0<code>Resource<\/code>\u00a0implementation<\/li>\n<li>A\u00a0<code>ResourcePatternResolver<\/code>\u00a0implementation<\/li>\n<\/ul>\n<p>A Spring application evaluates resources in question using one or more registered resolvers. When the type of resource is identified, the appropriate\u00a0<code>Resource<\/code>\u00a0implementation is used to access and\/or manipulate the underlying resource.<\/p>\n<p>If the implementations built into Spring Framework don&#8217;t fulfill your use case, it&#8217;s fairly straightforward to add support for additional types of resources by defining your own implementations of\u00a0<code>AbstractResource<\/code>\u00a0and\u00a0<code>ResourcePatternResolver<\/code>\u00a0interfaces.<\/p>\n<p>This article will introduce the Spring Resource, review Spring Cloud Azure&#8217;s implementation of Spring&#8217;s\u00a0<code>Resource<\/code>\u00a0(especially with regard to Azure Storage Account considerations and limitations), and consider how to expand said implementation to address those edge cases in which it would be useful to access multiple Azure Storage Accounts from a single Spring Boot application.<\/p>\n<h2>Getting resourceful<\/h2>\n<p>We&#8217;ve already mentioned that the Spring Framework defines several useful\u00a0<a href=\"https:\/\/docs.spring.io\/spring-framework\/docs\/current\/reference\/html\/core.html#resources-implementations\">Resource implementations<\/a>. As of this writing, the default types are:<\/p>\n<ul>\n<li><code>UrlResource<\/code><\/li>\n<li><code>ClassPathResource<\/code><\/li>\n<li><code>FileSystemResource<\/code><\/li>\n<li><code>PathResource<\/code><\/li>\n<li><code>ServletContextResource<\/code><\/li>\n<li><code>InputStreamResource<\/code><\/li>\n<li><code>ByteArrayResource<\/code><\/li>\n<\/ul>\n<p>As mentioned earlier, each resource will have a corresponding resource resolver.<\/p>\n<p>Enabling your Spring Boot application to use a custom\u00a0<code>Resource<\/code>\u00a0requires the following actions:<\/p>\n<ul>\n<li>Implement the\u00a0<code>Resource<\/code>\u00a0interface by extending\u00a0<code>AbstractResource<\/code><\/li>\n<li>Implement the\u00a0<code>ResourcePatternResolver<\/code>\u00a0interface to resolve the custom resource type<\/li>\n<li>Register the implementation of\u00a0<code>ResourcePatternResolver<\/code>\u00a0as a bean<\/li>\n<\/ul>\n<p>NOTE: Your resolver must be added to the default resource loader&#8217;s resolver set using the\u00a0<code>org.springframework.core.io.DefaultResourceLoader#addProtocolResolver<\/code>\u00a0method, but this code is present in\u00a0<code>AbstractAzureStorageProtocolResolver<\/code>; extending that class to create your implementation accomplishes this on your behalf unless you choose to override its\u00a0<code>setResourceLoader<\/code>\u00a0method.<\/p>\n<p>A\u00a0<code>ResourceLoader<\/code>\u00a0attempts to resolve each\u00a0<code>Resource<\/code>\u00a0by comparing its defined location\/format with all registered protocol pattern resolvers until a non-null resource is returned. If no match is found, the\u00a0<code>Resource<\/code>\u00a0will be evaluated against Spring&#8217;s built-in pattern resolvers.<\/p>\n<h2><a href=\"https:\/\/dev.to\/mkheck\/a-concise-guide-to-using-multiple-azure-storage-accounts-from-a-single-spring-boot-application-5chf#spring-resources-in-spring-cloud-azure\" name=\"spring-resources-in-spring-cloud-azure\"><\/a>Spring resources in Spring Cloud Azure<\/h2>\n<p>Spring Cloud Azure provides two Spring resource and resource pattern resolver implementations. In this article, we only discuss the implementation of the Azure Storage Blob resource. You can examine the source code for Spring Cloud Azure\u00a0<code>Resources<\/code>\u00a0at\u00a0<a href=\"https:\/\/github.com\/Azure\/azure-sdk-for-java\/tree\/spring-cloud-azure-starter-storage-blob_4.2.0\/sdk\/spring\/spring-cloud-azure-core\/src\/main\/java\/com\/azure\/spring\/cloud\/core\/resource\">Spring Cloud Azure<\/a>\u00a0and related documentation at\u00a0<a href=\"https:\/\/microsoft.github.io\/spring-cloud-azure\/4.2.0\/reference\/html\/index.html#resource-handling\">Resource Handling<\/a>.<\/p>\n<p>NOTE: We use\u00a0<a href=\"https:\/\/mvnrepository.com\/artifact\/com.azure.spring\/spring-cloud-azure-starter-storage-blob\/4.2.0\">Spring Cloud Azure Starter Storage Blob version 4.2.0<\/a>\u00a0for analysis and experiments.<\/p>\n<h2><a href=\"https:\/\/dev.to\/mkheck\/a-concise-guide-to-using-multiple-azure-storage-accounts-from-a-single-spring-boot-application-5chf#implementation-of-raw-abstractresource-endraw-\" name=\"implementation-of-raw-abstractresource-endraw-\"><\/a>Implementation of\u00a0<code>AbstractResource<\/code><\/h2>\n<p>The abstract implementation\u00a0<code>AzureStorageResource<\/code>\u00a0for Spring Cloud Azure primarily defines the format of the Azure storage resource protocol and accommodates the unique attributes of the Azure Storage Account service, e.g. the container name and file name. It is important to note that\u00a0<code>AzureStorageResource<\/code>\u00a0is decoupled from the Azure Storage SDK.<\/p>\n<p>The Spring Framework interface\u00a0<code>WritableResource<\/code>\u00a0represents the underlying API we build upon to read from and write to the Azure Storage resource.<\/p>\n<pre class=\"prettyprint\">abstract class AzureStorageResource extends AbstractResource implements WritableResource {\r\n\r\n    private boolean isAzureStorageResource(@NonNull String location) {\r\n        ......\r\n    }\r\n\r\n    String getContainerName(String location) {\r\n        ......\r\n    }\r\n\r\n    String getContentType(String location) {\r\n        ......\r\n    }\r\n\r\n    String getFilename(String location) {\r\n        ......\r\n    }\r\n\r\n    abstract StorageType getStorageType();\r\n}<\/pre>\n<p class=\"highlight plaintext\">The\u00a0<code>StorageBlobResource<\/code>\u00a0is Spring Cloud Azure Storage Blob&#8217;s implementation of the abstract class\u00a0<code>AbstractResource<\/code>. We can see\u00a0<code>StorageBlobResource<\/code>\u00a0uses the\u00a0<code>BlobServiceClient<\/code>\u00a0from the Azure Storage Blob SDK to implement all abstract methods, relying on the service client to interact with the Azure Storage Blob service.<\/p>\n<pre class=\"prettyprint\">public final class StorageBlobResource extends AzureStorageResource {\r\n    private final BlobServiceClient blobServiceClient;\r\n    private final BlobContainerClient blobContainerClient;\r\n    private final BlockBlobClient blockBlobClient;\r\n\r\n    public StorageBlobResource(BlobServiceClient blobServiceClient, String location, Boolean autoCreateFiles,\r\n                               String snapshot, String versionId, String contentType) {\r\n        ......\r\n        this.blobContainerClient = blobServiceClient.getBlobContainerClient(getContainerName(location));\r\n        BlobClient blobClient = blobContainerClient.getBlobClient(getFilename(location));\r\n        this.blockBlobClient = blobClient.getBlockBlobClient();\r\n    }\r\n\r\n    @Override\r\n    public OutputStream getOutputStream() throws IOException {\r\n        try {\r\n            ......\r\n            return this.blockBlobClient.getBlobOutputStream(options);\r\n        } catch (BlobStorageException e) {\r\n            throw new IOException(MSG_FAIL_OPEN_OUTPUT, e);\r\n        }\r\n    }\r\n\r\n    ......\r\n\r\n    @Override\r\n    StorageType getStorageType() {\r\n        return StorageType.BLOB;\r\n    }\r\n}<\/pre>\n<h2>Implementation of\u00a0<code>ResourcePatternResolver<\/code><\/h2>\n<p>Spring Cloud Azure provides an abstract implementation\u00a0<code>AbstractAzureStorageProtocolResolver<\/code>. This class incorporates general processing of the Azure storage resource protocol, exposes specific capabilities of the Azure Storage Account service, and adds the requisite logic to the default resource loader. Like\u00a0<code>AzureStorageResource<\/code>, the\u00a0<code>AbstractAzureStorageProtocolResolver<\/code>\u00a0is also not coupled to the Azure Storage SDK.<\/p>\n<pre class=\"prettyprint\">public abstract class AbstractAzureStorageProtocolResolver implements ProtocolResolver, ResourcePatternResolver,\r\n    ResourceLoaderAware, BeanFactoryPostProcessor {\r\n\r\n    protected final AntPathMatcher matcher = new AntPathMatcher();\r\n\r\n    protected abstract StorageType getStorageType();\r\n\r\n    protected abstract Resource getStorageResource(String location, Boolean autoCreate);\r\n\r\n    protected ConfigurableListableBeanFactory beanFactory;\r\n\r\n    protected abstract Stream&lt;StorageContainerItem&gt; listStorageContainers(String containerPrefix);\r\n\r\n    protected abstract StorageContainerClient getStorageContainerClient(String name);\r\n\r\n    @Override\r\n    public void setResourceLoader(ResourceLoader resourceLoader) {\r\n        if (resourceLoader instanceof DefaultResourceLoader) {\r\n            ((DefaultResourceLoader) resourceLoader).addProtocolResolver(this);\r\n        } else {\r\n            LOGGER.warn(\"Custom Protocol using azure-{}:\/\/ prefix will not be enabled.\", getStorageType().getType());\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {\r\n        this.beanFactory = beanFactory;\r\n    }\r\n\r\n    @Override\r\n    public Resource resolve(String location, ResourceLoader resourceLoader) {\r\n        if (AzureStorageUtils.isAzureStorageResource(location, getStorageType())) {\r\n            return getResource(location);\r\n        }\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public Resource[] getResources(String pattern) throws IOException {\r\n        Resource[] resources = null;\r\n\r\n        if (AzureStorageUtils.isAzureStorageResource(pattern, getStorageType())) {\r\n            if (matcher.isPattern(AzureStorageUtils.stripProtocol(pattern, getStorageType()))) {\r\n                String containerPattern = AzureStorageUtils.getContainerName(pattern, getStorageType());\r\n                String filePattern = AzureStorageUtils.getFilename(pattern, getStorageType());\r\n                resources = resolveResources(containerPattern, filePattern);\r\n            } else {\r\n                return new Resource[] { getResource(pattern) };\r\n            }\r\n        }\r\n        if (null == resources) {\r\n            throw new IOException(\"Resources not found at \" + pattern);\r\n        }\r\n        return resources;\r\n    }\r\n\r\n    @Override\r\n    public Resource getResource(String location) {\r\n        Resource resource = null;\r\n\r\n        if (AzureStorageUtils.isAzureStorageResource(location, getStorageType())) {\r\n            resource = getStorageResource(location, true);\r\n        }\r\n\r\n        if (null == resource) {\r\n            throw new IllegalArgumentException(\"Resource not found at \" + location);\r\n        }\r\n        return resource;\r\n    }\r\n\r\n    \/**\r\n     * Storage container item.\r\n     *\/\r\n    protected static class StorageContainerItem {\r\n        private final String name;\r\n        ......\r\n    }\r\n\r\n    protected static class StorageItem {\r\n\r\n        private final String container;\r\n        private final String name;\r\n        private final StorageType storageType;\r\n        ......\r\n    }\r\n\r\n    protected interface StorageContainerClient {\r\n\r\n        ......\r\n    }\r\n}<\/pre>\n<p>The resource resolver\u00a0<code>AzureStorageBlobProtocolResolver<\/code>\u00a0is Spring Cloud Azure Storage Blob&#8217;s implementation of\u00a0<code>ResourcePatternResolver<\/code>. It encapsulates resources according to the location or storage item pattern based on\u00a0<code>BlobServiceClient<\/code>\u00a0and returns the associated\u00a0<code>StorageBlobResource<\/code>.<\/p>\n<pre class=\"prettyprint\">public final class AzureStorageBlobProtocolResolver extends AbstractAzureStorageProtocolResolver {\r\n\r\n    private BlobServiceClient blobServiceClient;\r\n    @Override\r\n    protected StorageType getStorageType() {\r\n        return StorageType.BLOB;\r\n    }\r\n\r\n    @Override\r\n    protected Resource getStorageResource(String location, Boolean autoCreate) {\r\n        return new StorageBlobResource(getBlobServiceClient(), location, autoCreate);\r\n    }\r\n\r\n    private BlobServiceClient getBlobServiceClient() {\r\n        if (blobServiceClient == null) {\r\n            blobServiceClient = beanFactory.getBean(BlobServiceClient.class);\r\n        }\r\n        return blobServiceClient;\r\n    }\r\n}<\/pre>\n<h2>Opinions<\/h2>\n<p>As mentioned at the beginning of this post, the default capabilities fulfill the requirements admirably in the vast majority of circumstances. But in accordance with the Spring ethos, Spring Cloud Azure Starter Storage Blob was designed to seamlessly address 80-90% of use cases &#8220;out of the box&#8221;, while still allowing for remaining (edge) cases with some extra effort.<\/p>\n<p>As written, the storage blob resource supports multiple container operations using the same storage account. The salient point is that the blob paths under different containers can be properly resolved into\u00a0<code>StorageBlobResource<\/code>\u00a0objects. Combining the earlier code for\u00a0<code>StorageBlobResource<\/code>, the blob resource must hold a blob service client, and if\u00a0<code>blobServiceClient.getBlobContainerClient(getContainerName(location))<\/code>\u00a0successfully returns a\u00a0<code>BlobServiceClient<\/code>, the blob resource can be resolved and retrieved.<\/p>\n<p>The\u00a0<code>BlobServiceClient<\/code>\u00a0bean represents an Azure Storage Account in the Azure Storage Blob SDK, meaning that the current implementation does not support simultaneous availability using multiple Azure Storage Accounts.<\/p>\n<h2><a href=\"https:\/\/dev.to\/mkheck\/a-concise-guide-to-using-multiple-azure-storage-accounts-from-a-single-spring-boot-application-5chf#developing-an-extended-version-of-spring-cloud-azure-starter-storage-blob\" name=\"developing-an-extended-version-of-spring-cloud-azure-starter-storage-blob\"><\/a>Developing an extended version of Spring Cloud Azure Starter Storage Blob<\/h2>\n<p>For those rare cases in which it might be useful to simultaneously access multiple Azure Storage accounts from the same application, there is a way to make that happen. To demonstrate this capability, let&#8217;s create a new library called\u00a0<code>spring-cloud-azure-starter-storage-blob-extend<\/code>. The only external dependency for this new library is the existing\u00a0<code>spring-cloud-azure-starter-storage-blob<\/code>.<\/p>\n<h2><a href=\"https:\/\/dev.to\/mkheck\/a-concise-guide-to-using-multiple-azure-storage-accounts-from-a-single-spring-boot-application-5chf#extend-the-storage-blob-properties\" name=\"extend-the-storage-blob-properties\"><\/a>Extend the Storage Blob properties<\/h2>\n<p>While the primary goal is to support multiple storage accounts, a secondary design goal is to use a similar structure to\u00a0<code>AzureStorageBlobProperties<\/code>\u00a0in order to minimize the learning curve and to retain Spring Cloud Azure 4.0&#8217;s out of the box authentication features.<\/p>\n<pre class=\"prettyprint\">public class ExtendAzureStorageBlobsProperties {\r\n\r\n    public static final String PREFIX = \"spring.cloud.azure.storage.blobs\";\r\n\r\n    private boolean enabled = true;\r\n\r\n    private final List&lt;AzureStorageBlobProperties&gt; configurations = new ArrayList&lt;&gt;();\r\n\r\n    public boolean isEnabled() {\r\n        return enabled;\r\n    }\r\n\r\n    public void setEnabled(boolean enabled) {\r\n        this.enabled = enabled;\r\n    }\r\n\r\n    public List&lt;AzureStorageBlobProperties&gt; getConfigurations() {\r\n        return configurations;\r\n    }\r\n}<\/pre>\n<h2>Dynamically register Storage Blob beans<\/h2>\n<p>Since there will be multiple Storage Account configurations, we must name the beans corresponding to each storage account. The cleanest approach is to simply use the account name as the bean name.<\/p>\n<p>Now, let&#8217;s dynamically register these beans with the Spring context.<\/p>\n<pre class=\"prettyprint\">@Configuration(proxyBeanMethods = false)\r\n@ConditionalOnProperty(value = { \"spring.cloud.azure.storage.blobs.enabled\"}, havingValue = \"true\")\r\npublic class ExtendStorageBlobsAutoConfiguration implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {\r\n\r\n    private Environment environment;\r\n\r\n    public static final String EXTEND_STORAGE_BLOB_PROPERTIES_BEAN_NAME = \"extendAzureStorageBlobsProperties\";\r\n\r\n    @Override\r\n    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {\r\n        AzureGlobalProperties azureGlobalProperties =\r\n            Binder.get(environment)\r\n                  .bind(AzureGlobalProperties.PREFIX, AzureGlobalProperties.class)\r\n                  .orElse(new AzureGlobalProperties());\r\n        ExtendAzureStorageBlobsProperties blobsProperties =\r\n            Binder.get(environment)\r\n                  .bind(ExtendAzureStorageBlobsProperties.PREFIX, ExtendAzureStorageBlobsProperties.class)\r\n                  .orElseThrow(() -&gt; new IllegalArgumentException(\"Can not bind the azure storage blobs properties.\"));\r\n        \/\/ merge properties\r\n        for (AzureStorageBlobProperties azureStorageBlobProperties : blobsProperties.getConfigurations()) {\r\n            AzureStorageBlobProperties transProperties = new AzureStorageBlobProperties();\r\n            AzureGlobalPropertiesUtils.loadProperties(azureGlobalProperties, transProperties);\r\n            copyAzureCommonPropertiesIgnoreTargetNull(transProperties, azureStorageBlobProperties);\r\n        }\r\n\r\n        DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory;\r\n        registryBeanExtendAzureStorageBlobsProperties(factory, blobsProperties);\r\n        blobsProperties.getConfigurations().forEach(blobProperties -&gt; registryBlobBeans(factory, blobProperties));\r\n    }\r\n\r\n    private void registryBeanExtendAzureStorageBlobsProperties(DefaultListableBeanFactory beanFactory,\r\n                                                               ExtendAzureStorageBlobsProperties blobsProperties) {\r\n        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExtendAzureStorageBlobsProperties.class,\r\n            () -&gt; blobsProperties);\r\n        AbstractBeanDefinition rawBeanDefinition = beanDefinitionBuilder.getRawBeanDefinition();\r\n        beanFactory.registerBeanDefinition(EXTEND_STORAGE_BLOB_PROPERTIES_BEAN_NAME, rawBeanDefinition);\r\n    }\r\n\r\n    private void registryBlobBeans(DefaultListableBeanFactory beanFactory, AzureStorageBlobProperties blobProperties) {\r\n        String accountName = getStorageAccountName(blobProperties);\r\n        Assert.hasText(accountName, \"accountName can not be null or empty.\");\r\n        registryBeanStaticConnectionStringProvider(beanFactory, blobProperties, accountName);\r\n        registryBeanBlobServiceClientBuilderFactory(beanFactory, blobProperties, accountName);\r\n        registryBeanBlobServiceClientBuilder(beanFactory, accountName);\r\n        registryBeanBlobServiceClient(beanFactory, accountName);\r\n        registryBeanBlobContainerClient(beanFactory, blobProperties, accountName);\r\n        registryBeanBlobClient(beanFactory, blobProperties, accountName);\r\n    }\r\n\r\n    private void registryBeanBlobServiceClientBuilder(DefaultListableBeanFactory beanFactory,\r\n                                                      String accountName) {\r\n        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BlobServiceClientBuilder.class,\r\n            () -&gt; {\r\n                BlobServiceClientBuilderFactory builderFactory =\r\n                    beanFactory.getBean(accountName + BlobServiceClientBuilderFactory.class.getSimpleName(),\r\n                        BlobServiceClientBuilderFactory.class);\r\n                return builderFactory.build();\r\n            });\r\n        AbstractBeanDefinition rawBeanDefinition = beanDefinitionBuilder.getRawBeanDefinition();\r\n        beanFactory.registerBeanDefinition(\r\n            accountName + BlobServiceClientBuilder.class.getSimpleName(), rawBeanDefinition);\r\n    }\r\n\r\n    ......\r\n\r\n    @Override\r\n    public void setEnvironment(Environment environment) {\r\n        this.environment = environment;\r\n    }\r\n}<\/pre>\n<h2>Extend the\u00a0<code>AzureStorageBlobProtocolResolver<\/code><\/h2>\n<p>The next task is to make any container resolvable by the same resource pattern resolver. Specifying a storage blob resource location such as\u00a0<strong>azure-blob-accountname:\/\/containername\/test.txt<\/strong>, the resolver will use that to locate the appropriate\u00a0<code>BlobServiceClient<\/code>\u00a0bean by Azure Storage Account name and return the storage resource.<\/p>\n<pre class=\"prettyprint\">public class ExtendAzureStorageBlobProtocolResolver extends ExtendAbstractAzureStorageProtocolResolver {\r\n    private final Map&lt;String, BlobServiceClient&gt; blobServiceClientMap = new HashMap&lt;&gt;();\r\n\r\n    @Override\r\n    protected Resource getStorageResource(String location, Boolean autoCreate) {\r\n        return new ExtendStorageBlobResource(getBlobServiceClient(location), location, autoCreate);\r\n    }\r\n\r\n    private BlobServiceClient getBlobServiceClient(String locationPrefix) {\r\n        String storageAccount = ExtendAzureStorageUtils.getStorageAccountName(locationPrefix, getStorageType());\r\n        Assert.notNull(storageAccount, \"storageAccount can not be null.\");\r\n        String accountKey = storageAccount.toLowerCase(Locale.ROOT);\r\n        if (blobServiceClientMap.containsKey(accountKey)) {\r\n            return blobServiceClientMap.get(accountKey);\r\n        }\r\n\r\n        BlobServiceClient blobServiceClient = beanFactory.getBean(\r\n            accountKey + BlobServiceClient.class.getSimpleName(), BlobServiceClient.class);\r\n        Assert.notNull(blobServiceClient, \"blobServiceClient can not be null.\");\r\n        blobServiceClientMap.put(accountKey, blobServiceClient);\r\n        return blobServiceClient;\r\n    }\r\n}<\/pre>\n<p>Again, you need to add the bean\u00a0<code>ExtendAzureStorageBlobProtocolResolver<\/code>\u00a0to the Spring context.<\/p>\n<h2><a href=\"https:\/\/dev.to\/mkheck\/a-concise-guide-to-using-multiple-azure-storage-accounts-from-a-single-spring-boot-application-5chf#testing-the-spring-cloud-azure-starter-storage-blob-extend\" name=\"testing-the-spring-cloud-azure-starter-storage-blob-extend\"><\/a>Testing the Spring Cloud Azure Starter Storage Blob Extend<\/h2>\n<p>You can use\u00a0<a href=\"https:\/\/start.spring.io\/\">start.spring.io<\/a>\u00a0to generate a Spring Boot 2.6.7 or greater project with Azure Storage support (or build on\u00a0<a href=\"https:\/\/github.com\/Azure-Samples\/azure-spring-boot-samples\/tree\/spring-cloud-azure_v4.2.0\/storage\/spring-cloud-azure-starter-storage-blob\/storage-blob-sample\">this storage blob sample<\/a>\u00a0if you prefer).<\/p>\n<p>Add the extending starter dependency to the\u00a0<em>pom.xml<\/em>\u00a0file:<\/p>\n<pre class=\"prettyprint\">&lt;dependency&gt;\r\n  &lt;groupId&gt;com.azure.spring.extend&lt;\/groupId&gt;\r\n  &lt;artifactId&gt;spring-cloud-azure-starter-storage-blob-extend&lt;\/artifactId&gt;\r\n  &lt;version&gt;1.0-SNAPSHOT&lt;\/version&gt;\r\n&lt;\/dependency&gt;\r\n\r\n<\/pre>\n<p>Delete the\u00a0<em>src\/main\/resources\/application.properties<\/em>\u00a0file or add the following configuration file\u00a0<em>application-extend.yml<\/em>, which enables multiple storage account usage:<\/p>\n<p><em>application-extend.yml<\/em><\/p>\n<pre class=\"prettyprint\">spring:\r\n  cloud:\r\n    azure:\r\n      storage:\r\n        blob:\r\n          enabled: false\r\n        blobs:\r\n          enabled: true\r\n          configurations:\r\n            - account-name: ${FIRST_ACCOUNT}\r\n              container-name: ${FIRST_CONTAINER}\r\n              account-key: ${ACCOUNT_KEY_OF_FIRST_ACCOUNT}\r\n            - account-name: ${SECOND_ACCOUNT}\r\n              container-name: ${SECOND_CONTAINER}\r\n              account-key: ${ACCOUNT_KEY_OF_SECOND_ACCOUNT}<\/pre>\n<p>NOTE: You must provide values for the environment variables above (listed in all capital letters) with active Azure Storage Account resource information.<\/p>\n<p>Add class\u00a0<code>com.azure.spring.extend.sample.storage.resource.extend.SampleDataInitializer<\/code>\u00a0with the following body:<\/p>\n<pre class=\"prettyprint\">@Profile(\"extend\")\r\n@Component\r\npublic class SampleDataInitializer implements CommandLineRunner {\r\n    final static Logger logger = LoggerFactory.getLogger(SampleDataInitializer.class);\r\n\r\n    private final ConfigurableEnvironment env;\r\n\r\n    private final ExtendAzureStorageBlobProtocolResolver resolver;\r\n    private final ExtendAzureStorageBlobsProperties properties;\r\n\r\n    public SampleDataInitializer(ConfigurableEnvironment env, ExtendAzureStorageBlobProtocolResolver resolver,\r\n                                 ExtendAzureStorageBlobsProperties properties) {\r\n        this.env = env;\r\n        this.resolver = resolver;\r\n        this.properties = properties;\r\n    }\r\n\r\n    \/**\r\n     * This is used to initialize some data for each Azure Storage Account Blob container.\r\n     *\/\r\n    @Override\r\n    public void run(String... args) {\r\n        properties.getConfigurations().forEach(this::writeDataByStorageAccount);\r\n    }\r\n\r\n    private void writeDataByStorageAccount(AzureStorageBlobProperties blobProperties) {\r\n        String containerName = blobProperties.getContainerName();\r\n        if (!StringUtils.hasText(containerName) || blobProperties.getAccountName() == null) {\r\n            return;\r\n        }\r\n\r\n        String accountName = getStorageAccountName(blobProperties);\r\n        logger.info(\"Begin to initialize the {} container of the {} account\", containerName, accountName);\r\n        long currentTimeMillis = System.currentTimeMillis();\r\n        String fileName = \"fileName-\" + currentTimeMillis;\r\n        String data = \"data\" + currentTimeMillis;\r\n        Resource storageBlobResource = resolver.getResource(\"azure-blob-\" + accountName + \":\/\/\" + containerName +\"\/\" + fileName + \".txt\");\r\n        try (OutputStream os = ((WritableResource) storageBlobResource).getOutputStream()) {\r\n            os.write(data.getBytes());\r\n            logger.info(\"Write data to container={}, fileName={}.txt\", containerName, fileName);\r\n        } catch (IOException e) {\r\n            logger.error(\"Write data exception\", e);\r\n        }\r\n        logger.info(\"End to initialize the {} container of the {} account\", containerName, accountName);\r\n    }\r\n}<\/pre>\n<p>Run the sample with following Maven command:<\/p>\n<p><code>mvn clean spring-boot:run -Dspring-boot.run.profiles=extend<\/code><\/p>\n<p>Finally, verify the expected outcome. Your console should display the following output:<\/p>\n<pre class=\"prettyprint\">c.a.s.e.s.s.r.e.SampleDataInitializer    : Begin to initialize the container first of the account firstaccount.\r\nc.a.s.e.s.s.r.e.SampleDataInitializer    : Write data to container=first, fileName=fileName-1656641340271.txt\r\nc.a.s.e.s.s.r.e.SampleDataInitializer    : End to initialize the container first of the account firstaccount.\r\nc.a.s.e.s.s.r.e.SampleDataInitializer    : Begin to initialize the container second of the account secondaccount.\r\nc.a.s.e.s.s.r.e.SampleDataInitializer    : Write data to container=second, fileName=fileName-1656641343572.txt\r\nc.a.s.e.s.s.r.e.SampleDataInitializer    : End to initialize the container second of the account secondaccount.<\/pre>\n<p>All sample project code is published at the repository\u00a0<a href=\"https:\/\/github.com\/moarychan\/spring-cloud-azure-starter-storage-blob-extend-sample\/tree\/main\/spring-cloud-azure-starter-storage-blob-extend-sample\">spring-cloud-azure-starter-storage-blob-extend-sample<\/a>.<\/p>\n<p>Within this extended application, it&#8217;s still possible to revert to the original, single storage account usage of Spring Cloud Azure Starter Storage Blob by adding the following configuration file\u00a0<em>application-current.yml<\/em>:<\/p>\n<pre class=\"prettyprint\">spring:\r\n  cloud:\r\n    azure:\r\n      storage:\r\n        blob:\r\n          account-name: ${FIRST_ACCOUNT}\r\n          container-name: ${FIRST_CONTAINER}\r\n          account-key: ${ACCOUNT_KEY_OF_FIRST_ACCOUNT}\r\ncurrent:\r\n  second-container: ${SECOND_CONTAINER}<\/pre>\n<p>NOTE: You must set or replace the listed environment variable assigned values with active Azure Storage Account resource information.<\/p>\n<p>Run the sample with following Maven command:<\/p>\n<p><code>mvn clean spring-boot:run -Dspring-boot.run.profiles=current<\/code><\/p>\n<p>To verify correct operation using a single storage account, compare terminal output with that listed here:<\/p>\n<pre class=\"prettyprint\">c.a.s.e.s.s.r.c.SampleDataInitializer    : StorageApplication data initialization of 'first-container' begin ...\r\nc.a.s.e.s.s.r.c.SampleDataInitializer    : Write data to container=first-container, fileName=fileName1656641162614.txt\r\nc.a.s.e.s.s.r.c.SampleDataInitializer    : StorageApplication data initialization of 'first-container' end ...\r\nc.a.s.e.s.s.r.c.SampleDataInitializer    : StorageApplication data initialization of 'second-container' begin ...\r\nc.a.s.e.s.s.r.c.SampleDataInitializer    : Write data to container=second-container, fileName=fileName1656641165411.txt\r\nc.a.s.e.s.s.r.c.SampleDataInitializer    : StorageApplication data initialization of 'second-container' end ...<\/pre>\n<h2>Conclusion<\/h2>\n<p>Implementing a specific resource type and corresponding pattern resolver is relatively simple, largely thanks to clear documentation, the many built-in implementations, common usage within the Spring technology stack.<\/p>\n<p>One point that warrants attention is the protocol definition for the resource, e.g. the Azure Storage Blob Resource. We must note whether we are using\u00a0<strong>azure-blob:\/\/<\/strong>\u00a0or\u00a0<strong>azure-blob-[account-name]:\/\/<\/strong>\u00a0and plan app capabilities accordingly. Additionally, since the identifier of a network resource must be uniquely identifiable, the latter location format may result in a much longer name and also exposes the name of the storage account. These tradeoffs need to be evaluated in light of requirements and risk profile.<\/p>\n<h2><a href=\"https:\/\/dev.to\/mkheck\/a-concise-guide-to-using-multiple-azure-storage-accounts-from-a-single-spring-boot-application-5chf#references-amp-useful-resources\" name=\"references-amp-useful-resources\"><\/a>References &amp; Useful Resources<\/h2>\n<ul>\n<li><a href=\"https:\/\/docs.spring.io\/spring-framework\/docs\/current\/reference\/html\/core.html#resources\">Latest Spring Resources documentation<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/moarychan\/spring-cloud-azure-starter-storage-blob-extend-sample\">Extended starter for Spring Cloud Azure Starter Storage Blob and sample on GitHub<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Spring projects in general are opinionated: 80-90% of use cases are handled &#8220;by default&#8221;, and code is often much more concise than would be required otherwise due to Spring&#8217;s preference of convention over configuration. These and other &#8220;opinions&#8221; can result in dramatically less code to write and maintain and as a result, more focused impact. [&hellip;]<\/p>\n","protected":false},"author":97204,"featured_media":227205,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[14,1,8,249],"tags":[13,759,248,316,761,538],"class_list":["post-229292","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cloud","category-java","category-open-source","category-openjdk","tag-azure","tag-azure-spring-cloud","tag-java","tag-spring","tag-spring-boot","tag-spring-cloud"],"acf":[],"blog_post_summary":"<p>Spring projects in general are opinionated: 80-90% of use cases are handled &#8220;by default&#8221;, and code is often much more concise than would be required otherwise due to Spring&#8217;s preference of convention over configuration. These and other &#8220;opinions&#8221; can result in dramatically less code to write and maintain and as a result, more focused impact. [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/posts\/229292","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/users\/97204"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/comments?post=229292"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/posts\/229292\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/media\/227205"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/media?parent=229292"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/categories?post=229292"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/tags?post=229292"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}