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
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..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
@@ -10,149 +10,119 @@
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;
/**
- * 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.
+ * 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());
/**
- * 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
+ * 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.
*/
- @Override
- public Object createInstance(T metaData) throws Exception {
- LOGGER.info("Starting " + this.getClass().getSimpleName() + ".createInstance()");
- return createInstance(metaData, metaData.getConnectionEnvVar());
- }
+ private static final Map SERVICE_CLIENT_CACHE = new ConcurrentHashMap<>();
/**
- * 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
+ * Gets or creates a cached BlobServiceClient for the given connection.
+ * 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)
+ * @throws Exception if service client creation fails
*/
- private Object createInstance(T metaData, String envVar) throws Exception {
- LOGGER.info("Starting hydration with environment variable: " + envVar);
+ 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);
+ Object existing = SERVICE_CLIENT_CACHE.putIfAbsent(cacheKey, serviceClient);
+ return existing != null ? existing : serviceClient;
+ }
+ // ---- Service client construction (private) ----
+
+ private Object buildServiceClient(String envVar) throws Exception {
final String maybeConnString = System.getenv(envVar);
-
+ final ClassLoader cl = Thread.currentThread().getContextClassLoader();
+
if (maybeConnString != null && isConnectionString(maybeConnString)) {
- LOGGER.info("Detected connection string usage from environment variable: " + envVar);
- return buildWithConnectionString(metaData, maybeConnString);
+ return buildServiceClientWithConnectionString(cl, 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);
+ final Object credential = buildManagedIdentityCredential(cl, clientId);
+ return buildServiceClientWithManagedIdentity(cl, endpoint, credential);
}
}
- /**
- * 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;
+ 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);
+ return builderClass.getMethod("buildClient").invoke(builder);
+ }
- /**
- * 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;
+ 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);
+ return builderClass.getMethod("buildClient").invoke(builder);
+ }
- /**
- * 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) {
+ // ---- Auth utilities ----
+
+ 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 d6ed101..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
@@ -6,60 +6,21 @@
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.
+ * 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 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.");
- 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.");
- 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 db7f32c..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
@@ -6,54 +6,18 @@
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.
+ * 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 {
- 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;
- }
-
- @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;
+ public Object createInstance(BlobContainerMetaData metaData) throws Exception {
+ final Object serviceClient = getOrCreateServiceClient(metaData);
+ return serviceClient.getClass()
+ .getMethod("getBlobContainerClient", String.class)
+ .invoke(serviceClient, metaData.getContainerName());
}
}
+