Developing Serverless Applications with Spring MVC and AWS Lambda

Joey Mason
3 min readJan 19, 2017

--

Serverless computing is the future. By combining API Gateway with AWS Lambda, you can develop radically scalable, fault-tolerant APIs that are far more cost-effective than almost any traditional setup that involves managing your own fleet of servers. Not only that, but the fact that you no longer need to worry about managing your own machines means that development and operations is inherently easier.

Having said that, the main limitation many developers have faced (myself included) is the lack of frameworks and overall structure for serverless applications. The Serverless framework has emerged as a great option, but it’s really geared toward JavaScript developers. What about those of us who want to develop in Java?

JRestless is a good option. You can even use Spring! However, JRestless relies on Jax-RS and requires that you use Jax-RS mappings (e.g. @Path, @GET, @POST, etc.), so if you want to use Spring MVC, you’re out of luck. Thankfully, there is a new library from awslabs to solve this problem: aws-serverless-java-container.

This library allows you to develop Java applications for AWS Lambda and API Gateway using Jersey, Spark or Spring MVC. In this post, I’ll show you how to set up a simple Spring MVC application on Lambda and how to write integration tests for it properly.

MvcConfig.java

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = {"com.joeyvmason.serverless.spring"})
public class MvcConfig {
}

LambdaHandler.java

When creating the Lambda function in AWS, specify this is as the class to be invoked when executing the Lambda.

public class LambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {

private SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler = SpringLambdaContainerHandler.getAwsProxyHandler(MvcConfig.class);

@Override
public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) {
return handler.proxy(awsProxyRequest, context);
}

HelloController.java

@RestController
@RequestMapping("/hello")
public class HelloController {
@RequestMapping(method = RequestMethod.GET)
public String hello(@PathVariable String id) {
return "Hello World!";
}
}

IntegrationTestConfig.java

@Configuration
public class IntegrationTestConfig {

@Autowired
private ConfigurableWebApplicationContext applicationContext;

@Bean
public MockLambdaContext lambdaContext() {
return new MockLambdaContext();
}

@Bean
public SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> springLambdaContainerHandler() throws ContainerInitializationException {
SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler = SpringLambdaContainerHandler.getAwsProxyHandler(applicationContext);
handler.setRefreshContext(false);
return handler;
}

}

HelloControllerTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {MvcConfig.class, IntegrationTestConfig.class})
@WebAppConfiguration
@TestExecutionListeners(inheritListeners = false, listeners = {DependencyInjectionTestExecutionListener.class})
public class HelloControllerTest {

@Autowired
private MockLambdaContext lambdaContext;
@Test
public shouldSayHello() {
//when
AwsProxyRequest request = new AwsProxyRequestBuilder("/articles", "GET").build();
AwsProxyResponse response = handler.proxy(request, lambdaContext);
//then
assertThat(response.getStatusCode()).isEqualTo(200);
}
}

Lambda Function Configuration

  • Triggers: None
  • Runtime: Java 8 Runtime
  • Handler: com.joeyvmason.serverless.spring.application.LambdaHandler
  • Role: Create new Role from Template
  • Policy: Simple Microservices permissions

API Gateway Configuration

  • Proxy resource: true
  • Resource name: proxy
  • Resource path: {proxy+}

Map Resource to Lambda function

  • Select Integration Request
  • Integration Type: Lambda Function
  • Use Lambda Proxy Integration: true
  • Select region and Lambda function
  • Actions -> Deploy API

For a more in-depth example, as well as a guide to deploying this to AWS Lambda and API Gateway, check out this boilerplate: serverless-spring-boilerplate.

Some other things to remember about developing for AWS Lambda:

  • Lambda containers handle only one request at a time. In a traditional Spring app, resources (e.g. thread pools, HTTP connection pools, etc.) are shared by multiple threads. If you’re using Lambda, you should write your Spring app as if you only need resources for a single execution at a time.
  • Cold starts are a thing. Unlike JavaScript Lambda functions, it’s not uncommon for a Java Lambda container to take 10 seconds to initialize. This becomes less of an issue if traffic is consistent because Amazon will keep the containers around to handle requests. But remember, the bulkier the application, the longer the warm-up time will be, so it’s best to use a microservices architecture when developing Java apps for Lambda.
  • Build fat/uber JARs. Your Lambda function will not work otherwise. If you’re using Gradle, for example, you can do this using shadow.

--

--