Appeler directement l'SDK AWS v2 depuis CFML, Partie 1 : Charger Java sans mettre le feu au chargeur de classes

Chargez l'SDK AWS pour Java dans ColdFusion et construisez la base commune pour les téléchargements directs depuis le navigateur, la modération d'images et la gestion de domaines.

ColdFusion a toujours pu appeler directement des classes Java. Amazon publie un kit de développement logiciel complet pour Java. Il devrait donc être possible de charger les bibliothèques Amazon, de créer un client et de commencer à envoyer des requêtes.

C'est possible. Cette phrase fait beaucoup de travail émotionnel.

Les appels individuels aux services sont étonnamment simples. La majeure partie du travail se fait avant la première requête - recueillir des dépendances compatibles, les placer là où ColdFusion peut les trouver, empêcher des bibliothèques dupliquées de se battre pour le classloader et décider comment les identifiants seront résolus. C'est du travail d'infrastructure. Quand ça marche, rien d'intéressant ne se passe. Quand ça échoue, l'application produit une exception suffisamment longue pour qualifier de novella.

Dans cette série en quatre parties, nous allons appeler directement, depuis ColdFusion Markup Language, le kit de développement logiciel Amazon Web Services pour Java version 2.

Nous allons l'utiliser pour :

  • Autoriser des téléversements directs depuis le navigateur vers Amazon Simple Storage Service et vérifier ce qui est arrivé
  • Modérer des images téléversées avec Amazon Rekognition
  • Gérer des enregistrements de domaine avec Amazon Route 53

Ce premier article bâtit le fondement commun. À la fin, ColdFusion pourra charger les classes Java requises et créer des clients pour les trois services. Nous n'effectuerons pas encore de véritable requête au service. D'abord, nous allons nous assurer que l'arme est correctement assemblée avant de la pointer vers la production.

Pourquoi utiliser le kit de développement logiciel Java?

ColdFusion prend déjà en charge Amazon Simple Storage Service. Nous pouvons utiliser des chemins s3:// avec les fonctions familières de fichier et de répertoire. L'interface getCloudService() de ColdFusion expose aussi une collection plus large d'opérations de stockage. Si tout ce dont nous avons besoin est ceci :

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

alors nous devrions probablement utiliser la fonction native. Installer une collection de bibliothèques Java pour éviter d'appeler fileWrite() relèverait moins d'une décision architecturale que d'un appel à l'aide.

Notre raison d'utiliser le kit de développement logiciel d'Amazon est plus précise. Dans la deuxième partie, ColdFusion générera une requête de téléversement de courte durée, signée cryptographiquement. Le navigateur utilisera cette requête pour envoyer un fichier directement vers un bucket privé Amazon Simple Storage Service.

Les données du fichier ne passeront pas par ColdFusion.

ColdFusion authentifiera l'utilisateur, choisira la clé d'objet, autorisera l'opération et signera la requête. Amazon recevra le fichier. ColdFusion examinera ensuite l'objet résultant avant de l'associer aux données de l'application. Cela retire les gros transferts de fichiers de nos threads de requêtes ColdFusion tout en gardant l'application maîtresse de qui peut téléverser et de l'endroit où l'objet est stocké.

Le kit de développement logiciel Java nous donne aussi :

  • Un accès direct aux interfaces de services Amazon actuelles
  • Des objets de requête et de réponse précis
  • Des fournisseurs d'identifiants modernes
  • Des exceptions spécifiques au service et des identifiants de requête
  • Des versions de dépendances épinglées par l'application
  • Un modèle d'intégration cohérent pour Amazon Simple Storage Service, Rekognition et Route 53

Nous pourrions aussi appeler nous-mêmes les interfaces web d'Amazon. Nous devrions construire chaque requête, sérialiser son contenu, calculer une signature cryptographique, gérer les identifiants, choisir le bon point de terminaison de service, interpréter la réponse et décider quelles défaillances méritent une autre tentative.

Ça a l'air instructif. J'ai déjà appris suffisamment de leçons instructives en production.

Le kit de développement logiciel Amazon sait déjà comment faire tout cela. ColdFusion peut créer ses objets Java et les appeler directement :

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

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

