Definir o manipulador da função do Lambda em Java - AWS Lambda

Definir o manipulador da função do Lambda em Java

O manipulador da função do Lambda é o método no código da função que processa eventos. Quando sua função é invocada, o Lambda executa o método do manipulador. A função é executada até que o manipulador retorne uma resposta, seja encerrado ou atinja o tempo limite.

O repositório do GitHub para este guia fornece aplicativos de exemplo fáceis de implantar que demonstram uma variedade de tipos de manipuladores. Para obter detalhes, consulte o final deste tópico.

Exemplo de manipulador: runtimes do Java 17

No exemplo do Java 17 a seguir, uma classe chamada HandlerIntegerJava17 define um método do manipulador chamado handleRequest. O método do manipulador recebe as seguintes entradas:

  • Um IntegerRecord, que é um registro customizado do Java que representa os dados do evento. Neste exemplo, definimos IntegerRecord da seguinte forma:

    record IntegerRecord(int x, int y, String message) { }
  • Um objeto de contexto, que disponibiliza métodos e propriedades que fornecem informações sobre a invocação, a função e o ambiente de execução.

Suponha que desejemos escrever uma função que registre em log a message da entrada IntegerRecord e retorne a soma de x e y. Este é o código da função:

exemplo HandlerIntegerJava17.java
package example; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; // Handler value: example.HandlerInteger public class HandlerIntegerJava17 implements RequestHandler<IntegerRecord, Integer>{ @Override /* * Takes in an InputRecord, which contains two integers and a String. * Logs the String, then returns the sum of the two Integers. */ public Integer handleRequest(IntegerRecord event, Context context) { LambdaLogger logger = context.getLogger(); logger.log("String found: " + event.message()); return event.x() + event.y(); } } record IntegerRecord(int x, int y, String message) { }

Você especifica qual método deseja que o Lambda invoque ao definir o parâmetro do manipulador na configuração da função. É possível expressar o manipulador nos seguintes formatos:

  • package.Class::method: formato completo. Por exemplo: example.Handler::handleRequest.

  • package.Class: formato abreviado para classes que implantam uma interface de manipulador. Por exemplo: example.Handler.

Quando o Lambda invoca o manipulador, o runtime do Lambda recebe um evento como uma string formatada em JSON e o converte em um objeto. Para o exemplo anterior, uma amostra de evento poderá se assemelhar ao seguinte:

exemplo event.json
{ "x": 1, "y": 20, "message": "Hello World!" }

É possível salvar esse arquivo e testar a função localmente com o seguinte comando da AWS Command Line Interface (CLI):

aws lambda invoke --function-name function_name --payload file://event.json out.json

Exemplo de manipulador: runtimes do Java 11 e versões anteriores

O Lambda é compatível com registros para runtimes do Java 17 e versões posteriores. Em todos os runtimes do Java, é possível usar uma classe para representar os dados do evento. O exemplo a seguir usa uma lista de inteiros e um objeto de contexto como entrada e retorna a soma de todos os inteiros na lista.

exemplo Handler.java

No exemplo a seguir, uma classe chamada Handler define um método do manipulador chamado handleRequest. O método do manipulador usa um evento e um objeto de contexto como entrada e retorna uma string.

exemplo HandlerList.java
package example; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; import java.util.List; // Handler value: example.HandlerList public class HandlerList implements RequestHandler<List<Integer>, Integer>{ @Override /* * Takes a list of Integers and returns its sum. */ public Integer handleRequest(List<Integer> event, Context context) { LambdaLogger logger = context.getLogger(); logger.log("EVENT TYPE: " + event.getClass().toString()); return event.stream().mapToInt(Integer::intValue).sum(); } }

Para obter mais exemplos, consulte Código de exemplo do manipulador.

Código de inicialização

O Lambda executa seu código estático e o construtor de classe durante a fase de inicialização antes de invocar a função pela primeira vez. Os recursos criados durante a inicialização permanecem na memória entre as invocações e podem ser reutilizados pelo manipulador milhares de vezes. Dessa forma, é possível adicionar código de inicialização de forma externa ao método do manipulador principal para economizar tempo de computação e reutilizar recursos em diversas invocações.

