跳到正文
DEV Community· Vishnu B·· 3 小時前AI 評分52

AWS Lambda 與 Spring Boot 快速實作指南

AWS Lambda Basics: A Quick Walkthrough with Spring Boot

AI 導讀

教程示範以 Spring Boot 和 Spring Cloud Function 建立 AWS Lambda 函數,從本地 Maven 打包及控制台測試,到透過 Function URL 提供 HTTP 呼叫。內容説明 handler、事件、執行角色與主要限制,並提醒 NONE 會令端點公開,測試後應刪除 URL、函數及 CloudWatch log group。

正文

Introduction

If you have ever deployed an application the traditional way - provision an EC2 instance, install a runtime, keep it patched, watch it sit idle most of the day - AWS Lambda feels almost unfair. Lambda is AWS's serverless compute service: you upload your code, and AWS runs it for you in response to events. No servers to provision, no operating system to patch, and no paying for idle time. You are charged per request and for the exact duration your code runs, down to the millisecond.

This post is for Java and Spring Boot developers. We go through the basic concepts of Lambda - handlers, events, triggers, and limits - and then do two quick hands-on implementations with Spring Boot and Spring Cloud Function: a function you build locally with Maven, deploy through the AWS console and test, and the same idea exposed as an HTTP endpoint you can call with curl. We finish with how to clean everything up, because even small experiments can cost money if you leave them running or expose them publicly.

Everything below is verified against the official AWS and Spring documentation as of October 2026, and every code listing was compiled and executed locally while writing. Both ecosystems update regularly, so check the linked docs pages if something looks different for you.

What exactly is AWS Lambda?

Lambda is a Function-as-a-Service (FaaS). The mental model is simple:

  • You write a function - a small piece of code with a defined entry point.
  • You configure a trigger - what should invoke the function. This can be a manual test, an HTTP request, a file landing in S3, a message in SQS, a schedule, and many more.
  • Lambda runs your function when the trigger fires, scaling out to as many parallel copies as needed - up to your account's concurrency quota (1,000 concurrent executions per region by default, which you can raise or lower). Environments are kept warm and reused between invocations.

Because there is no always-on server, you pay nothing while nothing runs. The free tier includes 1 million requests and 400,000 GB-seconds of compute per month, and the experiments in this post are tiny next to that. Two honest caveats: the free tier is shared across everything in your account, and services around Lambda (like CloudWatch log storage) can bill separately. A billing alarm in AWS Budgets can help you spot spend, but alerts can be delayed and they do not stop charges - AWS has no hard spend cap. Deleting what you created when you are done (cleanup below) stops future charges from those resources - it does not erase anything already accrued, and anything else in the account bills as usual.

Core concepts in five minutes

The handler. The handler is the entry point - the method Lambda calls when your function is invoked. For plain Java it is a method reference like example.Handler::handleRequest. In this post we use Spring Cloud Function, so we never write a handler class ourselves: we set the handler to Spring's built-in org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest, and it routes each invocation to a @Bean of type Function<I, O>. Whatever you choose must match the handler setting on the function, or Lambda will not find your code.

The event object. The first argument to your handler is the event - a JSON document with the input for this invocation. Its shape depends on the trigger: an S3 trigger delivers bucket and object details, an HTTP trigger delivers the request method, headers and body, and a console test event is whatever JSON you type in. Spring Cloud Function converts that JSON into your function's declared input type automatically - a small record you define, or an AWS event type such as APIGatewayV2HTTPEvent.

The context object. The second argument is provided by Lambda itself and describes the invocation: the request ID, the CloudWatch log group name, and how much time remains before the timeout. Useful for logging and monitoring.

The execution role. Your function runs with an IAM role that decides what AWS resources it is allowed to touch. When you create a function in the console, Lambda creates a basic role that only permits writing logs to CloudWatch. If your function needs to read from S3 or write to DynamoDB, you add those permissions to this role - following least privilege, grant only what the function actually needs.

Cold starts. The first time a function is invoked (or after it has been idle), Lambda provisions a fresh execution environment - this adds a delay called a cold start, visible as Init Duration in the logs. Later invocations reuse the warm environment and skip it. For Spring Boot functions the JVM plus Spring context startup makes cold starts more noticeable than for a small Node.js or Python script. AWS Lambda SnapStart (available for the Java runtimes) and giving the function more memory both help - in Walkthrough 1 we raise the memory and timeout from the console defaults so the first cold start fits comfortably.

