Serverless Springboot 3
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:
- Set Up the Project: I’ll guide you through configuring your Spring Boot project, including setting up necessary dependencies and application properties.
- Configure Spring Security: We will implement and configure Spring Security to handle authentication and authorization within the serverless environment.
- 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.
- 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.
- 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
- 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 = []
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:
- Migrating Spring Boot 2.x.x Apps to AWS Lambda
- Understanding
var
in Java with Examples - Spring Cloud Gateway with Spring Boot 3.0 Load Balancing
Thanks , Happy coding !