No exemplo a seguir, o código de inicialização do cliente é externo ao método do manipulador principal. O runtime inicializa o cliente antes que a função veicule o primeiro evento. Os eventos subsequentes são muito mais rápidos porque o Lambda não precisa inicializar o cliente novamente.

exemplo Handler.java
package example; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; import java.util.Map; import software.amazon.awssdk.services.lambda.LambdaClient; import software.amazon.awssdk.services.lambda.model.GetAccountSettingsResponse; import software.amazon.awssdk.services.lambda.model.LambdaException; // Handler value: example.Handler public class Handler implements RequestHandler<Map<String,String>, String> { private static final LambdaClient lambdaClient = LambdaClient.builder().build(); @Override public String handleRequest(Map<String,String> event, Context context) { LambdaLogger logger = context.getLogger(); logger.log("Handler invoked"); GetAccountSettingsResponse response = null; try { response = lambdaClient.getAccountSettings(); } catch(LambdaException e) { logger.log(e.getMessage()); } return response != null ? "Total code size for your account is " + response.accountLimit().totalCodeSize() + " bytes" : "Error"; } }

Escolher tipos de entrada e saída

Especifique o tipo de objeto para o qual o evento é mapeado na assinatura do método do manipulador. No exemplo anterior, o runtime do Java desserializa o evento em um tipo que implementa a interface Map<String,String>. Mapas de string a string funcionam para eventos simples como o seguinte:

exemplo Event.json: dados climáticos
{ "temperatureK": 281, "windKmh": -3, "humidityPct": 0.55, "pressureHPa": 1020 }

No entanto, o valor de cada campo deve ser uma string ou um número. Se o evento incluir um campo que tenha um objeto como um valor, o runtime não poderá desserializá-lo e retornará um erro.

Escolha um tipo de entrada que funcione com os dados de evento que sua função processa. É possível usar um tipo básico, um tipo genérico ou um tipo bem definido.

Tipos de entrada
  • Integer, Long, Double etc. – o evento é um número sem formatação adicional, por exemplo, 3.5. O runtime converte o valor em um objeto do tipo especificado.

  • String: o evento é uma string JSON, incluindo aspas, por exemplo, "My string.". O runtime converte o valor (sem aspas) em um objeto String.

  • Type, Map<String,Type> etc. – o evento é um objeto JSON. O runtime o desserializa em um objeto da interface ou tipo especificado.

  • List<Integer>, List<String>, List<Object> etc. – o evento é uma matriz JSON. O runtime o desserializa em um objeto da interface ou tipo especificado.

  • InputStream: o evento é qualquer tipo JSON. O runtime transmite um fluxo de bytes do documento ao manipulador sem modificação. Desserialize a entrada e grave a saída em um fluxo de saída.

  • Tipo de biblioteca: para eventos enviados pelos serviços da AWS, use os tipos na biblioteca aws-lambda-java-events.

Se você definir seu próprio tipo de entrada, ele deve ser um objeto Java simples e antigo (POJO) desserializável e mutável, com um construtor padrão e propriedades para cada campo no evento. Chaves no evento que não mapeiem para uma propriedade e propriedades que não estejam incluídas no evento são descartadas sem erro.

O tipo de saída pode ser um objeto ou void. O runtime serializa valores de retorno em texto. Se a saída for um objeto com campos, o runtime serializa-o em um documento JSON. Se for um tipo que envolve um valor primitivo, o runtime retornará uma representação de texto desse valor.

Interfaces do manipulador

A biblioteca aws-lambda-java-core define duas interfaces para métodos do manipulador. Use as interfaces fornecidas para simplificar a configuração do manipulador e validar a assinatura do método do manipulador no momento da compilação.

A interface RequestHandler é um tipo genérico que usa dois parâmetros: o tipo de entrada e o tipo de saída. Ambos os tipos devem ser objetos. Quando você usa essa interface, o runtime do Java desserializa o evento em um objeto com o tipo de entrada e serializa a saída em texto. Use essa interface quando a serialização interna funcionar com seus tipos de entrada e saída.

exemplo Handler.java: interface do manipulador
// Handler value: example.Handler public class Handler implements RequestHandler<Map<String,String>, String>{ @Override public String handleRequest(Map<String,String> event, Context context)