What happens when Lambda is triggered - a fresh environment pays the cold start (runtime and Spring init, billed since Aug 2025); when a warm environment is reused, that init is not repeated, though later invocations are not guaranteed to be warm

Runtimes. Lambda offers managed runtimes for Java, Node.js, Python, .NET and Ruby, plus custom runtimes. For Java, prefer java21 or java25 - both are Amazon Linux 2023 based, while the older Amazon Linux 2 based runtimes are on a deprecation path. One practical difference from the scripted languages: the console's inline code editor does not work for Java, so the workflow is always build locally, upload the JAR.

Limits worth memorising

  • 15 minutes maximum per invocation - Lambda is for short tasks, not long-running jobs.
  • 128 MB to 10,240 MB of memory per function. CPU power scales proportionally with memory, so more memory can actually make a function finish faster (and sometimes cheaper).
  • 512 MB to 10,240 MB of temporary storage in /tmp, wiped between environments - never rely on it for persistence.
  • 6 MB payload for synchronous invocations, 1 MB for asynchronous ones.
  • 50 MB zipped deployment package on direct upload - larger zips go through S3, but the 250 MB unzipped limit applies either way. The shaded Spring Boot JAR we build below is about 38 MB, so it still fits a direct upload.

Walkthrough 1 - your first Lambda function with Spring Boot

You need a JDK 21 and Maven installed locally, plus an AWS account. The function receives a length and a width, multiplies them, and returns the area - deliberately tiny, so the moving parts stay visible.

The Java on Lambda workflow - write the code, build the shaded JAR, upload it, configure, invoke

