Writing an Enterprise-Grade AWS Lambda in Java – 用Java编写一个企业级的AWS Lambda

最后修改: 2021年 6月 1日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

It doesn’t require much code to put together a basic AWS Lambda in Java. To keep things small, we usually create our serverless applications with no framework support.

在Java中组建一个基本的AWS Lambda并不需要太多的代码。为了保持规模,我们通常在没有框架支持的情况下创建无服务器应用程序。

However, if we need to deploy and monitor our software at enterprise quality, we need to solve many of the problems that are solved out-of-the-box with frameworks like Spring.

然而,如果我们需要以企业的质量来部署和监控我们的软件,我们就需要解决许多像Spring这样的框架所能解决的开箱即用的问题。

In this tutorial, we’ll look at how to include configuration and logging capabilities in an AWS Lambda, as well as libraries that reduce boilerplate code, while still keeping things lightweight.

在本教程中,我们将研究如何在AWS Lambda中包含配置和日志功能,以及减少模板代码的库,同时仍然保持轻量级。

2. Building an Example

2.建立一个例子

2.1. Framework Options

2.1.框架选项

Frameworks like Spring Boot cannot be used to create AWS Lambdas. The Lambda has a different lifecycle from a server application, and it interfaces with the AWS runtime without directly using HTTP.

Spring Boot等框架不能用于创建AWS Lambdas。Lambda有一个与服务器应用程序不同的生命周期,它与AWS运行时的接口不直接使用HTTP。

Spring offers Spring Cloud Function, which can help us create an AWS Lambda, but we often need something smaller and simpler.

Spring提供了Spring Cloud Function,它可以帮助我们创建AWS Lambda,但我们通常需要更小更简单的东西。

We’ll take inspiration from DropWizard, which has a smaller feature set than Spring but still supports common standards, including configurability, logging, and dependency injection.

我们将从DropWizard中获得灵感,它的功能集比Spring小,但仍然支持通用标准,包括可配置性、日志和依赖注入。

While we may not need every one of these features from one Lambda to the next, we’ll build an example that solves all of these problems, so we can choose which techniques to use in future development.

虽然我们可能不需要从一个Lambda到下一个Lambda的每一个功能,但我们将建立一个解决所有这些问题的例子,所以我们可以在未来的开发中选择使用哪些技术。

2.2. Example Problem

2.2.示例问题

Let’s create an app that runs every few minutes. It’ll look at a “to-do list”, find the oldest job that’s not marked as done, and then create a blog post as an alert. It will also produce helpful logs to allow CloudWatch alarms to alert on errors.

让我们创建一个每几分钟运行一次的应用程序。它将查看一个 “待办事项列表”,找到未标记为已完成的最古老的工作,然后创建一篇博文作为警报。它还会产生有用的日志,让CloudWatch警报器对错误发出警报。

We’ll use the APIs on JsonPlaceholder as our back-end, and we’ll make the application configurable for both the base URLs of the APIs and the credentials we’ll use in that environment.

我们将使用JsonPlaceholder上的API作为我们的后端,并且我们将使应用程序对API的基本URL和我们将在该环境中使用的凭证都进行配置。

2.3. Basic Setup

2.3.基本设置

We’ll use the AWS SAM CLI to create a basic Hello World Example.

我们将使用AWS SAM CLI来创建一个基本的Hello World Example

Then we’ll change the default App class, which has an example API handler in it, into a simple RequestStreamHandler that logs on startup:

然后我们将把默认的App类(其中有一个API处理程序的例子)改为一个简单的RequestStreamHandler,在启动时记录。

public class App implements RequestStreamHandler {

    @Override
    public void handleRequest(
      InputStream inputStream, 
      OutputStream outputStream, 
      Context context) throws IOException {
        context.getLogger().log("App starting\n");
    }
}

As our example is not an API handler, we won’t need to read any input or produce any output. Right now, we’re using the LambdaLogger inside the Context passed to our function to do logging, though later on, we’ll look at how to use Log4j and Slf4j.

由于我们的例子不是一个API处理程序,我们不需要读取任何输入或产生任何输出。现在,我们使用传递给我们函数的Context内的LambdaLogger来做日志记录,尽管稍后我们会看看如何使用Log4jSlf4j

