Definir el controlador de las funciones de Lambda en Java - AWS Lambda

Definir el controlador de las funciones de Lambda en Java

El controlador de la función de Lambda es el método del código de la función que procesa eventos. Cuando se invoca una función, Lambda ejecuta el método del controlador. La función se ejecuta hasta que el controlador devuelve una respuesta, se cierra o se agota el tiempo de espera.

En esta página, se describe cómo trabajar con los controladores de funciones de Lambda en Java, incluidas opciones para la configuración del proyecto, las convenciones de nomenclatura y las prácticas recomendadas. Esta página también incluye un ejemplo de una función de Lambda de Java que recibe información sobre un pedido, genera un recibo en un archivo de texto y coloca este archivo en un bucket de Amazon Simple Storage Service (S3). Para obtener información sobre cómo implementar la función después de escribirla, consulte Implementar funciones de Lambda Java con archivos de archivo .zip o JAR o Implementar funciones Java Lambda con imágenes de contenedor.

Configuración del proyecto de controlador de Java

Cuando se trabaja con funciones de Lambda en Java, el proceso implica escribir el código, compilarlo e implementar los artefactos compilados en Lambda. Puede inicializar un proyecto de Lambda en Java de varias maneras. Por ejemplo, puede usar herramientas como el Arquetipo de Maven para funciones de Lambda, el comando sam init de AWS SAM CLI o incluso una configuración de proyecto Java estándar en su IDE preferido, como IntelliJ IDEA o Visual Studio Code. Si lo prefiere, puede crear la estructura de archivos necesaria de forma manual.

Un proyecto típico de la función de Lambda en Java sigue esta estructura general:

/project-root └ src └ main └ java └ example └ OrderHandler.java (contains main handler) └ <other_supporting_classes> └ build.gradle OR pom.xml

Puede usar Maven o Gradle para crear su proyecto y administrar las dependencias.

La lógica del controlador principal de su función reside en un archivo Java ubicado en el directorio src/main/java/example. En el ejemplo de esta página, el nombre de este archivo es OrderHandler.java. Además de este archivo, puede incluir clases de Java adicionales según sea necesario. Al implementar la función en Lambda, asegúrese de especificar la clase de Java que contiene el método de controlador principal que Lambda debe invocar durante una invocación.

Ejemplo de código de función de Lambda de Java

El siguiente ejemplo de código de función de Lambda de Java 21 recibe información sobre un pedido, genera un recibo en un archivo de texto y coloca este archivo en un bucket de Amazon S3.

