Skip to content

Quick start Spring

sapessi edited this page Jun 27, 2019 · 33 revisions

You can use the aws-serverless-java-container library to run a Spring application in AWS Lambda. You can use the library within your Lambda handler to load your Spring application and proxy events to it.

In the repository we have included a sample Spring application to get you started.

Maven archetype

You can quickly create a new serverless Spring 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-spring-archetype;

mvn archetype:generate -DgroupId=my.service -DartifactId=my-service -Dversion=1.0-SNAPSHOT \
       -DarchetypeGroupId=com.amazonaws.serverless.archetypes \
       -DarchetypeArtifactId=aws-serverless-spring-archetype \
       -DarchetypeVersion=1.3.2

The archetype sets up a new maven project. The pom.xml includes the dependencies you will need to build a basic Spring API. The generated code includes a StreamLambdaHandler class, the main entry point for AWS Lambda; a SpringApiConfig class that defines a basic Spring application; a controller package with a /ping resource; and a set of unit tests that exercise the application.

The project also includes a file called sam.yaml. 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 SAM Local to run your Serverless API or deploy it to AWS.

Manual setup / Converting existing projects

1. Import dependencies

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.3.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.

2. Create the Lambda handler

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 SpringLambdaContainerHandler object for AWS proxy events. The method receives a class annotated with Spring's @Configuration that defines your application. 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 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);
        }
    }

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        handler.proxyStream(inputStream, outputStream, context);
    }
}

In our sample application The PetStoreSpringAppConfig class contains the configuration.

@Configuration
@Import({ PetsController.class })
public class PetStoreSpringAppConfig {
}

3. Publish your Lambda function

You can follow the instructions in AWS Lambda's documentation on how to package your function for deployment.

Spring profiles

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 SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
    static {
        try {
            AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
            applicationContext.register(PetStoreSpringAppConfig.class);
            handler = SpringLambdaContainerHandler.getAwsProxyHandler(applicationContext, "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 framework", e);
        }
    }
    ...
}

If you need to reload profiles at runtime, you can use the SpringLambdaContainerHandler.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.

Custom @ControllerAdvice classes

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.

Optimizing cold start times

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.

Static handlers

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

Avoid component scan

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.

Avoid relationship autowiring

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.

Avoiding Constructor Injection by Name

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;
  }
}
Clone this wiki locally