Le package software.amazon.awssdk identifie la version 2 du kit de développement logiciel Java d'Amazon. Les anciens exemples utilisant des packages commençant par com.amazonaws appartiennent à la version 1. Cette distinction compte parce que les deux versions apparaissent encore dans les résultats de recherche, les publications de forums et les exemples de code écrits par quelqu'un qui a résolu notre problème exact il y a neuf ans, puis a disparu dans la brume.

Amazon décrit la version 2 comme une réécriture majeure plutôt qu'une simple mise à niveau mineure. Elle utilise abondamment des builders et prend en charge des clients web interchangeables. La documentation Amazon Web Services pour Java version 2 est la référence que nous utiliserons tout au long de cette série.

La structure de notre projet

Nous conserverons les dépendances Amazon dans un répertoire appartenant à l'application :

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

Maven lira pom.xml, résoudra les modules demandés et copiera leurs dépendances d'exécution dans aws-sdk/lib. Maven n'est ici qu'un outil de génération. Il n'a pas besoin de s'exécuter dans ColdFusion, et il n'a pas besoin d'être installé sur le serveur de production. Nous l'utilisons pour construire un ensemble de bibliothèques reproductible, puis déployons cet ensemble avec l'application.

N'importe quel ensemble de fichiers Java Archive téléchargés à la main jusqu'à ce que les messages d'erreur cessent ne vaut mieux pas. Cette méthode finit par produire un répertoire contenant trois versions de la même bibliothèque, deux dépendances manquantes et un fichier texte nommé notes.txt qui dit seulement « DO NOT TOUCH. »

Je sais cela parce que j'ai mené des recherches. Et par "recherches", j'entends "erreurs".

Création du fichier de dépendances Maven

Créez 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>

L’artefact bom est la bill of materials d’Amazon. Il garantit que chaque module Amazon utilise la même version. Sans cela, nous pourrions combiner par erreur des versions incompatibles de la bibliothèque centrale, du module d’authentification et des clients de service. Java expliquerait alors le problème au moyen d’une exception rédigée pour quelqu’un ayant des plans de fin de semaine très différents. Amazon recommande Maven et une bill of materials pour gérer les dépendances de la version 2. La documentation officielle de migration explique la même structure.

La version indiquée ici est celle utilisée pendant le développement et les tests de cette implémentation. Avant de l’adopter, vérifiez s’il existe une version plus récente, examinez les changements et testez l’ensemble complet de la bibliothèque.

Ne remplacez pas en production une version testée simplement parce qu’un nombre plus élevé est devenu disponible. C’est de la gestion des dépendances à la machine à sous.

Une fois pom.xml créé, exécutez cette commande à partir du répertoire aws-sdk :

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

Maven copiera les modules de service et leurs dépendances d’exécution dans lib. Lors d’une mise à niveau, remplacez le contenu de lib plutôt que de fusionner les nouveaux fichiers dans l’ancien répertoire. Laisser deux versions d’une dépendance dans le même classpath transforme la sélection des bibliothèques en loterie non documentée.

À ce stade, nous avons un ensemble de dépendances reproductible contenant uniquement les services Amazon utilisés par cette série.

Le piège de la bibliothèque de journalisation

Vous avez peut-être remarqué que chaque dépendance exclut cet artefact :

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

Ce n’est pas de la paranoïa décorative.

Les installations d’Adobe ColdFusion 2025 corrigées utilisées pendant le développement de cette série fournissent déjà la bibliothèque Simple Logging Facade for Java. Si Maven place une autre copie dans le répertoire des bibliothèques de l’application, ColdFusion peut charger une copie par son parent classloader pendant que l’application en charge une autre. Les deux classes portent le même nom, mais ont des identités de classloader différentes. Java les considère comme des types différents.

Éventuellement, le client Web Amazon tente d’initialiser son logger, et l’application échoue avec quelque chose qui ressemble à :

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

La partie utile de l’exception peut être enfouie sous une violation de contrainte de chargeur impliquant org.slf4j.LoggerFactory. C’est le genre d’erreur qui donne envie de réinstaller Java, de mettre à niveau ColdFusion, de rétrograder ColdFusion, de blâmer Docker, et d’examiner brièvement si l’agriculture pourrait offrir une carrière plus prévisible.