Step 1 - Create the Maven project. Make a folder lambda-area with this pom.xml, and put the Java files under src/main/java/example:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>example</groupId>
    <artifactId>lambda-area</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.2</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>21</java.version>
        <spring-cloud-function.version>5.0.5</spring-cloud-function.version>
        <aws-lambda-events.version>3.16.1</aws-lambda-events.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-aws</artifactId>
        </dependency>
        <dependency>
            <groupId>tools.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>${aws-lambda-events.version}</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.4.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-function-dependencies</artifactId>
                <version>${spring-cloud-function.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot.experimental</groupId>
                        <artifactId>spring-boot-thin-layout</artifactId>
                        <version>1.0.31.RELEASE</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                    <shadedArtifactAttached>true</shadedArtifactAttached>
                    <shadedClassifierName>aws</shadedClassifierName>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

What each piece does: spring-cloud-function-adapter-aws is the bridge between a Spring Boot app and Lambda. aws-lambda-java-events gives us typed AWS events (used in Walkthrough 2). aws-lambda-java-core is marked provided because the Lambda runtime already has it. jackson-databind here is Jackson 3 (the tools.jackson group) - the JSON library Spring Boot 4 uses; we need it directly in Walkthrough 2. The Spring Boot plugin with the thin layout plus the shade plugin builds a single deployable JAR with the aws classifier - the same build setup as the official Spring Cloud Function AWS sample. All versions were current on Maven Central when this was written.

Step 2 - The application and the function bean. src/main/java/example/AreaApplication.java:

package example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.util.function.Function;

@SpringBootApplication
public class AreaApplication {

    public static void main(String[] args) {
        SpringApplication.run(AreaApplication.class, args);
    }

    @Bean
    public Function<AreaInput, AreaResult> area() {
        return input -> new AreaResult(input.length() * input.width());
    }
}

And the two records, AreaInput.java and AreaResult.java:

package example;

public record AreaInput(double length, double width) {}
package example;

public record AreaResult(double area) {}

This is an ordinary Spring Boot application. The Lambda-specific idea is the bean: a Function<AreaInput, AreaResult> named area. Spring Cloud Function collects every Function/Consumer/Supplier bean into a function catalogue, converts the incoming event JSON into AreaInput, and serialises the returned AreaResult back to JSON.

Step 3 - Build the deployment package.

mvn clean package

This produces target/lambda-area-0.0.1-SNAPSHOT-aws.jar - the one with the -aws classifier is the shaded, deployable JAR.

Step 4 - Create the function. Open the Lambda console, go to the Functions page, and choose Create function. Select Author from scratch, enter myLambdaFunction as the name, pick Java 21 as the runtime, leave the architecture at x86_64, and create. Lambda also creates the basic execution role automatically.

Step 5 - Upload the JAR and set the handler. In the Code tab, choose Upload from, then .zip or .jar file, and upload target/lambda-area-0.0.1-SNAPSHOT-aws.jar. Then open Runtime settings, choose Edit, set the handler to org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest, and save. Since the project currently has exactly one function bean, the invoker routes to it automatically.

Step 6 - Set memory and timeout for the demo. A new console function starts with Lambda's defaults: 128 MB of memory and a 3-second timeout. A Spring Boot cold start (JVM start plus Spring context initialisation) can exceed 3 seconds at 128 MB, so the first test could time out before your code ever runs. On the function's Configuration tab, open General configuration, choose Edit, set Memory to 512 MB (the same value Spring's own Lambda sample uses) and Timeout to 30 seconds, and save. Treat these as demo settings, not production sizing: CPU scales with memory, and the bill is configured memory x billed duration, so once things run, right-size from the REPORT lines or with AWS Lambda Power Tuning.

Step 7 - Test it. In the Test section, create a test event named myTestEvent with this JSON:

{
  "length": 6,
  "width": 7
}

Click Test. You should see the response {"area":42.0} along with the function logs. Every invocation ends with a REPORT line showing Duration, Billed Duration, Memory Size, Max Memory Used, and on cold starts Init Duration. The bill itself is computed from the billed duration and the memory you configured - Max Memory Used is not billed, but it tells you whether you can right-size the function down to a cheaper memory setting. When you invoke the function from outside the console, output goes to CloudWatch Logs under the log group /aws/lambda/myLambdaFunction.

Walkthrough 2 - expose it over HTTP with a Function URL

A test button is nice, but usually you want to call your function from somewhere else. The simplest way is a function URL - a dedicated HTTPS endpoint for one function, no API Gateway required. (For a full API with routes, stages and API keys, API Gateway is the right tool; for a webhook or a quick endpoint, a function URL is enough.)

Step 1 - Add an HTTP function bean. A function URL wraps the incoming HTTP request into an event with the same shape as an API Gateway HTTP API (v2) event, so we write a function from APIGatewayV2HTTPEvent to APIGatewayV2HTTPResponse. The JSON body arrives as a string in event.getBody(), so we parse it, validate the input, and return a proper 400 instead of an unhandled 500 on bad input. Add this to AreaApplication:

    private static final ObjectMapper mapper = new ObjectMapper();

    @Bean
    public Function<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> areaHttp() {
        return event -> {
            if (Boolean.TRUE.equals(event.getIsBase64Encoded())) {
                return error("Send JSON as plain text");
            }
            Map<String, Object> body;
            try {
                body = mapper.readValue(
                        event.getBody() == null ? "{}" : event.getBody(),
                        new TypeReference<Map<String, Object>>() {});
            } catch (MismatchedInputException e) {
                return error("Body must be a JSON object");
            } catch (Exception e) {
                return error("Body must be valid JSON");
            }
            if (body == null) {
                return error("Body must be a JSON object");
            }
            Object length = body.get("length");
            Object width = body.get("width");
            if (!(length instanceof Number) || !(width instanceof Number)) {
                return error("Provide non-negative numeric length and width");
            }
            double l = ((Number) length).doubleValue();
            double w = ((Number) width).doubleValue();
            if (!Double.isFinite(l) || !Double.isFinite(w) || l < 0 || w < 0) {
                return error("Provide non-negative numeric length and width");
            }
            double area = l * w;
            if (!Double.isFinite(area)) {
                return error("Dimensions too large");
            }
            return APIGatewayV2HTTPResponse.builder()
                    .withStatusCode(200)
                    .withHeaders(Map.of("Content-Type", "application/json"))
                    .withBody("{\"area\":" + area + "}")
                    .build();
        };
    }

    private static APIGatewayV2HTTPResponse error(String message) {
        return APIGatewayV2HTTPResponse.builder()
                .withStatusCode(400)
                .withHeaders(Map.of("Content-Type", "application/json"))
                .withBody("{\"error\":\"" + message + "\"}")
                .build();
    }

with these imports at the top:

import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.exc.MismatchedInputException;

import java.util.Map;

Step 2 - Rebuild, re-upload, and route. Run mvn clean package again and upload the new -aws.jar the same way as before. The project now has two function beans, so tell the invoker which one this Lambda should use: on the function's Configuration tab, open Environment variables and add spring_cloud_function_definition = areaHttp. Lambda environment variable names cannot contain dots, so the usual Spring property spring.cloud.function.definition is written with underscores - Spring maps it back.

Step 3 - Create the URL. On the Configuration tab, choose Function URL, then Create function URL. For the auth type, choose NONE for this experiment, save, and Lambda gives you an endpoint like https://<url-id>.lambda-url.<region>.on.aws/.

A word of caution on NONE: it makes the endpoint public - anyone who has the URL can invoke your function, and every invocation runs on your account. Treat it as a temporary experiment: delete it right after testing (cleanup steps below), and for anything real use the AWS_IAM auth type (callers must present valid AWS credentials) or put proper authentication behind it. If you call the URL from browser JavaScript, you will also need to configure CORS on the function URL.

Step 4 - Call it. From any terminal:

curl -X POST 'https://<url-id>.lambda-url.<region>.on.aws/' -H 'Content-Type: application/json' -d '{"length": 6, "width": 7}'

# {"area":42.0}

That is a working serverless HTTP endpoint written as a Spring Boot function: no server, no framework to manage, no load balancer - and it scales on its own. If you want a safety brake while experimenting, set reserved concurrency on the function (Configuration, then Concurrency) to a small number like 5 - it hard-limits how many copies can run at once. It caps concurrency, not rupees: there is no setting that stops all spending, so treat it as a brake, not a wall.

Cleaning up afterwards

A few clicks keeps the account tidy and stops new charges from the resources you created:

  • Delete the function URL (Configuration, Function URL, Delete). If the auth type was NONE, also check the resource-based policy on the Permissions tab - the docs note that deleting the URL does not remove that policy automatically.
  • Delete the function itself if you are done with it.
  • Delete the CloudWatch log group /aws/lambda/myLambdaFunction if you do not need the logs - stored logs are one of the small charges that can outlive the experiment.

When Lambda fits - and when it doesn't

Lambda shines for:

  • Event-driven work - react to an S3 upload, an SQS message, a DynamoDB stream update.
  • Scheduled jobs - cron-style tasks with EventBridge, without keeping a server alive for them.
  • Webhooks and lightweight APIs - function URLs or API Gateway in front.
  • Glue between services - small pieces of logic that connect AWS services together.

It is the wrong tool for jobs longer than 15 minutes, workloads that need state kept in memory between invocations, or sustained high-throughput services where containers or EC2 can be more economical.

One last pointer: Spring Cloud Function (what we used here) fits small, single-purpose functions. If you instead want to take an existing Spring Boot REST API with its @RestController classes and run it on Lambda as-is, look at the AWS Serverless Java Container project - Spring's own documentation presents both options side by side. Either way, measure the cold start before putting latency-sensitive traffic on it.

Summary

  • Lambda runs your code in response to events - no servers to manage, billed per request plus milliseconds of runtime.
  • The handler is the entry point, the event is the input JSON, the context carries invocation metadata, and the execution role controls AWS permissions.
  • With Spring Cloud Function, your Lambda code is a plain @Bean of type Function<I, O>; the handler is always FunctionInvoker::handleRequest, and spring_cloud_function_definition picks the bean when there are several.
  • Key limits: 15 minutes per run, up to 10 GB memory, 6 MB sync payload, 50 MB direct-upload JAR.
  • Java means build locally and upload the shaded -aws.jar - the console editor only works for scripted languages.
  • A function URL turns the function into a public HTTPS endpoint - use auth type NONE only briefly, and delete it when done.
  • Clean up when you finish: delete the function URL, the function, and the CloudWatch log group.

Happy coding, and stay tuned for the next post!


This article was drafted with the help of AI and reviewed by me before publishing.

Sources

來源:DEV Community · dev.to