ejemplo Función de Lambda OrderHandler.java
package example; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.S3Exception; import java.nio.charset.StandardCharsets; /** * Lambda handler for processing orders and storing receipts in S3. */ public class OrderHandler implements RequestHandler<OrderHandler.Order, String> { private static final S3Client S3_CLIENT = S3Client.builder().build(); /** * Record to model the input event. */ public record Order(String orderId, double amount, String item) {} @Override public String handleRequest(Order event, Context context) { try { // Access environment variables String bucketName = System.getenv("RECEIPT_BUCKET"); if (bucketName == null || bucketName.isEmpty()) { throw new IllegalArgumentException("RECEIPT_BUCKET environment variable is not set"); } // Create the receipt content and key destination String receiptContent = String.format("OrderID: %s\nAmount: $%.2f\nItem: %s", event.orderId(), event.amount(), event.item()); String key = "receipts/" + event.orderId() + ".txt"; // Upload the receipt to S3 uploadReceiptToS3(bucketName, key, receiptContent); context.getLogger().log("Successfully processed order " + event.orderId() + " and stored receipt in S3 bucket " + bucketName); return "Success"; } catch (Exception e) { context.getLogger().log("Failed to process order: " + e.getMessage()); throw new RuntimeException(e); } } private void uploadReceiptToS3(String bucketName, String key, String receiptContent) { try { PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(bucketName) .key(key) .build(); // Convert the receipt content to bytes and upload to S3 S3_CLIENT.putObject(putObjectRequest, RequestBody.fromBytes(receiptContent.getBytes(StandardCharsets.UTF_8))); } catch (S3Exception e) { throw new RuntimeException("Failed to upload receipt to S3: " + e.awsErrorDetails().errorMessage(), e); } } }

Este archivo OrderHandler.java contiene las siguientes secciones de código:

  • package example: en Java, puede ser cualquier cosa, pero debe coincidir con la estructura de directorio del proyecto. Aquí, usamos package example porque la estructura del directorio es src/main/java/example.

  • Instrucciones import: utilícelas para importar las clases de Java que requiere la función de Lambda.

  • public class OrderHandler ...: define la clase de Java y debe ser una definición de clase válida.

  • private static final S3Client S3_CLIENT ...: inicializa un cliente S3 fuera de cualquiera de los métodos de la clase. Esto hace que Lambda ejecute este código durante la fase de inicialización.

  • public record Order ...: defina la forma del evento de entrada esperado en este registro personalizado de Java.

  • public String handleRequest(Order event, Context context): este es el método del controlador principal, que contiene la lógica principal de la aplicación.

  • private void uploadReceiptToS3(...) {}: este es un método auxiliar al que hace referencia el método del controlador principal handleRequest.

El siguiente archivo build.gradle o pom.xml acompaña esta función.

build.gradle
plugins { id 'java' } repositories { mavenCentral() } dependencies { implementation 'com.amazonaws:aws-lambda-java-core:1.2.3' implementation 'software.amazon.awssdk:s3:2.28.29' implementation 'org.slf4j:slf4j-nop:2.0.16' } task buildZip(type: Zip) { from compileJava from processResources into('lib') { from configurations.runtimeClasspath } } java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } build.dependsOn buildZip
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 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>example-java</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>example-java-function</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> <version>2.28.29</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>2.0.16</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>3.5.2</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*</exclude> <exclude>META-INF/versions/**</exclude> </excludes> </filter> </filters> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <release>21</release> </configuration> </plugin> </plugins> </build> </project>

Para que esta función se ejecute correctamente, su rol de ejecución debe permitir la acción s3:PutObject. Además, asegúrese de definir la variable de entorno RECEIPT_BUCKET. Tras una invocación correcta, el bucket de Amazon S3 debe contener un archivo de recibo.

nota

Es posible que esta función requiera configuración adicional para ejecutarse correctamente sin que se agote el tiempo de espera. Recomendamos configurar 256 MB de memoria y un tiempo de espera de 10 segundos. La primera invocación puede demorar más tiempo debido a un arranque en frío. Las invocaciones posteriores deberían ejecutarse mucho más rápido, ya que se reutiliza el entorno de ejecución.

Definiciones de clase válidas para los controladores de Java

Para definir su clase, la biblioteca aws-lambda-java-core define dos interfaces para los métodos de controlador. Utilice las interfaces proporcionadas para simplificar la configuración del controlador y validar la firma del método en tiempo de compilación.

La interfaz RequestHandler es un tipo genérico que toma dos parámetros: el tipo de entrada y el tipo de salida. Los dos tipos deben ser objetos. En este ejemplo, nuestra clase OrderHandler implementa RequestHandler<OrderHandler.Order, String>. El tipo de entrada es el registro Order que definimos dentro de la clase y el tipo de salida es String.

public class OrderHandler implements RequestHandler<OrderHandler.Order, String> { ... }

Cuando se utiliza esta interfaz, el tiempo de ejecución de Java deserializa el evento en un objeto con el tipo de entrada y serializa la salida en texto. Utilice esta interfaz si la serialización integrada funcione con los tipos de entrada y salida.

Para usar su propia serialización, puede implementar la interfaz RequestStreamHandler. Con esta interfaz, Lambda pasa al controlador un flujo de entrada y otrp flujo de salida. El controlador lee los bytes de la secuencia de entrada, escribe en la secuencia de salida y devuelve void. Para ver un ejemplo de esto con el tiempo de ejecución de Java 21, consulte HandlerStream.java.

Si solo trabaja con tipos básicos y genéricos (es decir String, Integer, =List o Map) en su función de Java, no necesita implementar una interfaz. Por ejemplo, si la función recibe una entrada Map<String, String> y devuelve un String, la definición de la clase y la firma del controlador pueden verse como el siguiente ejemplo:

public class ExampleHandler { public String handleRequest(Map<String, String> input, Context context) { ... } }

Además, cuando no implementa una interfaz, el objeto de contexto es opcional. Por ejemplo, la definición de la clase y la firma del controlador pueden verse como el siguiente ejemplo:

public class NoContextHandler { public String handleRequest(Map<String, String> input) { ... } }

Convenciones de nomenclatura de controladores

Para las funciones de Lambda en Java, si implementa la interfaz de RequestHandler o RequestStreamHandler, el método de su controlador principal debe tener handleRequest como nombre. Además, incluya la etiqueta @Override situada sobre el método handleRequest. Cuando implemente la función en Lambda, especifique el controlador principal en la configuración de la función en el siguiente formato:

  • <package>.<Class> – Por ejemplo, example.OrderHandler.

En el caso de las funciones de Lambda en Java que no implementan la interfaz de RequestHandler o RequestStreamHandler, es posible utilizar cualquier nombre para el controlador. Cuando implemente la función en Lambda, especifique el controlador principal en la configuración de la función en el siguiente formato:

  • <package>.<Class>::<handler_method_name> – Por ejemplo, example.Handler::mainHandler.

Definición del objeto de evento de entrada y acceso a él

El formato de entrada más común y estándar de las funciones de Lambda es JSON. En este ejemplo, la función espera una entrada similar a la siguiente:

{ "order_id": "12345", "amount": 199.99, "item": "Wireless Headphones" }

Al trabajar con funciones de Lambda en Java 17 o versiones posteriores, puede definir la forma del evento de entrada esperado como un registro de Java. En este ejemplo, definimos un registro dentro de la clase OrderHandler para representar un objeto Order:

public record Order(String orderId, double amount, String item) {}

Este registro coincide con la forma de entrada esperada. Después de definir el registro, puede escribir una firma de controlador que incluya una entrada de JSON que se ajuste a la definición del registro. El tiempo de ejecución de Java deserializa automáticamente este JSON en un objeto Java. Ahora puede acceder a los campos del objeto. Por ejemplo, event.orderId recupera el valor de orderId de la entrada original.

nota

Los registros de Java son una característica de los tiempos de ejecución de Java 17 y versiones posteriores únicamente. En todos los entornos de ejecución de Java, puede usar una clase para representar los datos de eventos. En esos casos, puede usar una biblioteca como jackson para deserializar las entradas de JSON.

Otros tipos de eventos de entrada

Hay muchos eventos de entrada posibles para las funciones de Lambda en Java:

  • Integer, Long, Double, etc.: - El evento es un número sin formato adicional; por ejemplo, 3.5. El tiempo de ejecución convierte el valor en un objeto del tipo especificado.

  • String: el evento es una cadena JSON, incluidas las comillas; por ejemplo, “My string”. El tiempo de ejecución convierte el valor en un objeto String sin comillas.

  • List<Integer>, List<String>, List<Object>, etc.: - El evento es una matriz JSON. El entorno de ejecución lo deserializa en un objeto del tipo especificado o una interfaz.

  • InputStream: el evento es cualquier tipo JSON. El entorno de ejecución pasa una secuencia de bytes del documento al controlador sin modificaciones. Usted deserializa la entrada y escribe la salida en una secuencia de salida.

  • Tipo de biblioteca: en el caso de los eventos enviados por otros servicios de AWS, utilice los tipos de la biblioteca aws-lambda-java-events. Por ejemplo, si Amazon Simple Queue Service (SQS) invoca la función de Lambda, utilice el objeto SQSEvent como entrada.

Acceso y uso del objeto de contexto de Lambda

El objeto de contexto de Lambda contiene información sobre la invocación, la función y el entorno de ejecución. En este ejemplo, el objeto de contexto es de tipo com.amazonaws.services.lambda.runtime.Context y es el segundo argumento de la función del controlador principal.

public String handleRequest(Order event, Context context) { ... }

Si la clase implementa la interfaz RequestHandler o RequestStreamHandler, el objeto de contexto es un argumento obligatorio. De lo contrario, el objeto de contexto es opcional. Para obtener más información sobre las firmas de controlador válidas, consulte Definiciones de clase válidas para los controladores de Java.

Si hace llamadas a otros servicios mediante el AWS SDK, el objeto de contexto es obligatorio en algunas áreas clave. Por ejemplo, para generar registros de funciones para Amazon CloudWatch, puede utilizar el método context.getLogger() a fin de obtener un objeto LambdaLogger para el registro. En este ejemplo, podemos usar el registrador para registrar un mensaje de error si el procesamiento falla por cualquier motivo:

context.getLogger().log("Failed to process order: " + e.getMessage());

Además del registro, también puede usar el objeto de contexto para supervisar las funciones. Para obtener más información acerca del objeto de contexto, consulte Uso del objeto de contexto Lambda para recuperar información de funciones de Java.

Uso de la versión 2 de AWS SDK para Java en el controlador

A menudo, utilizará las funciones de Lambda para interactuar con otros recursos de AWS o actualizarlos. La forma más sencilla de interactuar con estos recursos es utilizar la versión 2 de AWS SDK para Java.

nota

AWS SDK para Java (versión 1) está en modo de mantenimiento y está previsto que deje de ser compatible el 31 de diciembre de 2025. Le recomendamos que utilice únicamente la versión 2 de AWS SDK para Java de ahora en adelante.

Para agregar dependencias del SDK a su función, agréguelas al archivo build.gradle para Gradle o al archivo pom.xml para Maven. Le recomendamos que solo agregue las bibliotecas que necesite para su función. En el código de ejemplo anterior, utilizamos la biblioteca software.amazon.awssdk.services.s3. En Gradle, puede agregar esta dependencia agregando la siguiente línea en la sección de dependencias de build.gradle:

implementation 'software.amazon.awssdk:s3:2.28.29'

En Maven, agregue las siguientes líneas en la sección <dependencies> de pom.xml:

<dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> <version>2.28.29</version> </dependency>
nota

Es posible que esta no sea la versión más reciente del SDK. Elija la versión del SDK adecuada para la aplicación.

Luego, importe las dependencias directamente en la clase de Java:

import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.S3Exception;

El código de ejemplo inicializa un cliente de Amazon S3 de la siguiente manera:

private static final S3Client S3_CLIENT = S3Client.builder().build();

En este ejemplo, inicializamos el cliente de Amazon S3 fuera de la función del controlador principal para evitar tener que inicializarlo cada vez que invocamos nuestra función. Después de inicializar el cliente del SDK, podrá usarlo para interactuar con otros servicios de AWS. El código de ejemplo llama a la API PutObject de Amazon S3 de la siguiente manera:

PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(bucketName) .key(key) .build(); // Convert the receipt content to bytes and upload to S3 S3_CLIENT.putObject(putObjectRequest, RequestBody.fromBytes(receiptContent.getBytes(StandardCharsets.UTF_8)));

Acceso a las variables de entorno

En el código del controlador, puede hacer referencia a cualquier variable de entorno mediante el método System.getenv(). En este ejemplo, hacemos referencia a la variable de entorno RECEIPT_BUCKET definida mediante la siguiente línea de código:

String bucketName = System.getenv("RECEIPT_BUCKET"); if (bucketName == null || bucketName.isEmpty()) { throw new IllegalArgumentException("RECEIPT_BUCKET environment variable is not set"); }

Uso del estado global

Lambda ejecuta su código estático y el constructor de clases durante la fase de inicialización antes de invocar la función por primera vez. Los recursos que se crean durante la inicialización permanecen en la memoria entre las invocaciones, por lo que evita tener que crearlos cada vez que invoca la función.

En el código de ejemplo, el código de inicialización del cliente S3 está fuera del método del controlador principal. El tiempo de ejecución inicializa el cliente antes de que la función gestione su primer evento, lo que puede provocar tiempos de procesamiento más largos. Los eventos posteriores son mucho más rápidos porque Lambda no necesita volver a inicializar el cliente.

Prácticas recomendadas de codificación para las funciones de Lambda

Siga las directrices de la siguiente lista para utilizar las prácticas recomendadas de codificación al crear sus funciones de Lambda:

  • Separe el controlador de Lambda de la lógica del núcleo. Esto le permite probar las distintas unidades de la función con mayor facilidad.

  • Controle las dependencias del paquete de implementación de la función. El entorno de ejecución AWS Lambda contiene varias bibliotecas. Para disponer del conjunto más reciente de características y actualizaciones de seguridad, Lambda actualizará periódicamente estas bibliotecas. Estas actualizaciones pueden introducir cambios sutiles en el comportamiento de la función de Lambda. Para disponer de un control total de las dependencias que utiliza la función, empaquete todas las dependencias con el paquete de implementación.

  • Minimice la complejidad de las dependencias. Son preferibles los marcos de trabajo más sencillos, ya que se cargan rápidamente al arrancar el entorno de ejecución. Por ejemplo, es preferible utilizar los marcos de trabajo de inserción de dependencias (IoC) de Java más sencillos, como Dagger o Guice, que los más complejos, como Spring Framework.

  • Minimice el tamaño del paquete de implementación de acuerdo con las necesidades de su tiempo de ejecución. Esto reducirá la cantidad de tiempo que tarda el paquete de implementación en descargarse y desempaquetarse antes de la invocación. En las funciones creadas en Java, evite cargar toda la biblioteca de AWS SDK como parte del paquete de implementación. En lugar de ello, cree dependencias selectivas de los módulos que seleccionen los componentes del SDK que necesita (por ejemplo, DynamoDB, módulos del SDK de Amazon S3 y bibliotecas básicas de Lambda).

  • Reutilice el entorno de ejecución para mejorar el rendimiento de la función. Inicialice los clientes de SDK y las conexiones de base de datos fuera del controlador de funciones y almacene localmente en caché los recursos estáticos en el directorio /tmp. Las invocaciones posteriores procesadas por la misma instancia de su función pueden reutilizar estos recursos. Esto ahorra costes al reducir el tiempo de ejecución de la función.

    Para evitar posibles filtraciones de datos entre las invocaciones, no utilice el entorno de ejecución para almacenar datos de usuario, eventos u otra información con implicaciones de seguridad. Si su función se basa en un estado mutable que no se puede almacenar en la memoria dentro del controlador, considere crear una función independiente o versiones independientes de una función para cada usuario.

  • Utilice una directiva keep-alive para mantener conexiones persistentes. Lambda purga las conexiones inactivas a lo largo del tiempo. Si intenta reutilizar una conexión inactiva al invocar una función, se producirá un error de conexión. Para mantener la conexión persistente, use la directiva keep-alive asociada al tiempo de ejecución. Para ver un ejemplo, consulte Reutilización de conexiones con Keep-Alive en Node.js.

  • Utilice variables de entorno para pasar parámetros operativos a su función. Por ejemplo, si está escribiendo en un bucket de Amazon S3, en lugar de codificar de forma rígida el nombre del bucket, configúrelo como una variable de entorno.

  • Evite utilizar invocaciones recursivas en la función de Lambda, en las que la función se invoca a sí misma o inicia un proceso que puede volver a invocarla. Esto podría producir un volumen no intencionado de invocaciones de la función y costos elevados. Si observa un volumen imprevisto de invocaciones, establezca la simultaneidad reservada de funciones en 0 inmediatamente para limitar todas las invocaciones de la función mientras actualiza el código.

  • No utilice API no documentadas y no públicas en el código de la función de Lambda. Para tiempos de ejecución administrados de AWS Lambda, Lambda aplica periódicamente actualizaciones funcionales y de seguridad a las API internas de Lambda. Estas actualizaciones de las API internas pueden ser incompatibles con versiones anteriores, lo que conlleva consecuencias no deseadas, como errores de invocación si su función depende de estas API no públicas. Consulte la referencia de la API para obtener una lista de las API disponibles públicamente.

  • Escriba el código idempotente. Escribir el código idempotente para las funciones garantiza que los eventos duplicados se gestionen de la misma manera. El código debe validar y gestionar correctamente los eventos duplicados. Para obtener más información, consulte ¿Cómo puedo hacer que mi función de Lambda sea idempotente?.

  • Evite utilizar la caché de DNS de Java. Las funciones de Lambda ya almacenan en caché las respuestas de DNS. Si utiliza otra caché de DNS, es posible que se agoten los tiempos de espera de conexión.

    La clase java.util.logging.Logger puede habilitar indirectamente la caché de DNS de la JVM. Para anular la configuración predeterminada, defina networkaddress.cache.ttl en 0 antes de inicializar logger. Ejemplo:

    public class MyHandler { // first set TTL property static{ java.security.Security.setProperty("networkaddress.cache.ttl" , "0"); } // then instantiate logger var logger = org.apache.logging.log4j.LogManager.getLogger(MyHandler.class); }
  • Reduzca el tiempo que tarda Lambda en desempaquetar los paquetes de implementación escritos en Java colocando los archivos .jar de dependencias en un directorio /lib independiente. Esto es más rápido que colocar todo el código de la función en un único archivo jar con un gran número de archivos .class. Para obtener instrucciones, consulte Implementar funciones de Lambda Java con archivos de archivo .zip o JAR.