From c29721165a361b5837734c0fb1e9e7d9b5fc7967 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Wed, 1 Apr 2026 11:20:21 -0500 Subject: [PATCH 1/3] perf: cache BlobServiceClient per connection instead of BlobContainerClient per container Refactors BaseBlobHydrator to cache BlobServiceClient (which holds the HTTP pipeline) keyed by connection env var name, instead of caching individual BlobContainerClient objects keyed by connection+container. This means all containers under the same storage account share a single HTTP pipeline. Before: Each unique container built its own pipeline (~100-200ms each) After: One pipeline per connection, container/blob clients derived for free Cache reduced from LRU-32 (BlobContainerClient) to LRU-8 (BlobServiceClient). --- .../sdktype/blob/BaseBlobHydrator.java | 120 ++++++++++++++++++ .../sdktype/blob/BlobClientHydrator.java | 53 ++------ .../sdktype/blob/BlobContainerHydrator.java | 44 +------ 3 files changed, 135 insertions(+), 82 deletions(-) diff --git a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java index 97b0684..04ffede 100644 --- a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java +++ b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java @@ -10,6 +10,8 @@ import com.microsoft.azure.functions.sdktype.exceptions.SdkHydrationException; import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; /** @@ -20,10 +22,25 @@ * This class implements the Template Method pattern, where the overall algorithm * structure is defined in createInstance(), but specific steps are delegated to * subclass implementations. + * + * Maintains an internal cache of BlobServiceClient objects keyed by the connection + * environment variable name. Since BlobServiceClient holds the HttpPipeline (HTTP + * client, retry policies, auth), caching at this level means all containers and + * blobs under the same storage account share a single pipeline. Deriving + * BlobContainerClient and BlobClient from a BlobServiceClient is free — just URL + * construction, no new HTTP connections. */ public abstract class BaseBlobHydrator implements SdkTypeHydrator { protected static final Logger LOGGER = Logger.getLogger(BaseBlobHydrator.class.getName()); + /** + * Cache of BlobServiceClient objects, keyed by the connection environment + * variable name (e.g. "AzureWebJobsStorage"). Uses ConcurrentHashMap for + * lock-free reads under high throughput. Typically a function app has 1-3 + * storage connections, so unbounded growth is not a concern. + */ + private static final Map SERVICE_CLIENT_CACHE = new ConcurrentHashMap<>(); + /** * Implements the SdkTypeHydrator interface method. Extracts the connection environment variable * from metadata and delegates to the template method. @@ -70,6 +87,109 @@ private Object createInstance(T metaData, String envVar) throws Exception { } } + /** + * Gets or creates a cached BlobServiceClient for the given connection. + * The BlobServiceClient holds the HttpPipeline and is the most expensive object + * to create. All container and blob clients derived from it share the pipeline. + * + * @param metaData the metadata containing connection info + * @return a BlobServiceClient instance (cached per connection env var) + * @throws Exception if service client creation fails + */ + protected Object getOrCreateServiceClient(BlobMetaData metaData) throws Exception { + final String cacheKey = metaData.getConnectionEnvVar(); + Object cached = SERVICE_CLIENT_CACHE.get(cacheKey); + if (cached != null) { + return cached; + } + + LOGGER.info("Service client cache miss for: " + cacheKey + ". Building new BlobServiceClient."); + final Object serviceClient = buildServiceClient(cacheKey); + // putIfAbsent avoids overwriting if another thread built it concurrently + Object existing = SERVICE_CLIENT_CACHE.putIfAbsent(cacheKey, serviceClient); + return existing != null ? existing : serviceClient; + } + + /** + * Gets or creates a BlobContainerClient by deriving it from the cached BlobServiceClient. + * This is a cheap operation — no new HTTP pipeline, just URL construction. + * + * @param metaData the metadata containing connection and container info + * @return a BlobContainerClient instance + * @throws Exception if creation fails + */ + protected Object getOrCreateContainerClient(BlobMetaData metaData) throws Exception { + final Object serviceClient = getOrCreateServiceClient(metaData); + return serviceClient.getClass() + .getMethod("getBlobContainerClient", String.class) + .invoke(serviceClient, metaData.getContainerName()); + } + + /** + * Builds a BlobServiceClient using reflection, handling both connection string + * and managed identity scenarios. + * + * @param envVar the environment variable name for the connection + * @return a new BlobServiceClient instance + * @throws Exception if client creation fails + */ + private Object buildServiceClient(String envVar) throws Exception { + final String maybeConnString = System.getenv(envVar); + final ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + if (maybeConnString != null && isConnectionString(maybeConnString)) { + return buildServiceClientWithConnectionString(cl, maybeConnString); + } else { + final String accountName = System.getenv(envVar + "__accountName"); + final String serviceUri = System.getenv(envVar + "__serviceUri"); + final String blobServiceUri = System.getenv(envVar + "__blobServiceUri"); + final String clientId = System.getenv(envVar + "__clientId"); + + final String endpoint = resolveEndpoint(accountName, serviceUri, blobServiceUri); + final Object credential = buildManagedIdentityCredential(clientId); + return buildServiceClientWithManagedIdentity(cl, endpoint, credential); + } + } + + private Object buildServiceClientWithConnectionString(ClassLoader cl, String connStr) throws Exception { + final Class builderClass = cl.loadClass("com.azure.storage.blob.BlobServiceClientBuilder"); + final Object builder = builderClass.getDeclaredConstructor().newInstance(); + + builderClass.getMethod("connectionString", String.class).invoke(builder, connStr); + + final Object serviceClient = builderClass.getMethod("buildClient").invoke(builder); + LOGGER.info("Built BlobServiceClient using connection string."); + return serviceClient; + } + + private Object buildServiceClientWithManagedIdentity(ClassLoader cl, String endpoint, Object credential) throws Exception { + final Class builderClass = cl.loadClass("com.azure.storage.blob.BlobServiceClientBuilder"); + final Object builder = builderClass.getDeclaredConstructor().newInstance(); + + final Class tokenCredClass = cl.loadClass("com.azure.core.credential.TokenCredential"); + builderClass.getMethod("credential", tokenCredClass).invoke(builder, credential); + builderClass.getMethod("endpoint", String.class).invoke(builder, endpoint); + + final Object serviceClient = builderClass.getMethod("buildClient").invoke(builder); + LOGGER.info("Built BlobServiceClient using managed identity."); + return serviceClient; + } + + /** + * Derives a BlobClient from a cached BlobContainerClient via getBlobClient(blobName). + * This is essentially free — no HTTP pipeline build, just URL construction. + * + * @param containerClient a BlobContainerClient instance + * @param blobName the blob name + * @return a BlobClient pointing at the specific blob + * @throws Exception if reflection fails + */ + protected Object deriveBlobClient(Object containerClient, String blobName) throws Exception { + return containerClient.getClass() + .getMethod("getBlobClient", String.class) + .invoke(containerClient, blobName); + } + /** * Subclasses override to build their specific client using connection string authentication. * diff --git a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobClientHydrator.java b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobClientHydrator.java index d6ed101..4f85006 100644 --- a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobClientHydrator.java +++ b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobClientHydrator.java @@ -6,60 +6,27 @@ package com.microsoft.azure.functions.sdktype.blob; -import java.lang.reflect.Method; - /** - * Reflection logic for building a BlobClient from BlobClientMetaData, - * potentially throwing SdkHydrationException if reflection or environment - * variables are invalid. Supports both connection strings and managed identity. + * Reflection logic for building a BlobClient from BlobClientMetaData. + * Derives BlobClient from the cached BlobServiceClient in the base class, + * sharing the HTTP pipeline across all containers and blobs under the same + * storage account connection. */ public class BlobClientHydrator extends BaseBlobHydrator { @Override protected Object buildWithConnectionString(BlobClientMetaData metaData, String connStr) throws Exception { - final ClassLoader cl = Thread.currentThread().getContextClassLoader(); - final Class builderClass = cl.loadClass("com.azure.storage.blob.BlobClientBuilder"); - final Object builder = builderClass.getDeclaredConstructor().newInstance(); - - final Method connMethod = builderClass.getMethod("connectionString", String.class); - connMethod.invoke(builder, connStr); - - final Method contMethod = builderClass.getMethod("containerName", String.class); - contMethod.invoke(builder, metaData.getContainerName()); - - final Method bNameMethod = builderClass.getMethod("blobName", String.class); - bNameMethod.invoke(builder, metaData.getBlobName()); - - final Method buildM = builderClass.getMethod("buildClient"); - final Object blobClient = buildM.invoke(builder); - LOGGER.info("Successfully built BlobClient using connection string approach."); + final Object containerClient = getOrCreateContainerClient(metaData); + final Object blobClient = deriveBlobClient(containerClient, metaData.getBlobName()); + LOGGER.info("Derived BlobClient from cached service client (connection string)."); return blobClient; } @Override protected Object buildWithManagedIdentity(BlobClientMetaData metaData, String endpoint, Object credential) throws Exception { - LOGGER.info("buildWithManagedIdentity for container: " + metaData.getContainerName() + ", blob: " + metaData.getBlobName() + " endpoint: " + endpoint); - - final ClassLoader cl = Thread.currentThread().getContextClassLoader(); - final Class builderClass = cl.loadClass("com.azure.storage.blob.BlobClientBuilder"); - final Object builder = builderClass.getDeclaredConstructor().newInstance(); - - final Class tokenCredClass = cl.loadClass("com.azure.core.credential.TokenCredential"); - final Method credMethod = builderClass.getMethod("credential", tokenCredClass); - credMethod.invoke(builder, credential); - - final Method endpointMethod = builderClass.getMethod("endpoint", String.class); - endpointMethod.invoke(builder, endpoint); - - final Method contMethod = builderClass.getMethod("containerName", String.class); - contMethod.invoke(builder, metaData.getContainerName()); - - final Method bNameMethod = builderClass.getMethod("blobName", String.class); - bNameMethod.invoke(builder, metaData.getBlobName()); - - final Method buildM = builderClass.getMethod("buildClient"); - final Object blobClient = buildM.invoke(builder); - LOGGER.info("Successfully built BlobClient using managed identity approach."); + final Object containerClient = getOrCreateContainerClient(metaData); + final Object blobClient = deriveBlobClient(containerClient, metaData.getBlobName()); + LOGGER.info("Derived BlobClient from cached service client (managed identity)."); return blobClient; } } diff --git a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobContainerHydrator.java b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobContainerHydrator.java index db7f32c..efdaf32 100644 --- a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobContainerHydrator.java +++ b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobContainerHydrator.java @@ -6,54 +6,20 @@ package com.microsoft.azure.functions.sdktype.blob; -import java.lang.reflect.Method; - /** - * Reflection logic for building a BlobContainerClient from BlobContainerMetaData, - * throwing SdkHydrationException on reflection or environment errors. - * Supports both connection string usage and managed identity usage. + * Reflection logic for building a BlobContainerClient from BlobContainerMetaData. + * Derives BlobContainerClient from the cached BlobServiceClient in the base class, + * sharing the HTTP pipeline across all containers under the same storage account. */ public class BlobContainerHydrator extends BaseBlobHydrator { @Override protected Object buildWithConnectionString(BlobContainerMetaData metaData, String connStr) throws Exception { - final ClassLoader cl = Thread.currentThread().getContextClassLoader(); - final Class builderClass = cl.loadClass("com.azure.storage.blob.BlobContainerClientBuilder"); - final Object builder = builderClass.getDeclaredConstructor().newInstance(); - - final Method connMethod = builderClass.getMethod("connectionString", String.class); - connMethod.invoke(builder, connStr); - - final Method contMethod = builderClass.getMethod("containerName", String.class); - contMethod.invoke(builder, metaData.getContainerName()); - - final Method buildM = builderClass.getMethod("buildClient"); - final Object containerClient = buildM.invoke(builder); - LOGGER.info("Successfully built BlobContainerClient using connection string approach."); - return containerClient; + return getOrCreateContainerClient(metaData); } @Override protected Object buildWithManagedIdentity(BlobContainerMetaData metaData, String endpoint, Object credential) throws Exception { - LOGGER.info("buildWithManagedIdentity for container: " + metaData.getContainerName() + " endpoint: " + endpoint); - - final ClassLoader cl = Thread.currentThread().getContextClassLoader(); - final Class builderClass = cl.loadClass("com.azure.storage.blob.BlobContainerClientBuilder"); - final Object builder = builderClass.getDeclaredConstructor().newInstance(); - - final Class tokenCredClass = cl.loadClass("com.azure.core.credential.TokenCredential"); - final Method credMethod = builderClass.getMethod("credential", tokenCredClass); - credMethod.invoke(builder, credential); - - final Method endpointMethod = builderClass.getMethod("endpoint", String.class); - endpointMethod.invoke(builder, endpoint); - - final Method contMethod = builderClass.getMethod("containerName", String.class); - contMethod.invoke(builder, metaData.getContainerName()); - - final Method buildM = builderClass.getMethod("buildClient"); - final Object containerClient = buildM.invoke(builder); - LOGGER.info("Successfully built BlobContainerClient using managed identity approach."); - return containerClient; + return getOrCreateContainerClient(metaData); } } From b1f52b20d9942f3f16fd820f16a0b1e2baafb7c6 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Wed, 1 Apr 2026 13:41:16 -0500 Subject: [PATCH 2/3] refactor: simplify hydrator hierarchy - remove template method pattern - BaseBlobHydrator: removed abstract buildWithConnectionString/buildWithManagedIdentity, removed createInstance orchestration that duplicated auth detection. Now only owns BlobServiceClient caching (getOrCreateServiceClient) and auth utilities. - BlobClientHydrator: overrides createInstance directly, derives blob client from cached service client via getBlobContainerClient().getBlobClient() - BlobContainerHydrator: overrides createInstance directly, derives container client from cached service client via getBlobContainerClient() - All auth utilities made private (no longer needed by subclasses) - Removed unused Method import from BaseBlobHydrator --- .../sdktype/blob/BaseBlobHydrator.java | 196 ++---------------- .../sdktype/blob/BlobClientHydrator.java | 28 +-- .../sdktype/blob/BlobContainerHydrator.java | 18 +- 3 files changed, 42 insertions(+), 200 deletions(-) diff --git a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java index 04ffede..3f79f4b 100644 --- a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java +++ b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java @@ -15,20 +15,15 @@ import java.util.logging.Logger; /** - * Base class for Blob hydrators that handles common logic for connection string - * vs managed identity authentication. Subclasses override buildWithConnectionString - * and buildWithManagedIdentity to configure their specific builder types. - * - * This class implements the Template Method pattern, where the overall algorithm - * structure is defined in createInstance(), but specific steps are delegated to - * subclass implementations. - * - * Maintains an internal cache of BlobServiceClient objects keyed by the connection - * environment variable name. Since BlobServiceClient holds the HttpPipeline (HTTP - * client, retry policies, auth), caching at this level means all containers and - * blobs under the same storage account share a single pipeline. Deriving - * BlobContainerClient and BlobClient from a BlobServiceClient is free — just URL - * construction, no new HTTP connections. + * Base class for Blob hydrators. Owns BlobServiceClient caching and auth + * detection (connection string vs managed identity). Subclasses override + * {@link #createInstance} to derive their specific client type + * (BlobClient, BlobContainerClient) from the cached service client. + * + *

The BlobServiceClient holds the HTTP pipeline (HttpClient, retry policies, + * auth provider). Caching it per connection means all containers and blobs + * under the same storage account share a single pipeline. Deriving + * BlobContainerClient or BlobClient from it is free — just URL construction.

*/ public abstract class BaseBlobHydrator implements SdkTypeHydrator { protected static final Logger LOGGER = Logger.getLogger(BaseBlobHydrator.class.getName()); @@ -41,56 +36,9 @@ public abstract class BaseBlobHydrator implements SdkTyp */ private static final Map SERVICE_CLIENT_CACHE = new ConcurrentHashMap<>(); - /** - * Implements the SdkTypeHydrator interface method. Extracts the connection environment variable - * from metadata and delegates to the template method. - * - * @param metaData the metadata containing configuration details - * @return the built client instance - * @throws Exception if client creation fails - */ - @Override - public Object createInstance(T metaData) throws Exception { - LOGGER.info("Starting " + this.getClass().getSimpleName() + ".createInstance()"); - return createInstance(metaData, metaData.getConnectionEnvVar()); - } - - /** - * Main orchestration method that determines authentication type and delegates to subclass methods. - * This is the template method that defines the algorithm structure. - * - * @param metaData the metadata containing configuration details - * @param envVar the environment variable name or prefix for authentication - * @return the built client instance - * @throws Exception if client creation fails - */ - private Object createInstance(T metaData, String envVar) throws Exception { - LOGGER.info("Starting hydration with environment variable: " + envVar); - - final String maybeConnString = System.getenv(envVar); - - if (maybeConnString != null && isConnectionString(maybeConnString)) { - LOGGER.info("Detected connection string usage from environment variable: " + envVar); - return buildWithConnectionString(metaData, maybeConnString); - } else { - LOGGER.info("Detected Managed Identity usage. Prefix: " + envVar); - - final String accountName = System.getenv(envVar + "__accountName"); - final String serviceUri = System.getenv(envVar + "__serviceUri"); - final String blobServiceUri = System.getenv(envVar + "__blobServiceUri"); - final String clientId = System.getenv(envVar + "__clientId"); - - final String endpoint = resolveEndpoint(accountName, serviceUri, blobServiceUri); - final Object credential = buildManagedIdentityCredential(clientId); - - return buildWithManagedIdentity(metaData, endpoint, credential); - } - } - /** * Gets or creates a cached BlobServiceClient for the given connection. - * The BlobServiceClient holds the HttpPipeline and is the most expensive object - * to create. All container and blob clients derived from it share the pipeline. + * Subclasses call this and derive container/blob clients from the result. * * @param metaData the metadata containing connection info * @return a BlobServiceClient instance (cached per connection env var) @@ -105,34 +53,12 @@ protected Object getOrCreateServiceClient(BlobMetaData metaData) throws Exceptio LOGGER.info("Service client cache miss for: " + cacheKey + ". Building new BlobServiceClient."); final Object serviceClient = buildServiceClient(cacheKey); - // putIfAbsent avoids overwriting if another thread built it concurrently Object existing = SERVICE_CLIENT_CACHE.putIfAbsent(cacheKey, serviceClient); return existing != null ? existing : serviceClient; } - /** - * Gets or creates a BlobContainerClient by deriving it from the cached BlobServiceClient. - * This is a cheap operation — no new HTTP pipeline, just URL construction. - * - * @param metaData the metadata containing connection and container info - * @return a BlobContainerClient instance - * @throws Exception if creation fails - */ - protected Object getOrCreateContainerClient(BlobMetaData metaData) throws Exception { - final Object serviceClient = getOrCreateServiceClient(metaData); - return serviceClient.getClass() - .getMethod("getBlobContainerClient", String.class) - .invoke(serviceClient, metaData.getContainerName()); - } + // ---- Service client construction (private) ---- - /** - * Builds a BlobServiceClient using reflection, handling both connection string - * and managed identity scenarios. - * - * @param envVar the environment variable name for the connection - * @return a new BlobServiceClient instance - * @throws Exception if client creation fails - */ private Object buildServiceClient(String envVar) throws Exception { final String maybeConnString = System.getenv(envVar); final ClassLoader cl = Thread.currentThread().getContextClassLoader(); @@ -146,7 +72,7 @@ private Object buildServiceClient(String envVar) throws Exception { final String clientId = System.getenv(envVar + "__clientId"); final String endpoint = resolveEndpoint(accountName, serviceUri, blobServiceUri); - final Object credential = buildManagedIdentityCredential(clientId); + final Object credential = buildManagedIdentityCredential(cl, clientId); return buildServiceClientWithManagedIdentity(cl, endpoint, credential); } } @@ -154,125 +80,49 @@ private Object buildServiceClient(String envVar) throws Exception { private Object buildServiceClientWithConnectionString(ClassLoader cl, String connStr) throws Exception { final Class builderClass = cl.loadClass("com.azure.storage.blob.BlobServiceClientBuilder"); final Object builder = builderClass.getDeclaredConstructor().newInstance(); - builderClass.getMethod("connectionString", String.class).invoke(builder, connStr); - - final Object serviceClient = builderClass.getMethod("buildClient").invoke(builder); - LOGGER.info("Built BlobServiceClient using connection string."); - return serviceClient; + return builderClass.getMethod("buildClient").invoke(builder); } private Object buildServiceClientWithManagedIdentity(ClassLoader cl, String endpoint, Object credential) throws Exception { final Class builderClass = cl.loadClass("com.azure.storage.blob.BlobServiceClientBuilder"); final Object builder = builderClass.getDeclaredConstructor().newInstance(); - final Class tokenCredClass = cl.loadClass("com.azure.core.credential.TokenCredential"); builderClass.getMethod("credential", tokenCredClass).invoke(builder, credential); builderClass.getMethod("endpoint", String.class).invoke(builder, endpoint); - - final Object serviceClient = builderClass.getMethod("buildClient").invoke(builder); - LOGGER.info("Built BlobServiceClient using managed identity."); - return serviceClient; + return builderClass.getMethod("buildClient").invoke(builder); } - /** - * Derives a BlobClient from a cached BlobContainerClient via getBlobClient(blobName). - * This is essentially free — no HTTP pipeline build, just URL construction. - * - * @param containerClient a BlobContainerClient instance - * @param blobName the blob name - * @return a BlobClient pointing at the specific blob - * @throws Exception if reflection fails - */ - protected Object deriveBlobClient(Object containerClient, String blobName) throws Exception { - return containerClient.getClass() - .getMethod("getBlobClient", String.class) - .invoke(containerClient, blobName); - } + // ---- Auth utilities ---- - /** - * Subclasses override to build their specific client using connection string authentication. - * - * @param metaData the metadata containing configuration details - * @param connectionString the connection string from environment variable - * @return the built client instance - * @throws Exception if client creation fails - */ - protected abstract Object buildWithConnectionString(T metaData, String connectionString) throws Exception; - - /** - * Subclasses override to build their specific client using managed identity authentication. - * - * @param metaData the metadata containing configuration details - * @param endpoint the resolved endpoint URL - * @param credential the DefaultAzureCredential instance - * @return the built client instance - * @throws Exception if client creation fails - */ - protected abstract Object buildWithManagedIdentity(T metaData, String endpoint, Object credential) throws Exception; - - /** - * Decide if configValue is likely a connection string by checking for well-known keywords. - * - * @param val the value to check - * @return true if the value appears to be a connection string - */ - protected boolean isConnectionString(String val) { + private boolean isConnectionString(String val) { return val.contains("AccountKey=") || val.contains("DefaultEndpointsProtocol=") || val.contains("UseDevelopmentStorage=true"); } - /** - * Resolves the endpoint for managed identity from environment variables, or throws if none found. - * Checks accountName, blobServiceUri, and serviceUri in order. - * - * @param accountName the storage account name - * @param serviceUri the generic service URI - * @param blobServiceUri the blob-specific service URI - * @return the resolved endpoint URL - * @throws SdkHydrationException if no endpoint can be resolved - */ - protected String resolveEndpoint(String accountName, String serviceUri, String blobServiceUri) { + private String resolveEndpoint(String accountName, String serviceUri, String blobServiceUri) { if (accountName != null && !accountName.isEmpty()) { - final String ep = String.format("https://%s.blob.core.windows.net", accountName); - LOGGER.info("Resolved endpoint from accountName: " + ep); - return ep; + return String.format("https://%s.blob.core.windows.net", accountName); } if (blobServiceUri != null && !blobServiceUri.isEmpty()) { - LOGGER.info("Resolved endpoint from blobServiceUri: " + blobServiceUri); return blobServiceUri; } if (serviceUri != null && !serviceUri.isEmpty()) { - LOGGER.info("Resolved endpoint from serviceUri: " + serviceUri); return serviceUri; } - throw new SdkHydrationException("Missing accountName, blobServiceUri, or serviceUri for managed identity scenario."); + throw new SdkHydrationException( + "Missing accountName, blobServiceUri, or serviceUri for managed identity scenario."); } - /** - * Build the DefaultAzureCredential reflectively, including user-assigned clientId if present. - * - * @param clientId optional client ID for user-assigned managed identity - * @return the DefaultAzureCredential instance - * @throws Exception if credential creation fails - */ - protected Object buildManagedIdentityCredential(String clientId) throws Exception { - LOGGER.info("Building DefaultAzureCredential for managed identity."); - - final ClassLoader cl = Thread.currentThread().getContextClassLoader(); + private Object buildManagedIdentityCredential(ClassLoader cl, String clientId) throws Exception { final Class builderClass = cl.loadClass("com.azure.identity.DefaultAzureCredentialBuilder"); final Object builder = builderClass.getDeclaredConstructor().newInstance(); if (clientId != null && !clientId.isEmpty()) { - LOGGER.info("Using user-assigned managed identity: " + clientId); - final Method micidMethod = builderClass.getMethod("managedIdentityClientId", String.class); - micidMethod.invoke(builder, clientId); - } else { - LOGGER.info("Using system-assigned managed identity (no clientId)."); + builderClass.getMethod("managedIdentityClientId", String.class).invoke(builder, clientId); } - final Method buildMethod = builderClass.getMethod("build"); - return buildMethod.invoke(builder); + return builderClass.getMethod("build").invoke(builder); } } diff --git a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobClientHydrator.java b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobClientHydrator.java index 4f85006..f744b5d 100644 --- a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobClientHydrator.java +++ b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobClientHydrator.java @@ -7,26 +7,20 @@ package com.microsoft.azure.functions.sdktype.blob; /** - * Reflection logic for building a BlobClient from BlobClientMetaData. - * Derives BlobClient from the cached BlobServiceClient in the base class, - * sharing the HTTP pipeline across all containers and blobs under the same - * storage account connection. + * Builds a BlobClient by deriving it from the cached BlobServiceClient. + * Both the intermediate BlobContainerClient and the final BlobClient are + * free to create — just URL construction, no new HTTP pipeline. */ public class BlobClientHydrator extends BaseBlobHydrator { @Override - protected Object buildWithConnectionString(BlobClientMetaData metaData, String connStr) throws Exception { - final Object containerClient = getOrCreateContainerClient(metaData); - final Object blobClient = deriveBlobClient(containerClient, metaData.getBlobName()); - LOGGER.info("Derived BlobClient from cached service client (connection string)."); - return blobClient; - } - - @Override - protected Object buildWithManagedIdentity(BlobClientMetaData metaData, String endpoint, Object credential) throws Exception { - final Object containerClient = getOrCreateContainerClient(metaData); - final Object blobClient = deriveBlobClient(containerClient, metaData.getBlobName()); - LOGGER.info("Derived BlobClient from cached service client (managed identity)."); - return blobClient; + public Object createInstance(BlobClientMetaData metaData) throws Exception { + final Object serviceClient = getOrCreateServiceClient(metaData); + final Object containerClient = serviceClient.getClass() + .getMethod("getBlobContainerClient", String.class) + .invoke(serviceClient, metaData.getContainerName()); + return containerClient.getClass() + .getMethod("getBlobClient", String.class) + .invoke(containerClient, metaData.getBlobName()); } } diff --git a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobContainerHydrator.java b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobContainerHydrator.java index efdaf32..a79ce69 100644 --- a/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobContainerHydrator.java +++ b/azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BlobContainerHydrator.java @@ -7,19 +7,17 @@ package com.microsoft.azure.functions.sdktype.blob; /** - * Reflection logic for building a BlobContainerClient from BlobContainerMetaData. - * Derives BlobContainerClient from the cached BlobServiceClient in the base class, - * sharing the HTTP pipeline across all containers under the same storage account. + * Builds a BlobContainerClient by deriving it from the cached BlobServiceClient. + * Free to create — just URL construction, no new HTTP pipeline. */ public class BlobContainerHydrator extends BaseBlobHydrator { @Override - protected Object buildWithConnectionString(BlobContainerMetaData metaData, String connStr) throws Exception { - return getOrCreateContainerClient(metaData); - } - - @Override - protected Object buildWithManagedIdentity(BlobContainerMetaData metaData, String endpoint, Object credential) throws Exception { - return getOrCreateContainerClient(metaData); + public Object createInstance(BlobContainerMetaData metaData) throws Exception { + final Object serviceClient = getOrCreateServiceClient(metaData); + return serviceClient.getClass() + .getMethod("getBlobContainerClient", String.class) + .invoke(serviceClient, metaData.getContainerName()); } } + From fd2115d8f90ca2e20af7bc201eb42b8672bf9fe1 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Wed, 1 Apr 2026 15:00:01 -0500 Subject: [PATCH 3/3] chore: bump azure-functions-java-sdktypes version to 1.0.3 --- azure-functions-java-sdktypes/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-functions-java-sdktypes/pom.xml b/azure-functions-java-sdktypes/pom.xml index 9814906..5a5d807 100644 --- a/azure-functions-java-sdktypes/pom.xml +++ b/azure-functions-java-sdktypes/pom.xml @@ -14,7 +14,7 @@ com.microsoft.azure.functions azure-functions-java-sdktypes - 1.0.2 + 1.0.3 jar Microsoft Azure Functions Java SDK Types