Serverless Springboot 3

Jahid Momin
6 min readJul 27, 2024

--

Springboot 3.x.x Serverless Guide

Hello everyone, I’m Jahid Momin, and today I’ll walk you through the process of migration of a serverful Spring Boot 3.x.x application with Spring Security to a serverless architecture using AWS Lambda. This guide is designed to covers all necessary steps to ensure a smooth migration to serverless world .

In this article, we will:

  1. Set Up the Project: I’ll guide you through configuring your Spring Boot project, including setting up necessary dependencies and application properties.
  2. Configure Spring Security: We will implement and configure Spring Security to handle authentication and authorization within the serverless environment.
  3. Prepare for AWS Deployment: Learn how to prepare your application for deployment, including creating the AWS SAM (Serverless Application Model) template and setting up the necessary configurations.
  4. Deploy to AWS Lambda: Follow a step-by-step guide to build, package, and deploy your application using AWS SAM, including details on deploying with AWS CLI and testing the deployed application.

Let’s get started!

1. What is Serverless?

Serverless architecture allows you to build and run applications without managing infrastructure. AWS Lambda is a serverless compute service that runs your code in response to events and automatically manages the underlying compute resources.

2. Benefits of Serverless Architecture

  • Cost Efficiency: Pay only for the compute time you consume.
  • Scalability: Automatically scales your application.
  • Reduced Operational Overhead: No need to manage servers.

3. Introduction to AWS SAM

AWS SAM is an open-source framework for building serverless applications. It provides a simple way to define the functions, APIs, databases, and event source mappings of your application.

4. Prerequisites

Before we start, ensure you have the following installed:

  • Java 17
  • Maven
  • AWS CLI (configured with your credentials)
  • AWS SAM CLI

Setting Up Your Spring Boot Project

To start, create a new Spring Boot 3 project using the Spring Initializr. This web tool helps you generate a basic project structure and configuration for your application.

  1. Project Configuration:
  • Project: Maven
  • Language: Java 17
  • Spring Boot: 3.3.0
  • Group: com.example
  • Artifact: springboot3-app
  • Package Name: com.example.springboot3-app
  • Dependencies: Add the following dependencies:
  • Spring Web
  • Lombok
  • Spring Data JPA
  • MySQL Driver
  • Spring Security

2. Configure Application Properties

In your application.properties file, configure your database connection and HikariCP settings:

spring.application.name=springboot3-app
spring.datasource.url=jdbc:mysql://localhost:3306/db?createDatabaseIfNotExist=true&verifyServerCertificate=false&useSSL=false&serverTimeZone=UTC&useLegacyDatetimeCode=false
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.maximum-pool-size=1
spring.datasource.hikari.minimum-idle=1
spring.datasource.hikari.idle-timeout=60000
spring.datasource.hikari.max-lifetime=1800000

Explanation:

  • spring.datasource.url: Specifies the JDBC URL for your MySQL database.
  • spring.datasource.driver-class-name: Defines the driver class for MySQL.
  • spring.datasource.hikari.*: Configures the HikariCP connection pool, which helps manage database connections efficiently.

3. Create an Entity

Define a JPA entity to represent a database table:

package com.example.springboot3-app;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "test_table")
public class MyTestEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "name", nullable = false)
private String name;
}

Explanation:

  • @Entity: Marks the class as a JPA entity.
  • @Table(name = "test_table"): Specifies the table name in the database.
  • @Id and @GeneratedValue(strategy = GenerationType.IDENTITY): Define the primary key and its generation strategy.

4. Create a Repository

Create a repository interface for database operations:

package com.example.springboot3-app;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MyTestEntityRepository extends JpaRepository<MyTestEntity, Long> {
}

// JpaRepository: Provides CRUD operations and more for the entity.

5. Create a REST Controller

Implement a REST controller to handle HTTP requests:

package com.example.springboot3-app;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyRestController {

@Autowired
private MyTestEntityRepository myTestEntityRepository;

//without token its not accessible
@GetMapping("/api/test")
public ResponseEntity<String> test() {
return ResponseEntity.ok("Testing");
}

//public api it will be accessible without filter validation
@GetMapping("/api/getData")
public ResponseEntity<?> getData() {
return ResponseEntity.ok(myTestEntityRepository.findAll());
}
}

6. Configure Spring Security

Set up Spring Security to secure your application

AuthenticationEntryPoint: Handles authentication errors and sends unauthorized responses.

package com.example.springboot3-app.security;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Slf4j
public class AuthEntryPointJwt implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.error("Unauthorized error: {}", authException.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"Unauthorized access\"}");
}
}

