Calling AWS SDK v2 Directly from CFML, Part 1: Loading Java Without Setting Fire to the Classloader

Load the AWS SDK for Java into ColdFusion and build the shared foundation for direct browser uploads, image moderation, and domain management.

ColdFusion has always been able to call Java classes directly. Amazon publishes a comprehensive software development kit for Java. It should therefore be possible to load the Amazon libraries, create a client, and start making requests.

It is possible. That sentence is doing a lot of emotional labour.

The individual service calls are surprisingly straightforward. Most of the work happens before the first request: collecting compatible dependencies, putting them where ColdFusion can find them, preventing duplicate libraries from fighting over the classloader, and deciding how credentials will be resolved. This is infrastructure work. When it succeeds, nothing interesting happens. When it fails, the application produces an exception long enough to qualify as a novella.

In this four-part series, we’re going to call the Amazon Web Services software development kit for Java version 2 directly from ColdFusion Markup Language.

We’ll use it to:

  • Authorize direct browser uploads to Amazon Simple Storage Service and verify what arrived
  • Moderate uploaded images with Amazon Rekognition
  • Manage domain records with Amazon Route 53

This first article builds the shared foundation. By the end, ColdFusion will be able to load the required Java classes and create clients for all three services. We won’t make a real service request yet. First, we’re going to make sure the weapon is assembled correctly before pointing it at production.

Why Use the Java Software Development Kit?

ColdFusion already supports Amazon Simple Storage Service. We can use s3:// paths with familiar file and directory functions. ColdFusion’s getCloudService() interface also exposes a broader collection of storage operations. If all we need is this:

<cfscript>
    fileWrite(
        "s3://example-bucket/example.txt",
        "Hello from ColdFusion."
    );
</cfscript>

then we should probably use the native function. Installing a collection of Java libraries to avoid calling fileWrite() would be less of an architectural decision and more of a cry for help.

Our reason for using the Amazon software development kit is more specific. In part two, ColdFusion will generate a short-lived, cryptographically signed upload request. The browser will use that request to send a file directly to a private Amazon Simple Storage Service bucket.

The file data won’t pass through ColdFusion.

ColdFusion will authenticate the user, choose the object key, authorize the operation, and sign the request. Amazon will receive the file. ColdFusion will then inspect the resulting object before associating it with application data. That removes large file transfers from our ColdFusion request threads while keeping the application in control of who may upload and where the object is stored.

The Java software development kit also gives us:

  • Direct access to current Amazon service interfaces
  • Precise request and response objects
  • Modern credential providers
  • Service-specific exceptions and request identifiers
  • Application-pinned dependency versions
  • One consistent integration model for Amazon Simple Storage Service, Rekognition, and Route 53

We could call Amazon’s web interfaces ourselves instead. We would need to construct each request, serialize its contents, calculate a cryptographic signature, manage credentials, choose the correct service endpoint, interpret the response, and decide which failures deserve another attempt.

That sounds educational. I have learned enough educational lessons in production.

The Amazon software development kit already knows how to do those things. ColdFusion can create and call its Java objects directly:

<cfscript>
    region = createObject(
        "java",
        "software.amazon.awssdk.regions.Region"
    ).of( "ca-central-1" );

    writeDump( region.id() );
</cfscript>

The software.amazon.awssdk package identifies version 2 of the Amazon Java software development kit. Older examples using packages beginning with com.amazonaws belong to version 1. This distinction matters because both versions still appear in search results, forum posts, and code samples written by somebody who solved our exact problem nine years ago and then vanished into the mist.

Amazon describes version 2 as a substantial rewrite rather than a minor upgrade. It uses builders extensively and supports interchangeable web clients. The Amazon Web Services documentation for Java version 2 is the reference we’ll use throughout this series.

Our Project Structure

We’ll keep the Amazon dependencies in an application-owned directory:

application/
├── Application.cfc
├── models/
│   └── aws_client_factory.cfc
└── aws-sdk/
    ├── pom.xml
    └── lib/