Let’s quickly test this:

让我们快速测试一下。

$ sam build
$ sam local invoke

Mounting todo-reminder/.aws-sam/build/ToDoFunction as /var/task:ro,delegated inside runtime container
App starting
END RequestId: 2aaf6041-cf57-4414-816d-76a63c7109fd
REPORT RequestId: 2aaf6041-cf57-4414-816d-76a63c7109fd  Init Duration: 0.12 ms  Duration: 121.70 ms
  Billed Duration: 200 ms Memory Size: 512 MB     Max Memory Used: 512 MB 

Our stub application has started up and logged “App starting” to the logs.

我们的存根应用程序已经启动,并将“App starting”记录到日志中。

3. Configuration

3.配置

As we may deploy our application to multiple environments, or wish to keep things like credentials separate from our code, we need to be able to pass in configuration values at deployment or runtime. This is most commonly achieved by setting environment variables.

由于我们可能会将我们的应用程序部署到多个环境中,或者希望将证书等东西与我们的代码分开,我们需要能够在部署或运行时传入配置值。这通常是通过设置环境变量来实现的。

3.1. Adding Environment Variables to the Template

3.1.在模板中添加环境变量

The template.yaml file contains the settings for the lambda. We can add environment variables to our function using the Environment section under AWS::Serverless::Function section:

template.yaml文件包含lambda的设置。我们可以使用AWS::Serverless::Function部分下的Environment部分来为我们的函数添加环境变量。

Environment: 
  Variables:
    PARAM1: VALUE

The generated example template has a hard-coded environment variable PARAM1, but we need to set our environment variables at deployment time.

生成的示例模板有一个硬编码的环境变量PARAM1,但我们需要在部署时设置我们的环境变量。

Let’s imagine that we want our application to know the name of its environment in a variable ENV_NAME.

让我们想象一下,我们希望我们的应用程序在一个变量ENV_NAME中知道其环境的名称。

First, let’s add a parameter to the very top of the template.yaml file with a default environment name:

首先,让我们在template.yaml文件的最上面添加一个参数,并给出一个默认的环境名称。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: todo-reminder application

Parameters:
  EnvironmentName:
    Type: String
    Default: dev

Next, let’s connect that parameter to an environment variable in the AWS::Serverless::Function section:

接下来,让我们在AWS::Serverless::Function部分将该参数连接到一个环境变量。

Environment: 
  Variables: 
    ENV_NAME: !Ref EnvironmentName

Now, we’re ready to read the environment variable at runtime.

现在,我们已经准备好在运行时读取环境变量。

3.2. Read an Environment Variable

3.2.读取一个环境变量

Let’s read the environment variable ENV_NAME upon the construction of our App object:

让我们在构建我们的App对象时读取环境变量 ENV_NAME

private String environmentName = System.getenv("ENV_NAME");

We can also log the environment when handleRequest is called:

我们也可以在handleRequest被调用时记录环境。

context.getLogger().log("Environment: " + environmentName + "\n");

The log message must end in “\n” to separate logging lines. We can see the output:

日志信息必须以“\n”结尾,以分隔日志行。我们可以看到输出。

$ sam build
$ sam local invoke

START RequestId: 12fb0c05-f222-4352-a26d-28c7b6e55ac6 Version: $LATEST
App starting
Environment: dev

Here, we see that the environment has been set from the default in template.yaml.

在这里,我们看到环境已经从template.yaml中的默认设置。

3.3. Changing Parameter Values

3.3.改变参数值

We can use parameter overrides to supply a different value at runtime or deploy time:

我们可以使用参数覆盖来在运行时或部署时提供不同的值。

$ sam local invoke --parameter-overrides "ParameterKey=EnvironmentName,ParameterValue=test"

START RequestId: 18460a04-4f8b-46cb-9aca-e15ce959f6fa Version: $LATEST
App starting
Environment: test

3.4. Unit Testing with Environment Variables

3.4.用环境变量进行单元测试

As an environment variable is global to the application, we might be tempted to initialize it in a private static final constant. However, this makes it very difficult to unit test.