JwtTokenFilter: Ensures the filter is executed once per request & handles JWT token validation.

package com.example.springboot3-app.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.io.IOException;

@Slf4j
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
private final HandlerExceptionResolver handlerExceptionResolver;

public JwtTokenFilter(HandlerExceptionResolver handlerExceptionResolver) {
this.handlerExceptionResolver = handlerExceptionResolver;
}

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
try {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

filterChain.doFilter(request, response);

} catch (Exception e) {
handlerExceptionResolver.resolveException(request, response, null, e);
}

filterChain.doFilter(request, response);
}
}

SecurityConfiguration: Ensures the filter is executed once per request & handles JWT token validation.

package com.example.springboot3-app.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
private final JwtTokenFilter jwtAuthenticationFilter;
private final AuthEntryPointJwt unauthorizedHandler;

public SecurityConfiguration(JwtTokenFilter jwtAuthenticationFilter, AuthEntryPointJwt unauthorizedHandler) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.unauthorizedHandler = unauthorizedHandler;
}

//Defines security rules and settings for your application
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/getData").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(unauthorizedHandler)
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowedMethods(List.of("GET", "POST"));
configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);

return source;
}
}

Deploying to AWS Serverless

  1. Update pom.xml : Add aws-serverless-java-container-springboot3 dependency and add plugin to build shaded jar .
//update starter web to exlcude tomcat 

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>


<dependency>
<groupId>com.amazonaws.serverless</groupId>
<artifactId>aws-serverless-java-container-springboot3</artifactId>
<version>2.0.0</version>
</dependency>
   <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<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>
</build>

2. Creating the Lambda Handler :

To handle requests in AWS Lambda, we need to create a handler class. This class implements RequestStreamHandler and sets up the SpringBootLambdaContainerHandler to handle incoming requests.

package com.test3.demo;

import com.amazonaws.serverless.exceptions.ContainerInitializationException;
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler;
import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.FilterRegistration;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.EnumSet;


public class StreamLambdaHandler implements RequestStreamHandler {
private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;

static {
try {
LambdaContainerHandler.getContainerConfig().setInitializationTimeout(20000);
handler = new SpringBootProxyHandlerBuilder<AwsProxyRequest>()
.defaultProxy()
.servletApplication()
.asyncInit()
.springBootApplication(DemoApplication.class)
.buildAndInitialize();

} catch (ContainerInitializationException e) {
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);
}
}

3. Create the AWS SAM Template — template.yaml file in the root of your project:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Test Spring boot 3 API written with SpringBoot with the aws-serverless-java-container library

Globals:
Api:
EndpointConfiguration: REGIONAL

Resources:
TestSpringBoot3Function:
Type: AWS::Serverless::Function
Properties:
Handler: com.test3.demo.StreamLambdaHandler::handleRequest //lambda handler
Runtime: java17
CodeUri: . //output directory after build will generate
MemorySize: 512 //as per your need
Policies: AWSLambdaBasicExecutionRole
SnapStart:
ApplyOn: PublishedVersions
Environment:
Variables:
MAIN_CLASS: com.test3.demo.DemoApplication //for testing
Timeout: 60
Events:
HttpApiEvent:
Type: HttpApi
Properties:
TimeoutInMillis: 20000
PayloadFormatVersion: '1.0'

Outputs:
SpringBoot3TestApi:
Description: URL for application
Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com'
Export:
Name: SpringBoot3TestApi

3 .Build the Project :

sam build

//it will generate an .aws-sam folder in root folder

//Built Artifacts : .aws-sam\build
//Built Template : .aws-sam\build\template.yaml

4. Deploy Project :

sam deploy - guided

//it will ask your some question about cloudformation stack i ll share my responses
//to that questions lets refer that.
//samconfig.toml file

version = 0.1
[default.deploy.parameters]
stack_name = "springboot-3-api-stack"
resolve_s3 = true
s3_prefix = "springboot-3-api-stack"
region = "region-name"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
disable_rollback = true
image_repositories = []
sam build

Once deployed, you will receive an API Gateway endpoint URL. Use this URL to test your deployed application:

curl https://your-api-id.execute-api.region.amazonaws.com/api/test

Feel free to reach out if you have any questions or need further assistance with serverless migrations or Spring Boot. Your feedback and suggestions are always welcome!

Follow me on LinkedIn and check out my other blog posts:

Thanks , Happy coding !

--

--

Jahid Momin
Jahid Momin

Written by Jahid Momin

Team Lead | Sr Software Engineer | Spring boot | Microservices | JavaScript | CodeIgniter | HTML | CSS | ReactJS | NextJS | Youtuber | Writer

Responses (1)