Maven will read pom.xml, resolve the requested modules, and copy their runtime dependencies into aws-sdk/lib. Maven is only a build tool here. It doesn’t need to run inside ColdFusion, and it doesn’t need to be installed on the production server. We use it to construct a repeatable library bundle, then deploy that bundle with the application.

Don’t download individual Java Archive files by hand until the error messages stop. That method eventually produces a directory containing three versions of the same library, two missing dependencies, and a text file named notes.txt that says only “DO NOT TOUCH.”

I know this because I have conducted research. And by "research" I mean "mistakes."

Creating the Maven Dependency File

Create aws-sdk/pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://maven.apache.org/POM/4.0.0
        https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>coldfusion-aws-sdk</artifactId>
    <version>1.0.0</version>

    <properties>
        <aws.sdk.version>2.25.66</aws.sdk.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>software.amazon.awssdk</groupId>
                <artifactId>bom</artifactId>
                <version>${aws.sdk.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>rekognition</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>route53</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>url-connection-client</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
</project>

The bom artifact is Amazon’s bill of materials. It ensures that every Amazon module uses the same version. Without it, we could accidentally combine incompatible versions of the core library, authentication module, and service clients. Java would then explain the problem using an exception written for someone with very different weekend plans. Amazon recommends Maven and a bill of materials for managing version 2 dependencies. The official migration documentation explains the same structure.

The version shown here is the version used while developing and testing this implementation. Before adopting it, check for a newer release, review the changes, and test the complete library bundle.

Don’t replace a tested version in production merely because a larger number has become available. That is dependency management by slot machine.

Once pom.xml exists, run this command from the aws-sdk directory:

mvn dependency:copy-dependencies \
    -DincludeScope=runtime \
    -DoutputDirectory=lib

Maven will copy the service modules and their runtime dependencies into lib. When upgrading, replace the contents of lib rather than merging the new files into the old directory. Leaving two versions of a dependency in the same classpath turns library selection into an undocumented lottery.

At this point, we have a repeatable dependency bundle containing only the Amazon services used by this series.

The Logging-Library Trap

You may have noticed that every dependency excludes this artifact:

<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

This is not decorative paranoia.

The patched Adobe ColdFusion 2025 installations used while developing this series already provide the Simple Logging Facade for Java library. If Maven places another copy inside the application library directory, ColdFusion can load one copy through its parent classloader while the application loads another. The two classes have the same name but different classloader identities. Java considers them different types.

Eventually, the Amazon web client tries to initialize its logger, and the application fails with something resembling:

java.lang.NoClassDefFoundError:
Could not initialize class
software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient

The useful part of the exception may be buried beneath a loader constraint violation involving org.slf4j.LoggerFactory. This is the kind of error that encourages a person to reinstall Java, upgrade ColdFusion, downgrade ColdFusion, blame Docker, and briefly investigate whether farming might offer a more predictable career.

The fix is to let ColdFusion provide the logging interface it already owns. Our application bundle must not contain another slf4j-api Java Archive file.

After Maven finishes, verify the directory:

find lib -name "slf4j-api-*.jar"

The command should return nothing. If it finds a file, stop there. Don’t deploy the bundle and wait to see which classloader wins. Classloaders don’t recognize sportsmanship.

This exclusion addresses Adobe ColdFusion 2025. If you’re using another ColdFusion engine or version, inspect the libraries it already provides before deciding which dependencies belong in the application bundle.

Loading the Libraries Through Application.cfc

ColdFusion allows an application to define its own Java classpath through this.javaSettings. Add this to Application.cfc:

component {
    this.name = "AwsSdkExample";

    this.javaSettings = {
        loadPaths: [ getDirectoryFromPath( getCurrentTemplatePath() ) & "aws-sdk/lib" ],
        loadColdFusionClassPath: true,
        reloadOnChange: false
    };
}

The path is calculated from the physical location of Application.cfc. That makes it independent of the current request path and the directory from which ColdFusion was started. The settings mean:

  • loadPaths identifies the directory containing our Java libraries.
  • loadColdFusionClassPath allows the application classloader to continue using the classes ColdFusion already provides.
  • reloadOnChange prevents ColdFusion from watching the library directory and rebuilding the classloader while the application is running.

Adobe documents these settings in its Application.cfc variable reference. I keep reloadOnChange disabled in production. Replacing Java libraries beneath a running application is the sort of convenience that works wonderfully until it develops initiative.

When the dependency bundle changes, restart the ColdFusion application or engine through your normal deployment process. Don’t load the same directory through a second server-level classpath setting. One owner for the Amazon libraries is enough.

At this point, ColdFusion thinks it knows where the Amazon classes live.

Verifying the Classpath

Before creating service clients, verify that ColdFusion can load the classes we care about. Create a temporary diagnostic page:

<cfscript>
    required_classes = [
        "software.amazon.awssdk.services.s3.S3Client",
        "software.amazon.awssdk.services.s3.presigner.S3Presigner",
        "software.amazon.awssdk.services.rekognition.RekognitionClient",
        "software.amazon.awssdk.services.route53.Route53Client",
        "software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient"
    ];

    results = {};

    for ( class_name in required_classes ) {
        try {
            createObject( "java", class_name );
            results[ class_name ] = "loaded";
        } catch ( any error ) {
            results[ class_name ] = error.message;
        }
    }

    writeDump( results );
</cfscript>

Every entry should report loaded. The Amazon Simple Storage Service presigner is included because we’ll use it in part two to authorize direct browser uploads. This test doesn’t contact Amazon. It proves that:

  • ColdFusion can see the application library directory.
  • The three service modules are present.
  • The Amazon Simple Storage Service presigner is present.
  • The selected web client is present.
  • Java can initialize the classes without immediately losing an argument with the logging system.

Remove or protect this diagnostic page after testing. Public exception dumps can reveal physical paths, class names, and deployment details. Attackers enjoy documentation too.

Creating a Reusable Client Factory

The three service clients use the same basic construction pattern:

  1. Resolve credentials.
  2. Resolve a region.
  3. Create a web client.
  4. Pass those objects to the service client’s builder.
  5. Build the client.

We could repeat that code in every service component. We could also repeat every mistake. Instead, create models/aws_client_factory.cfc:

component output="false" {
    public any function init(
        required string default_region,
        any credentials_provider
    ) {
        variables.default_region = trim( arguments.default_region );

        if ( !len( variables.default_region ) ) {
            throw(
                type = "AwsClientFactory.InvalidConfiguration",
                message = "A default Amazon Web Services region is required."
            );
        }

        variables.client_classes = {
            s3: "software.amazon.awssdk.services.s3.S3Client",
            rekognition: "software.amazon.awssdk.services.rekognition.RekognitionClient",
            route53: "software.amazon.awssdk.services.route53.Route53Client"
        };

        if ( structKeyExists( arguments, "credentials_provider" ) ) {
            variables.credentials_provider = arguments.credentials_provider;
        } else {
            variables.credentials_provider = createObject( "java", "software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider" ).create();
        }

        return this;
    }

    public any function createClient(
        required string service_name,
        string region_name = variables.default_region
    ) {
        var service_key = lCase( trim( arguments.service_name ) );

        var selected_region = trim( arguments.region_name );

        if ( !structKeyExists( variables.client_classes, service_key ) ) {
            throw(
                type = "AwsClientFactory.UnsupportedService",
                message = "The requested Amazon service is not supported."
            );
        }

        if ( !len( selected_region ) ) {
            throw(
                type = "AwsClientFactory.InvalidRegion",
                message = "An Amazon Web Services region is required."
            );
        }

        var region = createObject( "java", "software.amazon.awssdk.regions.Region" ).of( selected_region );

        var http_client = createObject( "java", "software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient" ).builder().build();

        return createObject( "java", variables.client_classes[ service_key ] )
            .builder()
            .region( region )
            .credentialsProvider( variables.credentials_provider )
            .httpClient( http_client )
            .build();
    }
}

The service name is checked against an allowlist. It should come from our code, not an address or form parameter. There is no good reason to let a visitor choose which Java class the server creates. That sounds less like a feature and more like the opening paragraph of an incident report.

The factory accepts an optional credential provider. If we don’t supply one, it uses Amazon’s default credential provider. That provider can resolve credentials from several supported locations, including:

  • Java system properties
  • Environment variables
  • Local credential profiles
  • Web identity tokens
  • Container roles
  • Instance roles

The complete search order is documented in Amazon’s default credential provider chain. For production systems hosted by Amazon, temporary credentials supplied through a workload role are generally preferable to long-lived access keys. There is nothing technically sophisticated about storing a permanent secret in an environment variable. It is still a permanent secret. We have merely hidden it somewhere developers forget to inspect.

If the server must use an instance role specifically, we can create that provider explicitly:

<cfscript>
    credentials_provider = createObject( "java", "software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider" ).create();
    client_factory = new models.aws_client_factory( default_region = "ca-central-1", credentials_provider = credentials_provider );
</cfscript>

Explicit selection prevents an unexpected environment variable or local profile from winning earlier in the default search order. If the application must use access keys, create a static provider from secrets supplied by protected configuration:

<cfscript>
    credentials = createObject( "java", "software.amazon.awssdk.auth.credentials.AwsBasicCredentials" ).create( application.configuration.aws_access_key_id, application.configuration.aws_secret_access_key );
    credentials_provider = createObject( "java", "software.amazon.awssdk.auth.credentials.StaticCredentialsProvider" ).create( credentials );
    client_factory = new models.aws_client_factory( default_region = "ca-central-1", credentials_provider = credentials_provider );
</cfscript>

Don’t put those values directly in the component, Application.cfc, or source control. A secret committed to Git is no longer a secret. It is historical evidence.

Building the Three Clients

We can now verify that all three clients can be constructed:

<cfscript>
    client_factory = new models.aws_client_factory( default_region = "ca-central-1" );
    clients = {};

    try {
        clients.s3 = client_factory.createClient( "s3" );
        clients.rekognition = client_factory.createClient( "rekognition" );
        clients.route53 = client_factory.createClient( service_name = "route53", region_name = "ca-central-1" );

        writeOutput( "Amazon Simple Storage Service client created.<br>" );
        writeOutput( "Amazon Rekognition client created.<br>" );
        writeOutput( "Amazon Route 53 client created.<br>" );
    } finally {
        for ( service_name in clients ) {
            clients[ service_name ].close();
        }
    }
</cfscript>

Amazon Simple Storage Service and Amazon Rekognition use the configured application region. The Route 53 control plane is global, but its software development kit client is conventionally constructed using ca-central-1. The finally block closes every client created by this temporary test.

In the real service components, we won’t create a new client for every request. Amazon service clients are designed to be reused. Each ColdFusion service will create its client once, retain it, and close it when the application shuts down.

Creating a client proves that the builder, region, credential provider, and web implementation can be assembled. It doesn’t prove that the credentials are valid or authorized. Most providers resolve credentials lazily, and no service request has been made. That distinction will save us from writing “Amazon connection successful” when we have connected to absolutely nothing.

What We Have Built

We now have:

  • A repeatable Maven dependency definition
  • One tested version across every Amazon module
  • A library directory owned by the ColdFusion application
  • Protection against the duplicate logging-library problem
  • An application-specific Java classpath
  • A reusable factory for creating service clients
  • Support for the default credential chain or an explicitly selected provider
  • A diagnostic that proves the required clients and presigner can be constructed

More importantly, the service-specific articles no longer need to explain classloaders, Maven, or why LoggerFactory has betrayed us. They can concentrate on the useful code. In part two, we’ll generate short-lived upload permission in ColdFusion, send files directly from the browser to a private Amazon Simple Storage Service bucket, and verify what arrived without routing the file data through our server. No temporary files. No hand-built request signatures. No public bucket.

I am trying to grow as a person.