1. 概述

AWS Lambda 让我们可以构建轻量级应用,部署和扩缩容都非常方便。虽然可以使用 Spring Cloud Function 这类框架,但出于性能考虑,我们通常会尽量减少框架的使用。

有时候我们需要从 Lambda 中访问关系型数据库,这时 HibernateJPA 就派上用场了。但问题是:不依赖 Spring 的情况下,如何在 Lambda 中集成 Hibernate?

本文将探讨在 Lambda 中使用 RDBMS 的挑战,以及 Hibernate 在什么场景下有用。我们将使用 Serverless Application Model(SAM)构建一个 REST 接口来访问数据。

同时,我们还会介绍如何使用 Docker 和 AWS SAM CLI 在本地测试整个流程。


2. 在 Lambda 中使用 RDBMS 与 Hibernate 的挑战

Lambda 的代码包越小越好,这有助于减少冷启动时间。同时,Lambda 应该在毫秒级完成任务。然而,关系型数据库的访问通常涉及大量框架代码,性能开销较大。

在云原生架构中,我们更倾向于使用 DynamoDB 这类 Serverless 数据库,它们与 Lambda 更匹配。但项目中可能因历史原因或业务需求,必须使用关系型数据库。

2.1. 从 Lambda 访问 RDBMS

Lambda 执行完后,其容器会被冻结。这个容器可能被复用于下一次调用,也可能被 AWS 运行时销毁。因此,我们必须在单次调用的生命周期内谨慎管理所有资源

特别注意:不能依赖传统的数据库连接池。因为连接可能在容器冻结时仍未关闭,导致连接泄露。虽然可以在一次调用中使用连接池,但每次调用都得重新创建连接池,并且必须在函数结束时显式关闭所有连接并释放资源

这意味着,如果 Lambda 突然大规模并发,可能会耗尽数据库连接。即使 Lambda 立即释放连接,数据库也需要时间回收并准备下一次使用。因此,强烈建议对使用 RDBMS 的 Lambda 设置最大并发限制

⚠️ 在某些项目中,Lambda 并不是连接 RDBMS 的最佳选择。使用传统 Spring Data 服务(带连接池)部署在 EC2 或 ECS 上,可能是更稳妥的方案。

2.2. 为什么选择 Hibernate

判断是否需要 Hibernate,关键看不用它会多写多少代码。

  • ✅ 如果不用 Hibernate,你需要手写大量字段映射或复杂 JOIN 查询,那它就是个好选择。
  • ✅ 如果你的应用负载不高,对延迟不敏感,Hibernate 的开销可以接受。

2.3. Hibernate 是重量级技术

但也要权衡成本:

  • ❌ Hibernate 的 JAR 包有 7MB,启动时需要解析注解、构建 ORM 映射,耗时较长。
  • ❌ 对于通常只做简单任务的 Lambda 来说,这可能“杀鸡用牛刀”。

📌 替代方案:

  • 直接使用 JDBC
  • 轻量级 ORM 框架如 JDBI,既能提供查询抽象,又不会带来太多开销。

3. 示例应用

我们将构建一个低频物流追踪系统。客户寄送大件物品形成一个“托运单(Consignment)”。托运单在运输途中会不断“打卡”,记录时间和位置,客户可实时查看。

每个托运单有“起点”和“终点”,我们使用 what3words.com 作为地理定位服务。

假设移动设备网络不稳定,数据可能乱序到达。此外,每个托运单包含“物品列表”和“打卡记录”两个集合,这种复杂性正是使用 Hibernate 的理由。

3.1. 接口设计

REST API 设计如下:

  • POST /consignment
    创建托运单,返回 ID,需提供 sourcedestination。必须先创建才能操作。

  • POST /consignment/{id}/item
    为托运单添加物品,按接收顺序追加。

  • POST /consignment/{id}/checkin
    托运单在某地打卡,提供 location 和时间戳。数据库按时间戳排序存储。

  • GET /consignment/{id}
    获取托运单完整信息,包括是否已送达。

3.2. Lambda 架构

使用单个 Lambda 函数提供上述 REST 接口,通过 Serverless Application Model 定义。这意味着一个 handler 要处理所有请求。

为便于本地测试,我们使用 AWS SAM CLI 模拟运行,避免频繁部署到云端。


4. 创建 Lambda

先搭建一个空的 Lambda 框架,暂不实现数据访问层。