La solution consiste à laisser ColdFusion fournir l'interface de journalisation qu'il possède déjà. Notre ensemble d'application ne doit pas contenir un autre fichier Java Archive slf4j-api.

Une fois Maven terminé, vérifiez le répertoire :

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

La commande ne devrait rien retourner. Si elle trouve un fichier, arrêtez-vous là. Ne déployez pas l'ensemble et n'attendez pas de voir quel chargeur de classes l'emporte. Les chargeurs de classes ne reconnaissent pas l'esprit sportif.

Cette exclusion concerne Adobe ColdFusion 2025. Si vous utilisez un autre moteur ou une autre version de ColdFusion, examinez les bibliothèques qu'il fournit déjà avant de décider quelles dépendances doivent faire partie de l'ensemble d'application.

Chargement des bibliothèques via Application.cfc

ColdFusion permet à une application de définir son propre chemin de classe Java par l'entremise de this.javaSettings. Ajoutez ceci à Application.cfc :

component {
    this.name = "AwsSdkExample";

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

Le chemin est calculé à partir de l'emplacement physique de Application.cfc. Cela le rend indépendant du chemin de requête actuel et du répertoire à partir duquel ColdFusion a été démarré. Les paramètres signifient :

  • loadPaths identifie le répertoire contenant nos bibliothèques Java.
  • loadColdFusionClassPath permet au chargeur de classes de l'application de continuer à utiliser les classes déjà fournies par ColdFusion.
  • reloadOnChange empêche ColdFusion de surveiller le répertoire des bibliothèques et de reconstruire le chargeur de classes pendant l'exécution de l'application.

Adobe documente ces paramètres dans la référence des variables de son Application.cfc. Je garde reloadOnChange désactivé en production. Remplacer des bibliothèques Java sous une application en cours d'exécution est le genre de commodité qui fonctionne à merveille jusqu'à ce qu'elle prenne des initiatives.

Lorsque l'ensemble de dépendances change, redémarrez l'application ou le moteur ColdFusion par votre processus de déploiement habituel. Ne chargez pas le même répertoire par l'entremise d'un deuxième paramètre de chemin de classe au niveau du serveur. Un seul propriétaire pour les bibliothèques Amazon suffit.

À ce stade, ColdFusion pense savoir où se trouvent les classes Amazon.

Vérification du chemin de classe

Avant de créer des clients de service, vérifiez que ColdFusion peut charger les classes qui nous intéressent. Créez une page de diagnostic temporaire :

<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>

Chaque entrée devrait indiquer loaded. Le presignateur Amazon Simple Storage Service est inclus parce que nous l'utiliserons dans la partie deux pour autoriser les téléversements directs depuis le navigateur. Ce test ne communique pas avec Amazon. Il prouve que :

  • ColdFusion peut voir le répertoire de bibliothèques de l'application.
  • Les trois modules de service sont présents.
  • Le presignateur Amazon Simple Storage Service est présent.
  • Le client web sélectionné est présent.
  • Java peut initialiser les classes sans perdre immédiatement patience avec le système de journalisation.

Supprimez ou protégez cette page de diagnostic après les tests. Les vidages d'exceptions publics peuvent révéler des chemins physiques, des noms de classes et des détails de déploiement. Les attaquants aiment aussi la documentation.

Création d'une fabrique de clients réutilisable

Les trois clients de service utilisent le même modèle de construction de base :

  1. Résoudre les identifiants.
  2. Résoudre une région.
  3. Créer un client web.
  4. Passer ces objets au générateur du client de service.
  5. Construire le client.

Nous pourrions répéter ce code dans chaque composant de service. Nous pourrions aussi répéter chaque erreur. À la place, créez 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 = "Une région Amazon Web Services par défaut est requise."
            );
        }

        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 = "Le service Amazon demandé n'est pas pris en charge."
            );
        }

        if ( !len( selected_region ) ) {
            throw(
                type = "AwsClientFactory.InvalidRegion",
                message = "Une région Amazon Web Services est requise."
            );
        }

        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();
    }
}