由于环境变量对应用程序来说是全局性的,我们可能会倾向于在private static final常量中初始化它。然而,这使得单元测试变得非常困难。

As the handler class is initialized by the AWS Lambda runtime as a singleton for the entire life of the application, it’s better to use instance variables of the handler to store the runtime state.

由于处理程序类在应用程序的整个生命周期中被AWS Lambda运行时初始化为单子,所以最好使用处理程序的实例变量来存储运行时状态。

We can use System Stubs to set an environment variable, and Mockito deep stubs to make our LambdaLogger testable inside the Context. First, we have to add the MockitoJUnitRunner to the test:

我们可以使用System Stubs来设置环境变量,以及Mockito deep stubs来使我们的LambdaLogger可以在Context内测试。首先,我们必须将MockitoJUnitRunner添加到测试中。

@RunWith(MockitoJUnitRunner.class)
public class AppTest {

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private Context mockContext;

    // ...
}

Next, we can use an EnvironmentVariablesRule to enable us to control the environment variable before the App object is created:

接下来,我们可以使用一个EnvironmentVariablesRule,使我们能够在App对象创建之前控制环境变量。

@Rule
public EnvironmentVariablesRule environmentVariablesRule = 
  new EnvironmentVariablesRule();

Now, we can write the test:

现在,我们可以写测试了。

environmentVariablesRule.set("ENV_NAME", "unitTest");
new App().handleRequest(fakeInputStream, fakeOutputStream, mockContext);

verify(mockContext.getLogger()).log("Environment: unitTest\n");

As our lambdas get more complicated, it’s very useful to be able to unit test the handler class, including the way it loads its configuration.

随着我们的lambdas变得越来越复杂,能够对处理程序类进行单元测试是非常有用的,包括它加载配置的方式。

4. Handling Complex Configurations

4.处理复杂的配置

For our example, we’ll need the endpoint addresses for our API, as well as the name of the environment. The endpoint might vary at test time, but it has a default value.

对于我们的例子,我们将需要我们的API的端点地址,以及环境的名称。端点在测试时可能会有所不同,但它有一个默认值。

We can use System.getenv several times over, and even use Optional and orElse to drop to a default:

我们可以多次使用System.getenv,甚至使用OptionalorElse来下降到默认值。

String setting = Optional.ofNullable(System.getenv("SETTING"))
  .orElse("default");

However, this can require a lot of repetitive code and coordination of lots of individual Strings.

然而,这可能需要大量的重复性代码和协调大量单独的Strings。

4.1. Represent the Configuration as a POJO

4.1.以POJO形式表示配置

If we build a Java class to contain our configuration, we can share that with the services that need it:

如果我们建立一个Java类来包含我们的配置,我们可以与需要它的服务共享。

public class Config {
    private String toDoEndpoint;
    private String postEndpoint;
    private String environmentName;

    // getters and setters
}

Now we can construct our runtime components with the current configuration:

现在我们可以用当前的配置构建我们的运行时组件。

public class ToDoReaderService {
    public ToDoReaderService(Config configuration) {
        // ...
    }
}

The service can take any configuration values it needs from the Config object. We can even model the configuration as a hierarchy of objects, which may be useful if we have repeated structures like credentials:

服务可以从Config对象中获取它需要的任何配置值。我们甚至可以将配置建模为对象的层次结构,如果我们有重复的结构(如凭证),这可能很有用。

private Credentials toDoCredentials;
private Credentials postCredentials;

So far, this is just a design pattern. Let’s look at how to load these values in practice.

到目前为止,这还只是一种设计模式。让我们来看看如何在实践中加载这些值。

4.2. Configuration Loader

4.2.配置加载器

We can use lightweight-config to load our configuration from a .yml file in our resources.

我们可以使用lightweight-config从资源中的.yml文件加载我们的配置

Let’s add the dependency to our pom.xml:

让我们把依赖添加到我们的pom.xml

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>lightweight-config</artifactId>
    <version>1.1.0</version>
</dependency>

And then, let’s add a configuration.yml file to our src/main/resources directory. This file mirrors the structure of our configuration POJO and contains hardcoded values, placeholders to fill in from environment variables, and defaults:

然后,让我们在src/main/resources目录下添加一个configuration.yml文件。这个文件反映了我们的配置POJO的结构,并包含了硬编码值、从环境变量中填充的占位符和默认值。

toDoEndpoint: https://jsonplaceholder.typicode.com/todos
postEndpoint: https://jsonplaceholder.typicode.com/posts
environmentName: ${ENV_NAME}
toDoCredentials:
  username: baeldung
  password: ${TODO_PASSWORD:-password}
postCredentials:
  username: baeldung
  password: ${POST_PASSWORD:-password}

We can load these settings into our POJO using the ConfigLoader:

我们可以使用ConfigLoader将这些设置加载到我们的POJO中。

Config config = ConfigLoader.loadYmlConfigFromResource("configuration.yml", Config.class);

This fills in the placeholder expressions from the environment variables, applying defaults after the :- expressions. It’s quite similar to the configuration loader built into DropWizard.

它从环境变量中填入占位符表达式,在:-表达式后应用默认值。它与DropWizard中内置的配置加载器非常相似。

4.3. Holding the Context Somewhere

4.3.在某处保持语境

If we have several components – including the configuration – to load when the lambda first starts, it can be useful to keep these in a central place.

如果我们有几个组件–包括配置–要在lambda第一次启动时加载,把这些组件放在一个中心位置可能会很有用。

Let’s create a class called ExecutionContext that the App can use for object creation:

让我们创建一个名为ExecutionContext的类,App可以用来创建对象。

public class ExecutionContext {
    private Config config;
    private ToDoReaderService toDoReaderService;
    
    public ExecutionContext() {
        this.config = 
          ConfigLoader.loadYmlConfigFromResource("configuration.yml", Config.class);
        this.toDoReaderService = new ToDoReaderService(config);
    }
}

The App can create one of these in its initializer list:

App可以在其初始化器列表中创建其中一个。

private ExecutionContext executionContext = new ExecutionContext();

Now, when the App needs a “bean”, it can get it from this object.

现在,当App需要一个 “bean “时,它可以从这个对象中得到它。

5. Better Logging

5.更好的日志记录

So far, our use of the LambdaLogger has been very basic. If we bring in libraries that perform logging, the chances are that they’ll expect Log4j or Slf4j to be present. Ideally, our log lines will have timestamps and other useful context information.

到目前为止,我们对LambdaLogger的使用是非常基本的。如果我们引入执行日志记录的库,那么他们会期望Log4jSlf4j存在。理想情况下,我们的日志行将有时间戳和其他有用的上下文信息。

Most importantly, when we encounter errors, we ought to log them with plenty of useful information, and Logger.error usually does a better job at this task than homemade code.

最重要的是,当我们遇到错误时,我们应该用大量有用的信息来记录它们,而Logger.error通常比自制的代码更能完成这项任务。

5.1. Add the AWS Log4j Library

5.1.添加AWS Log4j库

We can enable the AWS lambda Log4j runtime by adding dependencies to our pom.xml:

我们可以通过向我们的pom.xml添加依赖项来启用AWS lambda Log4j运行时

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-log4j2</artifactId>
    <version>1.2.0</version>
</dependency>

We also need a log4j2.xml file in src/main/resources configured to use this logger:

我们还需要在src/main/resources中配置一个log4j2.xml文件来使用这个记录器。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration packages="com.amazonaws.services.lambda.runtime.log4j2">
    <Appenders>
        <Lambda name="Lambda">
            <PatternLayout>
                <pattern>%d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1} - %m%n</pattern>
            </PatternLayout>
        </Lambda>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Lambda" />
        </Root>
    </Loggers>
</Configuration>

5.2. Writing a Logging Statement

5.2.编写记录语句

Now, we add the standard Log4j Logger boilerplate to our classes:

现在,我们将标准的Log4j Logger模板添加到我们的类中。

public class ToDoReaderService {
    private static final Logger LOGGER = LogManager.getLogger(ToDoReaderService.class);

    public ToDoReaderService(Config configuration) {
        LOGGER.info("ToDo Endpoint on: {}", configuration.getToDoEndpoint());
        // ...
    }