Para usar sua própria serialização, implemente a interface RequestStreamHandler. Com essa interface, o Lambda transmite ao seu manipulador uma transmissão de entrada e uma de saída. O manipulador lê bytes do fluxo de entrada, grava no fluxo de saída e retorna um void.

O exemplo do Java 21 a seguir mostra como você poderia usar uma função do Lambda para processar pedidos. O exemplo usa tipos de leitor e gravador em buffer para trabalhar com os fluxos de entrada e saída, e mostra como você pode definir registros Java personalizados para usar em sua função.

exemplo HandlerStream.java
import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; public class HandlerStream implements RequestStreamHandler { private static final ObjectMapper objectMapper = new ObjectMapper(); @Override public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { Order order = objectMapper.readValue(input, Order.class); processOrder(order); OrderAccepted orderAccepted = new OrderAccepted(order.orderId); objectMapper.writeValue(output, orderAccepted); } private void processOrder(Order order) { // business logic } public record Order(@JsonProperty("orderId") String orderId, @JsonProperty("items") List<Item> items) { } public record Item(@JsonProperty("name") String name, @JsonProperty("quantity") Integer quantity) { } public record OrderAccepted(@JsonProperty("orderId") String orderId) { } }

Práticas recomendadas de código para as funções do Lambda em Java

Adote as diretrizes da lista a seguir para usar as práticas recomendadas de codificação ao compilar suas funções do Lambda:

  • Separe o manipulador do Lambda da lógica central. Isso permite que você crie uma função mais fácil para teste de unidade.

  • Controle as dependências no pacote de implantação da função. O ambiente de execução do AWS Lambda contém várias bibliotecas. Para habilitar o conjunto de recursos e atualizações de segurança mais recente, o Lambda atualizará periodicamente essas bibliotecas. Essas atualizações podem introduzir alterações sutis ao comportamento de sua função do Lambda. Para ter controle total das dependências usadas por sua função, empacote todas as dependências em seu pacote de implantação.

  • Minimize a complexidade de suas dependências. Prefira frameworks mais simples que sejam carregados rapidamente no startup do ambiente de execução. Por exemplo, prefira frameworks de injeção de dependência Java (IoC) mais simples, como Dagger ou Guice, em vez de frameworks mais complexos, como o Spring Framework.

  • Minimize o tamanho do pacote de implantação às necessidades do runtime. Isso reduzirá a quantidade de tempo necessária para que seu pacote de implantação seja obtido por download e desempacotado antes da invocação. Para funções criadas em .Java, evite carregar a biblioteca do AWS SDK inteira como parte do pacote de implantação. Em vez disso, dependa seletivamente dos módulos que coletam os componentes do SDK necessários (por exemplo, DynamoDB, módulos do SDK do Amazon S3 e bibliotecas principais do Lambda).

  • Aproveite a reutilização do ambiente de execução para melhorar a performance da função. Inicialize clientes SDK e conexões de banco de dados fora do manipulador de funções e armazene em cache os ativos estáticos localmente no diretório /tmp. As invocações subsequentes processadas pela mesma instância da função podem reutilizar esses recursos. Isso economiza custos reduzindo o runtime da função.

    Para evitar possíveis vazamentos de dados entre invocações, não use o ambiente de execução para armazenar dados do usuário, eventos ou outras informações com implicações de segurança. Se sua função depende de um estado mutável que não pode ser armazenado na memória dentro do manipulador, considere criar uma função separada ou versões separadas de uma função para cada usuário.

  • Use uma diretiva de keep-alive para manter conexões persistentes. O Lambda limpa conexões ociosas ao longo do tempo. A tentativa de reutilizar uma conexão ociosa ao invocar uma função resultará em um erro de conexão. Para manter sua conexão persistente, use a diretiva keep-alive associada ao runtime. Para obter um exemplo, consulte Reutilizar conexões com keep-alive em Node.js.

  • Use variáveis de ambiente para passar parâmetros operacionais para sua função. Por exemplo, se estiver gravando em um bucket do Amazon S3, em vez fixar no código o nome do bucket em que você está gravando, configure o nome do bucket como uma variável de ambiente.

  • Evite usar invocações recursivas em sua função do Lambda, em que a função invoca a si mesma ou inicia um processo que pode invocar a função novamente. Isso pode levar a um volume não intencional de invocações da função e a custos elevados. Se você observar um volume não intencional de invocações, defina a simultaneidade reservada da função como 0 imediatamente para limitar todas as invocações da função enquanto atualiza o código.

  • Não use APIs não documentadas e não públicas no código da função Lambda. Para os tempos de execução gerenciados pelo AWS Lambda, o Lambda aplica periodicamente atualizações funcionais e de segurança às APIs internas do Lambda. Essas atualizações internas da API podem ser incompatíveis com versões anteriores, gerando consequências não intencionais, como falhas de invocação, caso sua função tenha dependência nessas APIs não públicas. Consulte a referência da API para obter uma lista de APIs disponíveis publicamente.

  • Escreva um código idempotente. Escrever um código idempotente para suas funções garante que eventos duplicados sejam tratados da mesma maneira. Seu código deve validar eventos adequadamente e lidar corretamente com eventos duplicados. Para obter mais informações, consulte Como torno minha função do Lambda idempotente?.

  • Evite usar o cache do DNS Java. As funções do Lambda já armazenam as respostas de DNS em cache. Se usar outro cache do DNS, você poderá experimentar tempos limite de conexão.

    A classe java.util.logging.Logger pode habilitar indiretamente o cache do DNS da JVM. Para substituir as configurações padrão, defina networkaddress.cache.ttl como 0 antes de inicializar o logger. Exemplo:

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

    Para evitar falhas de UnknownHostException, é recomendável definir networkaddress.cache.negative.ttl como 0. Você pode definir essa propriedade para uma função do Lambda com a variável de ambiente AWS_LAMBDA_JAVA_NETWORKADDRESS_CACHE_NEGATIVE_TTL=0.

    Desabilitar o cache do DNS da JVM não desabilita o cache do DNS gerenciado do Lambda.

  • Reduza o tempo necessário para o Lambda desempacotar pacotes de implantação criados em Java colocando seus arquivos .jar de dependências em um diretório lib/separado. Isso é mais rápido do que colocar todo o código de sua função em um único jar com um grande número de arquivos .class. Para obter instruções, consulte Implantar funções do Lambda em Java com arquivos .zip ou JAR.

Código de exemplo do manipulador

O repositório do GitHub para este guia inclui aplicativos de exemplo que demonstram o uso de vários tipos e interfaces de manipuladores. Cada aplicativo de exemplo inclui scripts para fácil implantação e limpeza, um modelo do AWS SAM e recursos de suporte.

Aplicações de exemplo do Lambda em Java
  • java17-examples: uma função em Java que demonstra como usar um registro Java para representar um objeto de dados de evento de entrada.

  • java-basic: uma coleção de funções Java mínimas com testes de unidade e configuração de registro em log variável.

  • java-events: uma coleção de funções do Java contendo código básico sobre como lidar com eventos de vários serviços, como o Amazon API Gateway, o Amazon SQS e o Amazon Kinesis. Essas funções usam a versão mais recente da biblioteca aws-lambda-java-events (3.0.0 e versões mais recentes). Estes exemplos não exigem oAWS SDK como dependência.

  • s3-java: uma função em Java que processa eventos de notificação do Amazon S3 e usa a Java Class Library (JCL) para criar miniaturas de arquivos de imagem enviados por upload.

  • Use API Gateway to invoke a Lambda function (Usar o API Gateway para invocar uma função do Lambda): uma função Java que verifica uma tabela do Amazon DynamoDB contendo informações de funcionários. Em seguida, usa o Amazon Simple Notification Service para enviar uma mensagem de texto aos funcionários comemorando seus aniversários de empresa. Este exemplo usa o API Gateway para invocar a função.

As aplicações s3-java e java-events usam um evento de serviço da AWS como entrada e retornam uma string. O aplicativo java-basic inclui vários tipos de manipuladores:

Para testar diferentes tipos de manipuladores, basta alterar o valor do manipulador no modelo do AWS SAM. Para obter instruções detalhadas, consulte o arquivo readme do aplicativo de exemplo.