Le nom du service est vérifié par rapport à une liste d'autorisation. Il doit provenir de notre code, et non d'une adresse ou d'un paramètre de formulaire. Il n'y a aucune bonne raison de permettre à un visiteur de choisir quelle classe Java le serveur crée. Cela ressemble moins à une fonctionnalité qu'au premier paragraphe d'un rapport d'incident.

La fabrique accepte un fournisseur d'identifiants facultatif. Si nous n'en fournissons pas, elle utilise le fournisseur d'identifiants par défaut d'Amazon. Ce fournisseur peut résoudre les identifiants à partir de plusieurs emplacements pris en charge, notamment :

  • Propriétés système Java
  • Variables d'environnement
  • Profils d'identifiants locaux
  • Jetons d'identité Web
  • Rôles de conteneur
  • Rôles d'instance

L'ordre de recherche complet est documenté dans la chaîne par défaut du fournisseur d'identifiants d'Amazon. Pour les systèmes de production hébergés par Amazon, il est généralement préférable d'utiliser des identifiants temporaires fournis par un rôle de charge de travail plutôt que des clés d'accès à longue durée de vie. Il n'y a rien de techniquement sophistiqué à stocker un secret permanent dans une variable d'environnement. Cela reste un secret permanent. Nous l'avons simplement caché à un endroit que les développeurs oublient de vérifier.

Si le serveur doit utiliser un rôle d'instance en particulier, nous pouvons créer ce fournisseur explicitement :

<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>

La sélection explicite empêche une variable d'environnement inattendue ou un profil local de l'emporter plus tôt dans l'ordre de recherche par défaut. Si l'application doit utiliser des clés d'accès, créez un fournisseur statique à partir de secrets fournis par une configuration protégée :

<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>

Ne placez pas ces valeurs directement dans le composant, Application.cfc, ou le contrôle de source. Un secret commis dans Git n'est plus un secret. C'est une preuve historique.

Construction des trois clients

Nous pouvons maintenant vérifier que les trois clients peuvent être construits :

<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 et Amazon Rekognition utilisent la région d'application configurée. Le plan de contrôle de Route 53 est global, mais son client du kit de développement logiciel est généralement créé à l'aide de ca-central-1. Le bloc finally ferme chaque client créé par ce test temporaire.

Dans les vrais composants de service, nous ne créerons pas un nouveau client pour chaque requête. Les clients des services Amazon sont conçus pour être réutilisés. Chaque service ColdFusion créera son client une seule fois, le conservera, puis le fermera lorsque l'application s'arrêtera.

La création d'un client prouve que le constructeur, la région, le fournisseur d'identifiants et l'implémentation Web peuvent être assemblés. Cela ne prouve pas que les identifiants sont valides ou autorisés. La plupart des fournisseurs résolvent les identifiants à la demande, et aucune requête de service n'a encore été effectuée. Cette distinction nous évitera d'écrire « Connexion Amazon réussie » alors que nous n'avons absolument rien connecté.

Ce que nous avons construit

Nous avons maintenant :

  • Une définition de dépendance Maven reproductible
  • Une seule version testée pour chaque module Amazon
  • Un répertoire de bibliothèques détenu par l'application ColdFusion
  • Une protection contre le problème des bibliothèques de journalisation en double
  • Un classpath Java propre à l'application
  • Une fabrique réutilisable pour créer des clients de service
  • La prise en charge de la chaîne d'identifiants par défaut ou d'un fournisseur sélectionné explicitement
  • Un diagnostic qui prouve que les clients requis et le presigner peuvent être construits

Plus important encore, les articles propres à chaque service n'ont plus besoin d'expliquer les class loaders, Maven, ni pourquoi LoggerFactory nous a trahis. Ils peuvent se concentrer sur le code utile. Dans la deuxième partie, nous générerons une autorisation d'envoi à durée limitée dans ColdFusion, enverrons des fichiers directement du navigateur vers un bucket privé Amazon Simple Storage Service, et vérifierons ce qui est arrivé sans faire transiter les données du fichier par notre serveur. Pas de fichiers temporaires. Pas de signatures de requête construites à la main. Pas de bucket public.

Je m'efforce de grandir en tant que personne.