    // ...
}

Then we can test it from the command line:

然后我们可以从命令行中测试它。

$ sam build
$ sam local invoke

START RequestId: acb34989-980c-42e5-b8e4-965d9f497d93 Version: $LATEST
2021-05-23 20:57:15  INFO  ToDoReaderService - ToDo Endpoint on: https://jsonplaceholder.typicode.com/todos

5.3. Unit Testing Log Output

5.3.单元测试日志输出

In cases where testing log output is important, we can do that using System Stubs. Our configuration, optimized for AWS Lambda, directs the log output to System.out, which we can tap:

在测试日志输出很重要的情况下,我们可以使用System Stubs来实现。我们的配置,针对AWS Lambda进行了优化,将日志输出引向System.out,我们可以挖掘它。

@Rule
public SystemOutRule systemOutRule = new SystemOutRule();

@Test
public void whenTheServiceStarts_thenItOutputsEndpoint() {
    Config config = new Config();
    config.setToDoEndpoint("https://todo-endpoint.com");
    ToDoReaderService service = new ToDoReaderService(config);

    assertThat(systemOutRule.getLinesNormalized())
      .contains("ToDo Endpoint on: https://todo-endpoint.com");
}

5.4. Adding Slf4j Support

5.4.添加Slf4j支持

We can add Slf4j by adding the dependency:

我们可以通过添加依赖关系来添加Slf4j

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.13.2</version>
</dependency>

This allows us to see log messages from Slf4j enabled libraries. We can also use it directly:

这使得我们可以看到来自Slf4j启用的库的日志信息。我们也可以直接使用它。

public class ExecutionContext {
    private static final Logger LOGGER =
      LoggerFactory.getLogger(ExecutionContext.class);

    public ExecutionContext() {
        LOGGER.info("Loading configuration");
        // ...
    }

    // ...
}

Slf4j logging is routed through the AWS Log4j runtime:

Slf4j日志是通过AWS Log4j运行时进行路由。

$ sam local invoke

START RequestId: 60b2efad-bc77-475b-93f6-6fa7ddfc9f88 Version: $LATEST
2021-05-23 21:13:19  INFO  ExecutionContext - Loading configuration

6. Consuming a REST API with Feign

6.用Feign消耗一个REST API

If our Lambda consumes a REST service, we can use the Java HTTP libraries directly. However, there are benefits to using a lightweight framework.

如果我们的Lambda消费的是REST服务,我们可以直接使用Java HTTP库。然而,使用一个轻量级的框架也有好处。

OpenFeign is a great option for this. It allows us to plug in our choice of components for HTTP client, logging, JSON parsing, and much more.

OpenFeign是这方面的一个很好的选择。它允许我们在HTTP客户端、日志、JSON解析等方面插入我们选择的组件。

6.1. Adding Feign

6.1.添加佯装

We’ll use the Feign default client for this example, though the Java 11 client is also a very good option and works with the Lambda java11 runtime, based on Amazon Corretto.

在这个例子中,我们将使用Feign默认客户端,不过Java 11客户端也是一个非常好的选择,它可以与基于Amazon Corretto的Lambda java11运行时一起工作。

Additionally, we’ll use Slf4j logging and Gson as our JSON library:

此外,我们将使用Slf4j日志和Gson作为我们的JSON库。

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-core</artifactId>
    <version>11.2</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-slf4j</artifactId>
    <version>11.2</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>11.2</version>
</dependency>

We’re using Gson as our JSON library here because Gson is much smaller than Jackson. We could use Jackson, but this would make the start-up time slower. There’s also the option of using Jackson-jr, though this is still experimental.

我们在这里使用Gson作为我们的JSON库,因为GsonJackson小很多。我们可以使用Jackson,但这将使启动时间变慢。也可以选择使用Jackson-jr,尽管这仍然是实验性的。

6.2. Defining a Feign Interface

6.2.定义一个伪装的界面

First, we describe the API we’re going to call with an interface:

首先,我们用一个接口来描述我们要调用的API。

public interface ToDoApi {
    @RequestLine("GET /todos")
    List<ToDoItem> getAllTodos();
}