4.1. 前置条件

  1. 安装 Docker(用于运行测试数据库)
  2. 安装 AWS SAM CLI

验证安装:

$ docker --version
Docker version 19.03.12, build 48a66213fe

$ sam --version
SAM CLI, version 1.1.0

4.2. 创建 SAM 模板

使用 SAM CLI 初始化项目:

$ sam init

选择以下选项:

1 - AWS Quick Start Templates
13 - Java 8
1 - maven
Project name - shipping-tracker
1 - Hello World Example: Maven

⚠️ 选项编号可能因 SAM 版本而异。

进入 shipping-tracker 目录,检查 template.yaml,默认有一个 /hello 的 GET 接口。

构建并测试:

$ sam build
$ sam start-api

访问测试:

$ curl localhost:3000/hello
{ "message": "hello world", "location": "192.168.1.1" }

测试成功后按 CTRL+C 停止。

4.3. 定制我们的 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

重命名函数为 ShippingFunction

Resources:
  ShippingFunction:
    Type: AWS::Serverless::Function 

同步修改目录名、Java 包名(com.baeldung.lambda.shipping),并更新 template.yaml 中的 CodeUriHandler

Properties:
  CodeUri: ShippingFunction
  Handler: com.baeldung.lambda.shipping.App::handleRequest

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

删除 src/test 目录(示例中不保留测试)。

4.4. 测试空接口

重新构建并启动:

$ sam build
$ sam start-api

测试 GET:

$ curl localhost:3000/consignment/123
/consignment/{id}

测试 POST:

$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/
/consignment

返回值是请求路径,可用于后续路由。

4.5. 在 Lambda 中实现接口分发

我们使用单个 Lambda 处理四个接口。虽然可以拆分成多个 handler,但集中管理更利于复用代码。

需要在 handler 中实现类似 REST Controller 的路由逻辑。先创建 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();
    }
}

创建 ConsignmentItemCheckin 空类(后续作为实体模型)。

在 handler 中通过 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));

fromJsontoJson 可用 Jackson 实现。

4.6. 存根实现小结

目前我们完成了:

  • 创建 Lambda 支持 REST API
  • 本地测试(sam + curl
  • 实现基本路由

📌 注意:template.yaml 中的路径映射已由 API Gateway 过滤,因此 handler 中无需处理无效路径。

下一步:实现数据库、实体模型和 Hibernate 集成。


5. 搭建数据库

使用 PostgreSQL 作为 RDBMS(其他关系型数据库同理)。

5.1. 在 Docker 中启动 PostgreSQL

拉取镜像:

$ docker pull postgres:latest

创建专用网络(供 Lambda 容器通信):

$ docker network create shipping

启动数据库容器:

docker run --name postgres \
  --network shipping \
  -e POSTGRES_PASSWORD=password \
  -d postgres:latest
  • --name postgres:容器名
  • --network shipping:加入 shipping 网络
  • -e POSTGRES_PASSWORD=password:设置密码
  • -d:后台运行

5.2. 创建 Schema

进入容器创建 shipping schema:

$ docker exec -it postgres psql -U postgres

执行 SQL:

postgres=# create schema shipping;
CREATE SCHEMA

退出(CTRL+D)。数据库准备就绪。


6. 添加实体模型与 DAO

现在创建实体模型和 DAO。虽然每次调用只用一个连接,但我们会集成 HikariCP,展示如何为需要多连接的场景配置。

6.1. 添加 Hibernate 依赖

pom.xml 中添加:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.4.2.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-hikaricp</artifactId>
    <version>6.4.2.Final</version>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.16</version>
</dependency>

6.2. 实体模型

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
}

添加字段映射:

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

使用 @ElementCollection 管理集合(自动建关联表):

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

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
}

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. 创建 Shipping DAO

DAO 依赖外部传入的 Hibernate Session(由 Service 管理):

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

后续在 ShippingService 中注入。


7. Hibernate 生命周期管理

实体和 DAO 与普通项目类似。关键挑战是:**如何在 Lambda 生命周期中管理 SessionFactory**。

7.1. 数据库连接配置

通过环境变量配置数据库连接(template.yaml):

Environment: 
  Variables:
    DB_URL: jdbc:postgresql://postgres/postgres
    DB_USER: postgres
    DB_PASSWORD: password
  • postgres:容器名(Docker 网络内可解析)
  • postgres:默认数据库名
  • password:启动容器时设置的密码

