-
Notifications
You must be signed in to change notification settings - Fork 559
Quick start Spring Boot
⚠️ This page deals with Spring Boot 1.x, this is no longer supported starting with version 1.6: Please use Spring Boot 3.x instead and follow the related documentation.
You can use the aws-serverless-java-container
library to run a Spring Boot application in AWS Lambda. You can use the library within your Lambda handler to load your Spring Boot application and proxy events to it.
In the repository we have included a sample Spring Boot application to get you started.
You can quickly create a new serverless Spring Boot application using our Maven archetype. First, make sure Maven is installed in your environment and available in your PATH
. Next, using a terminal or your favorite IDE create a new application, the archetype groupId
is com.amazonaws.serverless.archetypes
and the artifactId
is aws-serverless-springboot-archetype
:
mvn archetype:generate -DgroupId=my.service -DartifactId=my-service -Dversion=1.0-SNAPSHOT \
-DarchetypeGroupId=com.amazonaws.serverless.archetypes \
-DarchetypeArtifactId=aws-serverless-springboot-archetype \
-DarchetypeVersion=1.5.2
The archetype sets up a new project that includes a pom.xml
file as well as a build.gradle
file. The generated code includes a StreamLambdaHandler
class, the main entry point for AWS Lambda; a resource
package with a /ping
resource; and a set of unit tests that exercise the application.
The project also includes a file called template.yml
. This is a SAM template that you can use to quickly test your application in local or deploy it to AWS. Open the README.md
file in the project root folder for instructions on how to use the SAM CLI to run your Serverless API or deploy it to AWS.
The first step is to import the Spring implementation of the library:
<dependency>
<groupId>com.amazonaws.serverless</groupId>
<artifactId>aws-serverless-java-container-spring</artifactId>
<version>1.5.2</version>
</dependency>
This will automatically also import the aws-serverless-java-container-core
and aws-lambda-java-core
libraries.
Dependency injection with Spring can have a significant impact on your function's cold start time. To address this, you can include the spring-context-indexer
dependency to generate a list of candidate components at compile time.
In your application package declare a new class that implements Lambda's RequestStreamHandler
interface. If you have configured API Gateway with a proxy integration, you can use the built-in POJOs AwsProxyRequest
and AwsProxyResponse
.
The next step is to declare the container handler object. The library exposes a utility static method that configures a SpringBootLambdaContainerHandler
object for AWS proxy events. The method receives a class annotated with Spring Boot's @SpringBootApplication
. The object should be declared as a class property and be static. By doing this, Lambda will re-use the instance for subsequent requests.
The handleRequest
method of the class can use the handler
object we declared in the previous step to send requests to the Spring application.
public class StreamLambdaHandler implements RequestStreamHandler {
private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
try {
handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class);
// If you are using HTTP APIs with the version 2.0 of the proxy model, use the getHttpApiV2ProxyHandler
// method: handler = SpringBootLambdaContainerHandler.getHttpApiV2ProxyHandler(Application.class);
} catch (ContainerInitializationException e) {
// if we fail here. We re-throw the exception to force another cold start
e.printStackTrace();
throw new RuntimeException("Could not initialize Spring Boot application", e);
}
}
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
handler.proxyStream(inputStream, outputStream, context);
}
}
In our sample application The Application
class contains the configuration.
@SpringBootApplication
@Import({ PetsController.class })
public class Application extends SpringBootServletInitializer {
// silence console logging
@Value("${logging.level.root:OFF}")
String message = "";
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
By default, Spring Boot projects include the spring-boot-maven-plugin
and an embedded Tomcat application server. To package the Spring Boot application for AWS Lambda, we do not need the Spring Boot maven plugin and we can configure the shade plugin to exclude the embedded Tomcat - the serverless-java-container library takes its place.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<excludes>
<exclude>org.apache.tomcat.embed:*</exclude>
</excludes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Spring Boot applications can be slow to start, particularly if they discover and initialize a lot of components. In the example above, we recommend using a static block or the constructor of your RequestStreamHandler
class to initialize the framework to take advantage of the higher CPU available in AWS Lambda during the initialization phase. However, AWS Lambda limits the initialization phase to 10 seconds. If you application takes longer than 10 seconds to start, AWS Lambda will assume the sandbox is dead and attempt to start a new one. To make the most of the 10 seconds available in the initialization, and still return control back to the Lambda runtime in a timely fashion, we support asynchronous initialization:
public class StreamLambdaHandler implements RequestStreamHandler {
private SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
public StreamLambdaHandler() throws ContainerInitializationException {
handler = new SpringBootProxyHandlerBuilder<AwsProxyRequest>()
.defaultProxy()
.asyncInit()
.springBootApplication(SlowApplication.class)
.buildAndInitialize();
}
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
handler.proxyStream(inputStream, outputStream, context);
}
}
The SpringBootProxyHandlerBuilder
provides a method to pass the default initialization parameters to Serverless Java Container (defaultProxy()
) as well as an asyncInit()
method. The asynchronous initializer uses an initialization timestamp to estimate how much time has already elapsed since AWS Lambda created the handler. The asynchronous initializer will start our Spring Boot application context in a separate thread and return control back to AWS Lambda just before the 10 seconds timeout expires. Obviously, if your application takes less than 10 seconds to start we will return cotrol back to AWS Lambda right away. When Lambda sends the first event, Serverless Java Container will wait for the asynchronous initializer to complete its work before sending the event for processing. By default, Serverless Java Container will wait for an additional 10 seconds. You can change this using the setInitializationTimeout
method of the ContainerConfig
object.
The Spring archetypes and samples include a @Bean
that returns a HandlerExceptionResolver
in the @Application
class to improve Spring start time. By default, the handlerExceptionResolver()
method supersedes custom @ControllerAdvice
classes. To use your own custom class you must first remove the handlerExceptionResolver()
from the @Application
class.
Spring relies heavily on introspection to discover resources, beans, and wire your application together. Introspection can be CPU-intensive and contributes to the cold start time of a Java application. There are a number of optimization we can make in our Spring application to minimize cold start times in Lambda.
In our examples above, the SpringLambdaContainerHandler
is declared as a static variable in the RequestStreamHandler
implementation and initialized in a static block. This means Lambda executes the code as it starts the JVM, giving you better performance out of the gate.
private static SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
try {
handler = SpringLambdaContainerHandler.getAwsProxyHandler(PetStoreSpringAppConfig.class);
} catch (ContainerInitializationException e) {
// if we fail here. We re-throw the exception to force another cold start
e.printStackTrace();
throw new RuntimeException("Could not initialize Spring framework", e);
}
}
The Spring @ComponentScan
annotation can receive a package and automatically scans all of the classes in the package for Spring-related configuration, resources, and beans. While this comes very handy during development, when classes are changing frequently, it forces the Spring framework to perform the notoriously heavy operation of discovering all of the necessary classes at is starts. Before deploying your application to AWS Lambda, you should consider switching from the @ComponentScan
annotation to direct @Import
of your classes.
@Configuration
@Import({ PetsController.class })
public class PetStoreSpringAppConfig {
...
}
Starting the SpringLambdaContainerHandler
with the PetStoreAppConfig
class above, allows Spring to introspect only the classes you declared in your @Import
statement - which are also already loaded by the JVM - thus avoiding the heavy task of scanning an entire package.
The Spring container can automatically wire relationships between collaboration beans using introspection of the bean classes. This eliminates the need to explicitly specify the relationships as properties or constructor arguments within the application configuration metadata.
Instead, use the @Autowired
annotation to load the bean from a class you previously declared in your @Import
annotation.
To associate parameter names with their respective bean, Spring requires the to be compiled with the debug flag enabled. Spring caches the relationship on disk, which causes significant I/O time penalty. Use the @ConstructorProperties
annotation instead.
public class Pet {
@ConstructorProperties({"name", "breed"})
public Pet(String name, String breed) {
this.name = name;
this.breed = synopsis;
}
}
You can follow the instructions in AWS Lambda's documentation on how to package your function for deployment.
There are two ways to activate Spring Profiles (as defined with the @Profile
annotation). We recommend using the static initializer that receives a list of profiles. The Serverless Java Container framework takes care of setting the profile and initializing the application at once.
public class StreamLambdaHandler implements RequestStreamHandler {
private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
try {
handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class, "profile-1", "profile-2");
} catch (ContainerInitializationException e) {
// if we fail here. We re-throw the exception to force another cold start
e.printStackTrace();
throw new RuntimeException("Could not initialize Spring Boot application", e);
}
}
...
}
If you need to reload profiles at runtime, you can use the SpringBootLambdaContainerHandler.activateSpringProfiles(String...)
method - be aware that this operation causes the entire application context to reload, making the following request slower. See @Profile
documentation for details.