This describes the path within the API and any objects that are to be produced from the JSON response. Let’s create the ToDoItem to model the response from our API:

这描述了API中的路径以及将从JSON响应中产生的任何对象。让我们创建ToDoItem来模拟来自我们API的响应。

public class ToDoItem {
    private int userId;
    private int id;
    private String title;
    private boolean completed;

    // getters and setters
}

6.3. Defining a Client from the Interface

6.3.从界面上定义一个客户端

Next, we use the Feign.Builder to convert the interface into a client:

接下来,我们使用Feign.Builderinterface转换成客户端。

ToDoApi toDoApi = Feign.builder()
  .decoder(new GsonDecoder())
  .logger(new Slf4jLogger())
  .target(ToDoApi.class, config.getToDoEndpoint());

In our example, we’re also using credentials. Let’s say these are supplied via basic authentication, which would require us to add a BasicAuthRequestInterceptor before the target call:

在我们的例子中,我们也在使用凭证。假设这些证书是通过基本认证提供的,这就要求我们在target调用之前添加一个BasicAuthRequestInterceptor

.requestInterceptor(
   new BasicAuthRequestInterceptor(
     config.getToDoCredentials().getUsername(),
     config.getToDoCredentials().getPassword()))

7. Wiring the Objects Together

7.将物体连接起来

Up to this point, we’ve created the configurations and beans for our application, but we haven’t wired them together yet. We have two options for this. Either we wire the objects together using plain Java, or we use some sort of dependency injection solution.

到此为止,我们已经为我们的应用程序创建了配置和bean,但我们还没有把它们连在一起。对此我们有两个选择。要么我们用普通的Java将这些对象连接在一起,要么我们使用某种依赖注入的解决方案。

7.1. Constructor Injection

7.1.构造函数注入

As everything is a plain Java object, and as we’ve built the ExecutionContext class to coordinate construction, we can do all the work in its constructor.

由于一切都是普通的Java对象,而且我们已经建立了ExecutionContext类来协调构造,我们可以在其构造函数中完成所有工作。

We might expect to extend the constructor to build all the beans in order:

我们可能希望扩展构造函数,以按顺序构建所有的Bean。

this.config = ... // load config
this.toDoApi = ... // build api
this.postApi = ... // build post API
this.toDoReaderService = new ToDoReaderService(toDoApi);
this.postService = new PostService(postApi);

This is the simplest solution. It encourages well-defined components that are both testable and easy to compose at runtime.

这是最简单的解决方案。它鼓励定义良好的组件,这些组件既可测试又易于在运行时组成。

However, above a certain number of components, this starts to become long-winded and harder to manage.

然而,超过一定数量的组件,这就开始变得冗长和难以管理。

7.2. Bring in a Dependency Injection Framework

7.2.引入依赖注入框架

DropWizard uses Guice for dependency injection. This library is relatively small and can help manage the components in an AWS Lambda.

DropWizard使用Guice进行依赖性注入。这个库相对较小,可以帮助管理AWS Lambda中的组件。

Let’s add its dependency:

让我们添加其依赖性

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>5.0.1</version>
</dependency>

7.3. Use Injection Where It’s Easy

7.3.在容易的地方使用注入法

We can annotate beans constructed from other beans with the @Inject annotation to make them automatically injectable:

我们可以用@Inject注解来注解由其他Bean构建的Bean,使其自动成为可注入的

public class PostService {
    private PostApi postApi;

    @Inject
    public PostService(PostApi postApi) {
        this.postApi = postApi;
    }

    // other functions
}

7.4. Creating a Custom Injection Module

7.4.创建一个自定义注入模块

For any beans where we have to use custom load or construction code, we can use a Module as a factory:

对于任何我们必须使用自定义加载或构造代码的Bean,我们可以使用Module作为工厂