📌 生产环境应通过参数化配置,避免硬编码。

7.2. 创建 SessionFactory

配置 Hibernate 设置:

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

构建 SessionFactory

StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
  .applySettings(settings)
  .build();

return new MetadataSources(registry)
  .addAnnotatedClass(Consignment.class)
  .addAnnotatedClass(Item.class)
  .addAnnotatedClass(Checkin.class)
  .buildMetadata()
  .buildSessionFactory();

⚠️ HBM2DDL_* 仅用于测试,生产环境必须注释掉!

7.3. 资源释放

SessionFactory 在冷启动时创建(避免重复初始化):

private SessionFactory sessionFactory = createSessionFactory();

但 Hikari 连接池可能持有连接不释放。由于 Lambda 没有“销毁”生命周期钩子,必须手动清理:

private void flushConnectionPool() {
    ConnectionProvider connectionProvider = sessionFactory.getSessionFactoryOptions()
      .getServiceRegistry()
      .getService(ConnectionProvider.class);
    HikariDataSource hikariDataSource = connectionProvider.unwrap(HikariDataSource.class);
    hikariDataSource.getHikariPoolMXBean().softEvictConnections();
}

softEvictConnections() 释放连接但保留池对象,适合复用场景。

7.4. 集成到 Handler

确保每次调用后释放连接:

try {
    ShippingService service = new ShippingService(sessionFactory, new ShippingDao());
    return routeRequest(input, service);
} finally {
    flushConnectionPool();
}

ShippingService 通过构造函数注入 SessionFactoryShippingDao

7.5. 测试 Hibernate 配置

构建并启动(指定 Docker 网络):

$ sam build
$ sam local start-api --docker-network shipping

访问接口触发 DDL 生成:

$ curl localhost:3000/consignment/123
{"id":null,"source":null,"destination":null,"items":[],"checkins":[],"delivered":false}

检查数据库表是否创建:

$ docker exec -it postgres pg_dump -s -U postgres
CREATE TABLE shipping.consignment_item (
    consignment_id character varying(255) NOT NULL,
...

确认后注释掉 HBM2DDL_* 配置。


8. 完成业务逻辑

ShippingService 使用 ShippingDao 实现业务逻辑。每个方法用 try-with-resources 确保 Session 关闭。

8.1. 创建托运单

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. 查看托运单

public Consignment view(String consignmentId) {
    try (Session session = sessionFactory.openSession()) {
        return shippingDao.find(session, consignmentId)
          .orElseGet(Consignment::new);
    }
}

📌 生产环境应返回 404,此处简化处理。

8.3. 添加物品

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

⚠️ 不存在的托运单被忽略(简化处理)。

8.4. 打卡

按时间戳排序,并在到达终点时标记为已送达:

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. 应用测试

模拟一个包裹从白宫到帝国大厦的旅程。

创建托运单:

$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/
"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207"

添加物品:

$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120000", "description":"picture"}' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"

$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120001", "description":"piano"}' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"

中途打卡:

$ curl -d '{"location":"united.alarm.raves", "timeStamp":"20200101T173301"}' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

再次打卡:

$ curl -d '{"location":"wink.sour.chasing", "timeStamp":"20200101T191202"}' \
  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",
  "source":"data.orange.brings",
  "destination":"heave.wipes.clay",
  "items":[...],
  "checkins":[...],
  "delivered":false
}

到达终点(标记为已送达):

$ curl -d '{"location":"heave.wipes.clay", "timeStamp":"20200101T214622"}' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

后续延迟的消息(中间站点)到达:

$ curl -d '{"location":"deflection.famed.apple", "timeStamp":"20200101T201254"}' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

最终查询,打卡记录按时间戳正确排序,且 delivered=true


10. 总结

本文探讨了在轻量级 Lambda 中使用重量级框架 Hibernate 的挑战与解决方案。

我们完成了:

  • 使用 SAM 构建 RESTful Lambda
  • 本地 Docker + SAM CLI 测试
  • 设计 Hibernate 实体模型
  • 正确管理 SessionFactory 生命周期(冷启动创建,调用后释放连接)

关键踩坑点:

  • 必须手动释放连接池softEvictConnections
  • 避免生产环境自动生成 DDL
  • 控制 Lambda 并发,防止数据库连接耗尽

完整代码示例可在 GitHub 获取:aws-lambda-modules


原始标题:How to Implement Hibernate in an AWS Lambda Function in Java