1. Overview
1.概述
AWS Lambda allows us to create lightweight applications that can be deployed and scaled easily. Though we can use frameworks like Spring Cloud Function, for performance reasons, we usually use as little framework code as possible.
AWS Lambda使我们能够创建可轻松部署和扩展的轻量级应用程序。尽管我们可以使用Spring Cloud Function等框架,但出于性能方面的考虑,我们通常尽可能少地使用框架代码。
Sometimes we need to access a relational database from a Lambda. This is where Hibernate and JPA can be very useful. But, how do we add Hibernate to our Lambda without Spring?
有时我们需要从Lambda中访问关系型数据库。这时,Hibernate和JPA可能会非常有用。但是,如果没有Spring,我们如何将Hibernate添加到我们的Lambda中?
In this tutorial, we’ll look at the challenges of using any RDBMS within a Lambda, and how and when Hibernate can be useful. Our example will use the Serverless Application Model to build a REST interface to our data.
在本教程中,我们将探讨在Lambda中使用任何RDBMS的挑战,以及Hibernate如何以及何时可以发挥作用。我们的例子将使用无服务器应用模型来建立一个REST接口,以获取我们的数据。
We’ll look at how to test everything on our local machine using Docker and the AWS SAM CLI.
我们将看看如何使用Docker和AWS SAM CLI在我们的本地机器上测试一切。
2. Challenges Using RDBMS and Hibernate in Lambdas
2.在Lambdas中使用RDBMS和Hibernate面临的挑战
Lambda code needs to be as small as possible to speed up cold starts. Also, a Lambda should be able to do its job in milliseconds. However, using a relational database can involve a lot of framework code and can run more slowly.
Lambda代码需要尽可能小,以加快冷启动速度。另外,Lambda应该能够在几毫秒内完成其工作。然而,使用关系型数据库会涉及大量的框架代码,运行速度会更慢。
In cloud-native applications, we try to design using cloud-native technologies. Serverless databases like Dynamo DB can be a better fit for Lambdas. However, the need for a relational database may come from some other priority within our project.
在云原生应用程序中,我们尝试使用云原生技术进行设计。像Dynamo DB这样的无服务器数据库可以更适合Lambdas。然而,对关系型数据库的需求可能来自于我们项目中的一些其他优先事项。
2.1. Using an RDBMS From a Lambda
2.1.从Lambda中使用RDBMS
Lambdas run for a small amount of time and then their container is paused. The container may be reused for a future invocation, or it may be disposed of by the AWS runtime if no longer needed. This means that any resources the container claims must be managed carefully within the lifetime of a single invocation.
Lambdas运行一小段时间后,其容器就会暂停。容器可能会被重新用于未来的调用,或者在不再需要时被AWS运行时处理掉。这意味着必须在单个调用的生命周期内谨慎管理容器所要求的任何资源。
Specifically, we cannot rely on conventional connection pooling for our database, as any connections opened could potentially stay open without being safely disposed of. We can use connection pools during the invocation, but we have to create the connection pool each time. Also, we need to shut down all connections and release all resources as our function ends.
具体来说,我们的数据库不能依靠传统的连接池,因为任何打开的连接都有可能在没有安全处置的情况下保持开放。我们可以在调用过程中使用连接池,但是我们每次都必须创建连接池。此外,我们需要在函数结束时关闭所有连接并释放所有资源。
This means that using a Lambda with a database can cause connection problems. A sudden upscale of our Lambda can consume too many connections. Although the Lambda may release connections straight away, we still rely on the database being able to prepare them for the next Lambda invocation. Therefore, it’s often a good idea to use a maximum concurrency limit on any Lambda that uses a relational database.
这意味着将Lambda与数据库一起使用会导致连接问题。我们的Lambda的突然升级会消耗太多的连接。虽然Lambda可能会直接释放连接,但我们仍然依赖于数据库能够为下一次Lambda调用准备连接。因此,在任何使用关系型数据库的Lambda上使用最大并发限制通常是个好主意。
In some projects, Lambda is not the best choice for connecting to an RDBMS, and a traditional Spring Data service, with a connection pool, perhaps running in EC2 or ECS, maybe a better solution.
在某些项目中,Lambda并不是连接RDBMS的最佳选择,传统的Spring Data服务,加上连接池,也许在EC2或ECS中运行,也许是更好的解决方案。
2.2. The Case for Hibernate
2.2.Hibernate的案例
A good way to determine if we need Hibernate is to ask what sort of code we’d have to write without it.
确定我们是否需要Hibernate的一个好方法是问,如果没有它,我们要写什么样的代码。
If not using Hibernate would cause us to have to code complex joins or lots of boilerplate mapping between fields and columns, then from a coding perspective, Hibernate is a good solution. If our application does not experience a high load or the need for low latency, then the overhead of Hibernate may not be an issue.
如果不使用Hibernate会导致我们不得不在字段和列之间编码复杂的连接或大量的模板映射,那么从编码的角度来看,Hibernate是一个很好的解决方案。如果我们的应用程序没有经历高负荷或低延迟的需求,那么Hibernate的开销可能就不是问题。
2.3. Hibernate Is a Heavyweight Technology
2.3.Hibernate是一个重量级的技术
However, we also need to consider the cost of using Hibernate in a Lambda.
然而,我们也需要考虑在Lambda中使用Hibernate的成本。
The Hibernate jar file is 7 MB in size. Hibernate takes time at start-up to inspect annotations and create its ORM capability. This is enormously powerful, but for a Lambda, it can be overkill. As Lambdas are usually written to perform small tasks, the overhead of Hibernate may not be worth the benefits.
Hibernate的jar文件大小为7MB。Hibernate在启动时需要时间来检查注释并创建其ORM能力。这是非常强大的,但对于Lambda来说,这可能是过犹不及。由于Lambda通常是为执行小任务而编写的,Hibernate的开销可能不值得。
It may be easier to use JDBC directly. Alternatively, a lightweight ORM-like framework such as JDBI may provide a good abstraction over queries, without too much overhead.
直接使用JDBC可能会更容易。另外,类似于ORM的轻量级框架,如JDBI可能会提供一个很好的查询抽象,而不会有太大的开销。
3. An Example Application
3.一个应用实例
In this tutorial, we’ll build a tracking application for a low-volume shipping company. Let’s imagine they collect large items from customers to create a Consignment. Then, wherever that consignment travels, it’s checked in with a timestamp, so the customer can monitor it. Each consignment has a source and destination, for which we’ll use what3words.com as our geolocation service.
在本教程中,我们将为一个小批量的运输公司建立一个跟踪应用程序。让我们设想一下,他们从客户那里收集大件物品,创建一个Consignment。然后,无论该托运货物在哪里旅行,它都会被签入一个时间戳,这样客户就可以监控它。每个寄售物品都有一个来源和目的地,为此我们将使用what3words.com作为我们的地理定位服务。
Let’s also imagine that they’re using mobile devices with bad connections and retries. Therefore, after a consignment is created, the rest of the information about it can arrive in any order. This complexity, along with needing two lists for each consignment – the items and the check-ins – is a good reason to use Hibernate.
我们还可以想象,他们使用的是连接不良和重试的移动设备。因此,在一个寄售物品被创建后,关于它的其他信息可以以任何顺序到达。这种复杂性,再加上每个托运物品需要两个列表–物品和签到–是使用Hibernate的一个很好的理由。
3.1. API Design
3.1.API设计
We’ll create a REST API with the following methods:
我们将用以下方法创建一个REST API。
- POST /consignment – create a new consignment, returning the ID, and supplying the source and destination; must be done before any other operations
- POST /consignment/{id}/item – add an item to the consignment; always adds to the end of the list
- POST /consignment/{id}/checkin – check a consignment in at any location along the way, supplying the location and a timestamp; will always be maintained in the database in order of timestamp
- GET /consignment/{id} – get the full history of a consignment, including whether it has reached its destination
3.2. Lambda Design
3.2.兰达设计
We’ll use a single Lambda function to provide this REST API with the Serverless Application Model to define it. This means our single Lambda handler function will need to be able to satisfy all of the above requests.
我们将使用单个Lambda函数来提供这个REST API,并使用无服务器应用模型来定义它。这意味着我们的单一Lambda处理函数将需要能够满足上述所有请求。
To make it quick and easy to test, without the overhead of deploying to AWS, we’ll test everything on our development machines.
为了使测试快速而简单,没有部署到AWS的开销,我们将在我们的开发机器上测试一切。
4. Creating the Lambda
4.创建Lambda
Let’s set up a fresh Lambda to satisfy our API, but without implementing its data access layer yet.
让我们建立一个新的Lambda来满足我们的API,但还没有实现其数据访问层。
4.1. Prerequisites
4.1.先决条件
First, we need to install Docker if we do not have it already. We’ll need it to host our test database, and it’s used by the AWS SAM CLI to simulate the Lambda runtime.
首先,我们需要安装Docker,如果我们还没有安装的话。我们需要它来托管我们的测试数据库,而且AWS SAM CLI也使用它来模拟Lambda运行时。
We can test whether we have Docker:
我们可以测试我们是否有Docker。
$ docker --version
Docker version 19.03.12, build 48a66213fe
Next, we need to install the AWS SAM CLI and then test it:
接下来,我们需要安装AWS SAM CLI,然后测试它。
$ sam --version
SAM CLI, version 1.1.0
Now we’re ready to create our Lambda.
现在我们准备创建我们的Lambda。
4.2. Creating the SAM Template
4.2.创建SAM模板
The SAM CLI provides us a way of creating a new Lambda function:
SAM CLI为我们提供了一种创建新Lambda函数的方法。
$ sam init
This will prompt us for the settings of the new project. Let’s choose the following options:
这将提示我们对新项目的设置。让我们选择以下选项。
1 - AWS Quick Start Templates
13 - Java 8
1 - maven
Project name - shipping-tracker
1 - Hello World Example: Maven
We should note that these option numbers may vary with later versions of the SAM tooling.
我们应该注意到,这些选项编号可能会随着SAM工具的后期版本而变化。
Now, there should be a new directory called shipping-tracker in which there’s a stub application. If we look at the contents of its template.yaml file, we’ll find a function called HelloWorldFunction with a simple REST API:
现在,应该有一个名为shipping-tracker的新目录,其中有一个存根应用程序。如果我们看一下它的template.yaml文件的内容,我们会发现一个叫做HelloWorldFunction的函数,有一个简单的REST API。
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
By default, this satisfies a basic GET request on /hello. We should quickly test that everything is working, by using sam to build and test it:
默认情况下,这满足了/hello上的一个基本GET请求。我们应该通过使用sam来构建和测试它,快速测试一切是否正常。
$ sam build
... lots of maven output
$ sam start-api
Then we can test the hello world API using curl:
然后我们可以使用hello world API测试curl。
$ curl localhost:3000/hello
{ "message": "hello world", "location": "192.168.1.1" }
After that, let’s stop sam running its API listener by using CTRL+C to abort the program.
之后,让我们通过使用CTRL+C中止程序来停止sam运行其API监听器。
Now that we have an empty Java 8 Lambda, we need to customize it to become our API.
现在我们有了一个空的Java 8 Lambda,我们需要对它进行定制,使之成为我们的API。
4.3. Creating Our API
4.3.创建我们的API
To create our API, we need to add our own paths to the Events section of the template.yaml file:
为了创建我们的API,我们需要在template.yaml文件的Events部分添加自己的路径。
CreateConsignment:
Type: Api
Properties:
Path: /consignment
Method: post
AddItem:
Type: Api
Properties:
Path: /consignment/{id}/item
Method: post
CheckIn:
Type: Api
Properties:
Path: /consignment/{id}/checkin
Method: post
ViewConsignment:
Type: Api
Properties:
Path: /consignment/{id}
Method: get
Let’s also rename the function we’re calling from HelloWorldFunction to ShippingFunction:
让我们也把我们要调用的函数从HelloWorldFunction改名为ShippingFunction。
Resources:
ShippingFunction:
Type: AWS::Serverless::Function
Next, we’ll rename the directory it’s to ShippingFunction and change the Java package from helloworld to com.baeldung.lambda.shipping. This means we’ll need to update the CodeUri and Handler properties in template.yaml to point to the new location:
接下来,我们将把它的目录重命名为ShippingFunction,并把Java包从helloworld改为com.baeldung.lambda.shiping。这意味着我们需要更新CodeUri和Handler中的属性,以指向新的位置。
Properties:
CodeUri: ShippingFunction
Handler: com.baeldung.lambda.shipping.App::handleRequest
Finally, to make space for our own implementation, let’s replace the body of the handler:
最后,为了给我们自己的实现腾出空间,让我们替换处理程序的主体。
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");
return new APIGatewayProxyResponseEvent()
.withHeaders(headers)
.withStatusCode(200)
.withBody(input.getResource());
}
Though unit tests are a good idea, for this example, we’ll also delete the provided unit tests by deleting the src/test directory.
虽然单元测试是个好主意,但在这个例子中,我们也将通过删除src/test目录来删除提供的单元测试。
4.4. Testing the Empty API
4.4.测试空的API
Now we’ve moved things around and created our API and a basic handler, let’s double-check everything still works:
现在我们已经把东西搬来搬去,并创建了我们的API和一个基本的处理程序,让我们仔细检查一下一切是否仍然有效。
$ sam build
... maven output
$ sam start-api
Let’s use curl to test the HTTP GET request:
让我们使用curl来测试HTTP GET请求。
$ curl localhost:3000/consignment/123
/consignment/{id}
We can also use curl -d to POST:
我们也可以使用curl -d来POST。
$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/
/consignment
As we can see, both requests end successfully. Our stub code outputs the resource – the path of the request – which we can use when we set up routing to our various service methods.
正如我们所看到的,两个请求都成功结束。我们的存根代码输出了resource–请求的路径–我们可以在设置路由到各种服务方法时使用它。
4.5. Creating the Endpoints Within the Lambda
4.5.在Lambda中创建端点
We’re using a single Lambda function to handle our four endpoints. We could’ve created a different handler class for each endpoint in the same codebase or written a separate application for each endpoint, but keeping related APIs together allows a single fleet of Lambdas to serve them with common code, which can be a better use of resources.
我们使用一个Lambda函数来处理我们的四个端点。我们可以在同一个代码库中为每个端点创建不同的处理类,或者为每个端点编写一个单独的应用程序,但是将相关的API放在一起,允许一个Lambdas舰队用共同的代码为它们服务,这样可以更好地利用资源。
However, we need to build the equivalent of a REST controller to dispatch each request to a suitable Java function. So, we’ll create a stub ShippingService class and route to it from the handler:
然而,我们需要建立一个相当于REST控制器的东西,将每个请求分配给一个合适的Java函数。因此,我们将创建一个存根ShippingService类,并从处理程序路由到它。
public class ShippingService {
public String createConsignment(Consignment consignment) {
return UUID.randomUUID().toString();
}
public void addItem(String consignmentId, Item item) {
}
public void checkIn(String consignmentId, Checkin checkin) {
}
public Consignment view(String consignmentId) {
return new Consignment();
}
}
We’ll also create empty classes for Consignment, Item, and Checkin. These will soon become our model.
我们还将为Consignment、Item,和Checkin创建空类。这些将很快成为我们的模型。
Now that we have a service, let’s use the resource to route to the appropriate service methods. We’ll add a switch statement to our handler to route requests to the service:
现在我们有了一个服务,让我们使用resource来路由到适当的服务方法。我们将在处理程序中添加一个switch语句,将请求路由到服务。
Object result = "OK";
ShippingService service = new ShippingService();
switch (input.getResource()) {
case "/consignment":
result = service.createConsignment(
fromJson(input.getBody(), Consignment.class));
break;
case "/consignment/{id}":
result = service.view(input.getPathParameters().get("id"));
break;
case "/consignment/{id}/item":
service.addItem(input.getPathParameters().get("id"),
fromJson(input.getBody(), Item.class));
break;
case "/consignment/{id}/checkin":
service.checkIn(input.getPathParameters().get("id"),
fromJson(input.getBody(), Checkin.class));
break;
}
return new APIGatewayProxyResponseEvent()
.withHeaders(headers)
.withStatusCode(200)
.withBody(toJson(result));
We can use Jackson to implement our fromJson and toJson functions.
我们可以使用Jackson来实现我们的fromJson和toJson函数。
4.6. A Stubbed Implementation
4.6.一个存根的实现
So far, we’ve learned how to create an AWS Lambda to support an API, test it using sam and curl, and build basic routing functionality within our handler. We could add more error handling on bad inputs.
到目前为止,我们已经学会了如何创建AWS Lambda来支持API,使用sam和curl进行测试,并在我们的处理程序中建立基本的路由功能。我们可以添加更多关于不良输入的错误处理。
We should note that the mappings within the template.yaml already expects the AWS API Gateway to filter requests that are not for the right paths in our API. So, we need less error handling for bad paths.
我们应该注意,template.yaml内的映射已经期望AWS API Gateway过滤那些不属于我们API中正确路径的请求。因此,我们需要减少对不良路径的错误处理。
Now, it’s time to implement our service with its database, entity model, and Hibernate.
现在,是时候用数据库、实体模型和Hibernate实现我们的服务了。
5. Setting up the Database
5.设置数据库
For this example, we’ll use PostgreSQL as the RDBMS. Any relational database could work.
在这个例子中,我们将使用PostgreSQL作为RDBMS。任何关系型数据库都可以工作。
5.1. Starting PostgreSQL in Docker
5.1 在Docker中启动PostgreSQL
First, we’ll pull a PostgreSQL docker image:
首先,我们要拉一个PostgreSQL docker镜像。
$ docker pull postgres:latest
... docker output
Status: Downloaded newer image for postgres:latest
docker.io/library/postgres:latest
Let’s now create a docker network for this database to run in. This network will allow our Lambda to communicate with the database container:
现在让我们创建一个docker网络,让这个数据库在其中运行。这个网络将允许我们的Lambda与数据库容器进行通信。
$ docker network create shipping
Next, we need to start the database container within that network:
接下来,我们需要在该网络中启动数据库容器。
docker run --name postgres \
--network shipping \
-e POSTGRES_PASSWORD=password \
-d postgres:latest
With –name, we’ve given the container the name postgres. With –network, we’ve added it to our shipping docker network. To set the password for the server, we used the environment variable POSTGRES_PASSWORD, set with the -e switch.
通过-name,我们给这个容器取名为postgres。通过-network,我们把它添加到我们的shipping docker网络。为了设置服务器的密码,我们使用环境变量POSTGRES_PASSWORD,用-e开关设置。
We also used -d to run the container in the background, rather than tie up our shell. PostgreSQL will start in a few seconds.
我们还用-d来在后台运行容器,而不是占用我们的shell。PostgreSQL将在几秒钟内启动。
5.2. Adding a Schema
5.2.添加一个模式
We’ll need a new schema for our tables, so let’s use the psql client inside our PostgreSQL container to add the shipping schema:
我们将需要一个新的表的模式,所以让我们使用PostgreSQL容器内的psql客户端来添加shipping模式。
$ docker exec -it postgres psql -U postgres
psql (12.4 (Debian 12.4-1.pgdg100+1))
Type "help" for help.
postgres=#
Within this shell, we create the schema:
在这个外壳中,我们创建了模式。
postgres=# create schema shipping;
CREATE SCHEMA
Then we use CTRL+D to exit the shell.
然后我们使用CTRL+D来退出shell。
We now have PostgreSQL running, ready for our Lambda to use it.
我们现在有PostgreSQL在运行,准备让我们的Lambda使用它。
6. Adding Our Entity Model and DAO
6.添加我们的实体模型和DAO
Now we have a database, let’s create our entity model and DAO. Although we’re only using a single connection, let’s use the Hikari connection pool to see how it could be configured for Lambdas that maybe need to run multiple connections against the database in a single invocation.
现在我们有了一个数据库,让我们来创建我们的实体模型和DAO。虽然我们只使用一个连接,但让我们使用Hikari连接池,看看如何为那些可能需要在一次调用中针对数据库运行多个连接的Lambdas进行配置。
6.1. Adding Hibernate to the Project
6.1.将Hibernate添加到项目中
We’ll add dependencies to our pom.xml for both Hibernate and the Hikari Connection Pool. We’ll also add the PostgreSQL JDBC driver:
我们将在pom.xml中添加Hibernate和Hikari Connection Pool的依赖项。我们还将添加PostgreSQL JDBC驱动程序。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.21.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-hikaricp</artifactId>
<version>5.4.21.Final</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.16</version>
</dependency>
6.2. Entity Model
6.2.实体模型
Let’s flesh out the entity objects. A Consignment has a list of items and check-ins, as well as its source, destination, and whether it has been delivered yet (that is, whether it has checked into its final destination):
让我们充实一下实体对象的内容。一个Consignment有一个物品和签收的列表,以及它的来源、目的地,以及它是否已经交付(也就是说,它是否已经签收到最终目的地)。
@Entity(name = "consignment")
@Table(name = "consignment")
public class Consignment {
private String id;
private String source;
private String destination;
private boolean isDelivered;
private List items = new ArrayList<>();
private List checkins = new ArrayList<>();
// getters and setters
}
We’ve annotated the class as an entity and with a table name. We’ll provide getters and setters, too. Let’s mark the getters with the column names:
我们已经把这个类注释为一个实体,并且有一个表名。我们也将提供getters和setters。让我们用列名来标记获取器。
@Id
@Column(name = "consignment_id")
public String getId() {
return id;
}
@Column(name = "source")
public String getSource() {
return source;
}
@Column(name = "destination")
public String getDestination() {
return destination;
}
@Column(name = "delivered", columnDefinition = "boolean")
public boolean isDelivered() {
return isDelivered;
}
For our lists, we’ll use the @ElementCollection annotation to make them ordered lists in separate tables with a foreign key relation to the consignment table:
对于我们的列表,我们将使用@ElementCollection注解,使它们成为独立表中的有序列表,与consignment表有外键关系。
@ElementCollection(fetch = EAGER)
@CollectionTable(name = "consignment_item", joinColumns = @JoinColumn(name = "consignment_id"))
@OrderColumn(name = "item_index")
public List getItems() {
return items;
}
@ElementCollection(fetch = EAGER)
@CollectionTable(name = "consignment_checkin", joinColumns = @JoinColumn(name = "consignment_id"))
@OrderColumn(name = "checkin_index")
public List getCheckins() {
return checkins;
}
Here’s where Hibernate starts to pay for itself, performing the job of managing collections quite easily.
这里是Hibernate开始付出代价的地方,它很容易完成管理集合的工作。
The Item entity is more straightforward:
Item实体更直接。
@Embeddable
public class Item {
private String location;
private String description;
private String timeStamp;
@Column(name = "location")
public String getLocation() {
return location;
}
@Column(name = "description")
public String getDescription() {
return description;
}
@Column(name = "timestamp")
public String getTimeStamp() {
return timeStamp;
}
// ... setters omitted
}
It’s marked as @Embeddable to enable it to be part of the list definition in the parent object.
它被标记为@Embeddable以使其成为父对象中列表定义的一部分。
Similarly, we’ll define Checkin:
同样地,我们将定义Checkin。
@Embeddable
public class Checkin {
private String timeStamp;
private String location;
@Column(name = "timestamp")
public String getTimeStamp() {
return timeStamp;
}
@Column(name = "location")
public String getLocation() {
return location;
}
// ... setters omitted
}
6.3. Creating a Shipping DAO
6.3.创建一个航运DAO
Our ShippingDao class will rely on being passed an open Hibernate Session. This will require the ShippingService to manage the session:
我们的ShippingDao类将依赖于被传递给一个开放的HibernateSession。这将需要ShippingService来管理会话。
public void save(Session session, Consignment consignment) {
Transaction transaction = session.beginTransaction();
session.save(consignment);
transaction.commit();
}
public Optional<Consignment> find(Session session, String id) {
return Optional.ofNullable(session.get(Consignment.class, id));
}
We’ll wire this into our ShippingService later on.
我们将把它连接到我们的ShippingService以后。
7. The Hibernate Lifecycle
7.Hibernate的生命周期
So far, our entity model and DAO are comparable to non-Lambda implementations. The next challenge is creating a Hibernate SessionFactory within the Lambda’s lifecycle.
到目前为止,我们的实体模型和DAO可以与非Lambda的实现相媲美。下一个挑战是在Lambda的生命周期内创建一个Hibernate SessionFactory。
7.1. Where Is the Database?
7.1.数据库在哪里?
If we’re going to access the database from our Lambda, then it needs to be configurable. Let’s put the JDBC URL and database credentials into environment variables within our template.yaml:
如果我们要从我们的Lambda访问数据库,那么它就需要可配置。让我们把JDBC URL和数据库凭证放到template.yaml的环境变量中。
Environment:
Variables:
DB_URL: jdbc:postgresql://postgres/postgres
DB_USER: postgres
DB_PASSWORD: password
These environment variables will get injected into the Java runtime. The postgres user is the default for our Docker PostgreSQL container. We assigned the password as password when we started the container earlier.
这些环境变量将被注入到Java运行时中。postgres用户是我们Docker PostgreSQL容器的默认用户。我们在之前启动容器时将密码分配为password。
Within the DB_URL, we have the server name – //postgres is the name we gave our container – and the database name postgres is the default database.
在DB_URL中,我们有服务器名称–//postgres是我们给容器的名称–而数据库名称postgres是默认数据库。
It’s worth noting that, though we’re hard-coding these values in this example, SAM templates allow us to declare inputs and parameter overrides. Therefore, they can be made parameterizable later on.
值得注意的是,尽管我们在这个例子中对这些值进行了硬编码,但SAM模板允许我们声明输入和参数覆盖。因此,它们可以在以后成为可参数化的。
7.2. Creating the Session Factory
7.2.创建会话工厂
We have both Hibernate and the Hikari connection pool to configure. To provide settings to Hibernate, we add them to a Map:
我们有Hibernate和Hikari>连接池需要配置。为了向Hibernate提供设置,我们将它们添加到一个Map。
Map<String, String> settings = new HashMap<>();
settings.put(URL, System.getenv("DB_URL"));
settings.put(DIALECT, "org.hibernate.dialect.PostgreSQLDialect");
settings.put(DEFAULT_SCHEMA, "shipping");
settings.put(DRIVER, "org.postgresql.Driver");
settings.put(USER, System.getenv("DB_USER"));
settings.put(PASS, System.getenv("DB_PASSWORD"));
settings.put("hibernate.hikari.connectionTimeout", "20000");
settings.put("hibernate.hikari.minimumIdle", "1");
settings.put("hibernate.hikari.maximumPoolSize", "2");
settings.put("hibernate.hikari.idleTimeout", "30000");
settings.put(HBM2DDL_AUTO, "create-only");
settings.put(HBM2DDL_DATABASE_ACTION, "create");
Here, we’re using System.getenv to pull runtime settings from the environment. We’ve added the HBM2DDL_ settings to make our application generate the database tables. However, we should comment out or remove these lines after the database schema is generated, and should avoid allowing our Lambda to do this in production. It’s helpful for our testing now, though.
在这里,我们使用System.getenv来从环境中提取运行时设置。我们添加了HBM2DDL_设置,使我们的应用程序生成数据库表。然而,我们应该在数据库模式生成后注释或删除这些行,并且应该避免让我们的Lambda在生产中这样做。不过,这对我们现在的测试很有帮助。
As we can see, many of the settings have constants already defined in the AvailableSettings class in Hibernate, though the Hikari-specific ones don’t.
我们可以看到,许多设置已经在Hibernate的AvailableSettings类中定义了常量,尽管Hikari特有的设置没有。
Now that we have the settings, we need to build the SessionFactory. We’ll individually add our entity classes to it:
现在我们有了设置,我们需要建立SessionFactory。我们将分别向它添加我们的实体类。
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySettings(settings)
.build();
return new MetadataSources(registry)
.addAnnotatedClass(Consignment.class)
.addAnnotatedClass(Item.class)
.addAnnotatedClass(Checkin.class)
.buildMetadata()
.buildSessionFactory();
7.3. Manage Resources
7.3.管理资源
On startup, Hibernate performs code generation around the entity objects. It is not intended for an application to perform this action more than once, and it uses time and memory to do it. So, we want to do this once on the cold-start of our Lambda.
在启动时,Hibernate会围绕实体对象执行代码生成。它并不打算让一个应用程序多次执行这个动作,而且这样做会消耗时间和内存。因此,我们想在Lambda的冷启动时做一次。
Therefore, we should create the SessionFactory as our handler object is created by the Lambda framework. We can do this in the initializer list of the handler class:
因此,我们应该在Lambda框架创建我们的处理程序对象时创建SessionFactory。我们可以在处理程序类的初始化器列表中这样做。
private SessionFactory sessionFactory = createSessionFactory();
However, as our SessionFactory has a connection pool, there’s a risk that it will hold connections open between invocations, tying up database resources.
然而,由于我们的SessionFactory有一个连接池,它有可能在调用之间保持连接开放,占用数据库资源。
Worse than that, there’s no lifecycle event that allows a Lambda to close down resources if it’s being disposed of by the AWS runtime. So, there’s a chance that a connection being held this way will never be properly released.
更糟糕的是,如果Lambda正在被AWS运行时处置,没有生命周期事件允许Lambda关闭资源。因此,以这种方式持有的连接有可能永远不会被正确释放。
We can solve that by digging into the SessionFactory for our connection pool and explicitly closing down any connections:
我们可以通过挖掘连接池的SessionFactory并明确关闭任何连接来解决这个问题。
private void flushConnectionPool() {
ConnectionProvider connectionProvider = sessionFactory.getSessionFactoryOptions()
.getServiceRegistry()
.getService(ConnectionProvider.class);
HikariDataSource hikariDataSource = connectionProvider.unwrap(HikariDataSource.class);
hikariDataSource.getHikariPoolMXBean().softEvictConnections();
}
This works in this case because we specified the Hikari connection pool, which provides softEvictConnections to allow us to release its connections.
这在这种情况下是可行的,因为我们指定了Hikari连接池,它提供了softEvictConnections以允许我们释放其连接。
We should note that SessionFactory‘s close method would also close the connections, but it would also render the SessionFactory unusable.
我们应该注意,SessionFactory的close方法也会关闭连接,但它也会使SessionFactory无法使用。
7.4. Add to the Handler
7.4.添加到处理程序中
Now, we need to ensure the handler uses the session factory and releases its connections. With that in mind, let’s extract most of the controller functionality into a method called routeRequest and modify our handler to release the resources in a finally block:
现在,我们需要确保处理程序使用会话工厂并释放其连接。考虑到这一点,让我们将控制器的大部分功能提取到一个名为routeRequest的方法中,并修改我们的处理程序,在finally块中释放资源。
try {
ShippingService service = new ShippingService(sessionFactory, new ShippingDao());
return routeRequest(input, service);
} finally {
flushConnectionPool();
}
We’ve also changed our ShippingService to have the SessionFactory and ShippingDao as properties, injected via the constructor, but it’s not using them yet.
我们也改变了我们的ShippingService,将SessionFactory和ShippingDao作为属性,通过构造函数注入,但它还没有使用它们。
7.5. Testing Hibernate
7.5.测试Hibernate
At this point, though the ShippingService does nothing, invoking the Lambda should cause Hibernate to start up and generate DDL.
此时,尽管ShippingService什么也没做,但调用Lambda应导致Hibernate启动并生成DDL。
Let’s double-check the DDL it generates before we comment out the settings for that:
在我们注释出相关设置之前,让我们仔细检查一下它生成的DDL。
$ sam build
$ sam local start-api --docker-network shipping
We build the application as before, but now we’re adding the –docker-network parameter to sam local. This runs the test Lambda within the same network as our database so that the Lambda can reach the database container by using its container name.
我们像以前一样构建应用程序,但现在我们将-docker-network参数添加到sam local。这将在与我们的数据库相同的网络中运行测试Lambda,这样Lambda就可以通过使用其容器名称到达数据库容器。
When we first hit the endpoint using curl, our tables should be created:
当我们第一次使用curl点击端点时,我们的表应该被创建。
$ curl localhost:3000/consignment/123
{"id":null,"source":null,"destination":null,"items":[],"checkins":[],"delivered":false}
The stub code still returned a blank Consignment. But, let’s now check the database to see if the tables were created:
存根代码仍然返回一个空白的Consignment。但是,现在让我们检查一下数据库,看看这些表是否被创建。
$ docker exec -it postgres pg_dump -s -U postgres
... DDL output
CREATE TABLE shipping.consignment_item (
consignment_id character varying(255) NOT NULL,
...
Once we’re happy our Hibernate setup is working, we can comment out the HBM2DDL_ settings.
一旦我们确信我们的Hibernate设置是有效的,我们就可以注释掉HBM2DDL_设置。
8. Complete the Business Logic
8.完成业务逻辑
All that remains is to make the ShippingService use the ShippingDao to implement the business logic. Each method will create a session factory in a try-with-resources block to ensure it gets closed.
剩下的就是让ShippingService使用ShippingDao来实现业务逻辑。每个方法都将在一个try-with-resources块中创建一个会话工厂,以确保它被关闭。
8.1. Create Consignment
8.1.创建托运
A new consignment hasn’t been delivered and should receive a new ID. Then we should save it in the database:
一个新的托运货物还没有交付,应该收到一个新的ID。然后我们应该把它保存在数据库中。
public String createConsignment(Consignment consignment) {
try (Session session = sessionFactory.openSession()) {
consignment.setDelivered(false);
consignment.setId(UUID.randomUUID().toString());
shippingDao.save(session, consignment);
return consignment.getId();
}
}
8.2. View Consignment
8.2.查看托运
To get a consignment, we need to read it from the database by ID. Though a REST API should return Not Found on an unknown request, for this example, we’ll just return an empty consignment if none is found:
为了获得一个托运货物,我们需要按ID从数据库中读取它。虽然REST API应该在一个未知的请求中返回Not Found,但在这个例子中,如果没有找到,我们将只返回一个空的托运。
public Consignment view(String consignmentId) {
try (Session session = sessionFactory.openSession()) {
return shippingDao.find(session, consignmentId)
.orElseGet(Consignment::new);
}
}
8.3. Add Item
8.3.添加项目
Items will go into our list of items in the order received:
物品将按照收到的顺序进入我们的物品清单。
public void addItem(String consignmentId, Item item) {
try (Session session = sessionFactory.openSession()) {
shippingDao.find(session, consignmentId)
.ifPresent(consignment -> addItem(session, consignment, item));
}
}
private void addItem(Session session, Consignment consignment, Item item) {
consignment.getItems()
.add(item);
shippingDao.save(session, consignment);
}
Ideally, we’d have better error handling if the consignment did not exist, but for this example, non-existent consignments will be ignored.
理想情况下,如果托运货物不存在,我们会有更好的错误处理,但对于这个例子,不存在的托运货物将被忽略。
8.4. Check-In
8.4.签到
The check-ins need to be sorted in order of when they happen, not when the request is received. Also, when the item reaches the final destination, it should be marked as delivered:
签收需要按照发生的时间排序,而不是按照收到请求的时间排序。另外,当物品到达最终目的地时,应该标记为已交付。
public void checkIn(String consignmentId, Checkin checkin) {
try (Session session = sessionFactory.openSession()) {
shippingDao.find(session, consignmentId)
.ifPresent(consignment -> checkIn(session, consignment, checkin));
}
}
private void checkIn(Session session, Consignment consignment, Checkin checkin) {
consignment.getCheckins().add(checkin);
consignment.getCheckins().sort(Comparator.comparing(Checkin::getTimeStamp));
if (checkin.getLocation().equals(consignment.getDestination())) {
consignment.setDelivered(true);
}
shippingDao.save(session, consignment);
}
9. Testing the App
9.测试应用程序
Let’s simulate a package traveling from The White House to the Empire State Building.
让我们模拟一个从白宫到帝国大厦的包裹。
An agent creates the journey:
一个代理人创造了这个旅程。
$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/
"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207"
We now have the ID 3dd0f0e4-fc4a-46b4-8dae-a57d47df5207 for the consignment. Then, someone collects two items for the consignment – a picture and a piano:
我们现在有寄售物品的ID3dd0f0e4-fc4a-46b4-8dae-a57d47df5207。然后,有人为托运的物品收集了两件物品–一张照片和一架钢琴。
$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120000", "description":"picture"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"
$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120001", "description":"piano"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"
Sometime later, there’s a check-in:
过了一会儿,有一个签到。
$ curl -d '{"location":"united.alarm.raves", "timeStamp":"20200101T173301"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"
And again later:
后来又是如此。
$ curl -d '{"location":"wink.sour.chasing", "timeStamp":"20200101T191202"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"
The customer, at this point, requests the status of the consignment:
这时,客户要求了解托运货物的情况。
$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
"id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
"source":"data.orange.brings",
"destination":"heave.wipes.clay",
"items":[
{"location":"data.orange.brings","description":"picture","timeStamp":"20200101T120000"},
{"location":"data.orange.brings","description":"piano","timeStamp":"20200101T120001"}
],
"checkins":[
{"timeStamp":"20200101T173301","location":"united.alarm.raves"},
{"timeStamp":"20200101T191202","location":"wink.sour.chasing"}
],
"delivered":false
}%
They see the progress, and it’s not yet delivered.
他们看到了进展,但还没有交付。
A message should have been sent at 20:12 to say it reached deflection.famed.apple, but it gets delayed, and the message from 21:46 at the destination gets there first:
一条信息应该在20:12发出,说它到达了deflection.famed.apple,但它被延迟了,而目的地21:46的信息先到了。
$ curl -d '{"location":"heave.wipes.clay", "timeStamp":"20200101T214622"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"
The customer, at this point, requests the status of the consignment:
这时,客户要求了解托运货物的情况。
$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
"id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
...
{"timeStamp":"20200101T191202","location":"wink.sour.chasing"},
{"timeStamp":"20200101T214622","location":"heave.wipes.clay"}
],
"delivered":true
}
Now it’s delivered. So, when the delayed message gets through:
现在,它被送达了。因此,当延迟的信息被送达时,。
$ curl -d '{"location":"deflection.famed.apple", "timeStamp":"20200101T201254"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"
$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
"id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
...
{"timeStamp":"20200101T191202","location":"wink.sour.chasing"},
{"timeStamp":"20200101T201254","location":"deflection.famed.apple"},
{"timeStamp":"20200101T214622","location":"heave.wipes.clay"}
],
"delivered":true
}
The check-in is put in the right place in the timeline.
签到被放在时间轴上的正确位置。
10. Conclusion
10.结语
In this article, we discussed the challenges of using a heavyweight framework like Hibernate in a lightweight container such as AWS Lambda.
在这篇文章中,我们讨论了在AWS Lambda这样的轻量级容器中使用Hibernate这样一个重量级框架的挑战。
We built a Lambda and REST API and learned how to test it on our local machine using Docker and AWS SAM CLI. Then, we constructed an entity model for Hibernate to use with our database. We also used Hibernate to initialize our tables.
我们建立了一个Lambda和REST API,并学习了如何使用Docker和AWS SAM CLI在我们的本地机器上进行测试。然后,我们为Hibernate构建了一个实体模型,以便与我们的数据库一起使用。我们还使用Hibernate来初始化我们的表。
Finally, we integrated the Hibernate SessionFactory into our application, ensuring to close it before the Lambda exited.
最后,我们将Hibernate SessionFactory集成到我们的应用程序中,确保在Lambda退出前关闭它。
As usual, the example code for this article can be found over on GitHub.
像往常一样,本文的示例代码可以在GitHub上找到over。