public class Services extends AbstractModule {
    @Override
    protected void configure() {
        Config config = 
          ConfigLoader.loadYmlConfigFromResource("configuration.yml", Config.class);

        ToDoApi toDoApi = Feign.builder()
          .decoder(new GsonDecoder())
          .logger(new Slf4jLogger())
          .logLevel(FULL)
          .requestInterceptor(... // omitted
          .target(ToDoApi.class, config.getToDoEndpoint());

        PostApi postApi = Feign.builder()
          .encoder(new GsonEncoder())
          .logger(new Slf4jLogger())
          .logLevel(FULL)
          .requestInterceptor(... // omitted
          .target(PostApi.class, config.getPostEndpoint());

        bind(Config.class).toInstance(config);
        bind(ToDoApi.class).toInstance(toDoApi);
        bind(PostApi.class).toInstance(postApi);
    }
}

Then we use this module inside our ExecutionContext via an Injector:

然后我们通过一个Injector在我们的ExecutionContext中使用这个模块。

public ExecutionContext() {
    LOGGER.info("Loading configuration");

    try {
        Injector injector = Guice.createInjector(new Services());
        this.toDoReaderService = injector.getInstance(ToDoReaderService.class);
        this.postService = injector.getInstance(PostService.class);
    } catch (Exception e) {
        LOGGER.error("Could not start", e);
    }
}

This approach scales well, as it localizes bean dependencies to the classes closest to each bean. With a central configuration class building every bean, any change in dependency always requires changes there, too.

这种方法具有良好的扩展性,因为它将Bean的依赖关系定位到最接近每个Bean的类。有了构建每个Bean的中心配置类,任何依赖性的改变都需要在那里进行改变。

We should also note that it’s important to log errors that occur during start-up — if this fails, the Lambda cannot run.

我们还应该注意,记录启动过程中发生的错误很重要–如果失败,Lambda就无法运行。

7.5. Using the Objects Together

7.5.共同使用这些对象

Now that we have an ExecutionContext with services that have the APIs inside them, configured by the Config, let’s complete our handler:

现在我们有了一个ExecutionContext,其中有由Config配置的API的服务,让我们完成我们的处理程序。

@Override
public void handleRequest(InputStream inputStream, 
  OutputStream outputStream, Context context) throws IOException {

    PostService postService = executionContext.getPostService();
    executionContext.getToDoReaderService()
      .getOldestToDo()
      .ifPresent(postService::makePost);
}

Let’s test this:

让我们来测试一下。

$ sam build
$ sam local invoke

Mounting /Users/ashleyfrieze/dev/tutorials/aws-lambda/todo-reminder/.aws-sam/build/ToDoFunction as /var/task:ro,delegated inside runtime container
2021-05-23 22:29:43  INFO  ExecutionContext - Loading configuration
2021-05-23 22:29:44  INFO  ToDoReaderService - ToDo Endpoint on: https://jsonplaceholder.typicode.com
App starting
Environment: dev
2021-05-23 22:29:44 73264c34-ca48-4c3e-a2b4-5e7e74e13960 INFO  PostService - Posting about: ToDoItem{userId=1, id=1, title='delectus aut autem', completed=false}
2021-05-23 22:29:44 73264c34-ca48-4c3e-a2b4-5e7e74e13960 INFO  PostService - Post: PostItem{title='To Do is Out Of Date: 1', body='Not done: delectus aut autem', userId=1}
END RequestId: 73264c34-ca48-4c3e-a2b4-5e7e74e13960

8. Conclusion

8.结语

In this article, we looked at the importance of features like configuration and logging when using Java to build an enterprise-grade AWS Lambda. We saw how frameworks like Spring and DropWizard provide these tools by default.

在这篇文章中,我们看了使用Java构建企业级AWS Lambda时,配置和日志等功能的重要性。我们看到Spring和DropWizard等框架是如何默认提供这些工具的。

We explored how to use environment variables to control configuration and how to structure our code to make unit testing possible.

我们探讨了如何使用环境变量来控制配置,以及如何构造我们的代码以使单元测试成为可能。

Then, we looked at libraries for loading configuration, building a REST client, marshaling JSON data, and wiring our objects together, with a focus on choosing smaller libraries to make our Lambda start as quickly as possible.

然后,我们研究了用于加载配置、建立REST客户端、处理JSON数据和将我们的对象连接在一起的库,重点是选择较小的库,使我们的Lambda尽可能快地启动。

As always, the example code can be found over on GitHub.

一如既往,可以在GitHub上找到示例代码