Spring Framework 7, Spring Boot 4 Request Handling

Spring Framework 7, Spring Boot 4 Request Handling

Request Handling in Spring Boot Application: How The Internal Flow Works

In this tutorial, we are going to look into how request handling works in a Spring application.

Request Lifecycle Architectural Diagram

Let's look at the request-handling architectural diagram.
Article Image
Here, we are using a simple real-life analogy, e.g, a restaurant order process, to understand the overall flow.
First, the HTTP request is created by the user; it's like you give the order to the waiter.

Servlet Filter

It's like a security guard at the entrance who checks IDs, codes, or passes before letting you in. This will execute before the Spring context process. This is best for CORS, security tokens, etc.

The embedded web server:

Generally, it listens on a port, e.g 8080, and converts the HTTP request from the user to a Spring-compatible request. It's like the restaurant host receives you at the door, assigns a table, and routes the order.

DispatcherServlet:

This is the central entry point that every incoming request will pass through this servlet. It acts as a single gateway for all the requests. It's like a central manager that receives the customer's orders and decides which chef will prepare the corresponding dish.

HandlerInterceptor:

It's like the table manager who checks the reservation (preHandle), observes the meal service, and cleans up after you leave(afterCompletion). It has three lifecycle hooks.
  • preHandle() it executes before the controller logic; if it doesn't satified will return false to stop the execution.
  • postHandle() This will execute after the controller finishes and before rendering data. This is helpful when you want to add properties in the response like timestamp or user info, etc.
  • afterCompletion() Execute after the request processing finishes. This is helpful for resource cleanup.

HandlerMapping:

It's like checking the menu to see which chef handles like pasta, pizza or desserts. This is a lookup table that matches the request URL and HTTP methods, e.g GET /api/products to the controller method or handler function.

HandlerAdapter:

It's like passing the order instruction to the assigned chef in a format that they understand. It will execute the matched controller and convert the request parameters to method arguments.

Global Exception Handler:

It's like a crisis management team; if something happens, the team will manage the situation smoothly. Captured the unhandled exception thrown and map them to a formatted error HTTP response.

HttpMessageConverter:

It's like the prepared meal delivered to the user. It serializes Java objects to a JSON response or deserializes incoming JSON to a Java object.

Request Lifecycle Execution Flow

Let's look at the step-by-step execution flow to understand how it works.
  • The client sends the HTTP request for e.g GET /api/products/1 .
  • The embedded server, like Tomcat, receives the raw bytes and pass to the DispatcherServlet.
  • DispatcherServlet queries the HandlerMapping for locating the code that handles the GET /api/products/1 .
  • DispatcherServlet will pass control to HandlerAdapter that resolve the path variable, i.e id = 1 and execute the destination method.
  • The business logic processing will happen via controller, services, and repository.
  • The return response will pass to HttpMessageConverter that will format it as JSON.
  • DispatcherServlet returns the status code with the JSON body via an embedded server like Tomcat to the client.
If you notice here, DispatcherServlet play a key role in the request lifecycle.

Example

Let's look into the code implementation. This is a simple working code that will demonstrate interceptors, controller, error handling, etc.

Domain Model:

java
package com.csbyte.taskpulse.requestlifecycle;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "products")
public class Product {

    @Id
    private String id;
    private String name;
    private double price;

    public Product() {}

    public Product(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
}
java
package com.csbyte.taskpulse.requestlifecycle;

import java.time.LocalDateTime;

public record ErrorResponse(int status, String message, LocalDateTime timestamp) {
}

Custom Exception:

java
package com.csbyte.taskpulse.requestlifecycle;

public class ProductNotFoundException extends Exception {
    public ProductNotFoundException(String id) {
        super("Product with ID '" + id + "' was not found.");
    }
}

Request Processing Interceptor (HandlerInterceptor):

java
package com.csbyte.taskpulse.requestlifecycle;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class ExecutionTimeInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        request.setAttribute("startTime", startTime);
        System.out.println("Interceptor preHandle " + request.getMethod() + " " + request.getRequestURI());
        return true; // Continue execution chain
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        long startTime = (Long) request.getAttribute("startTime");
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("Interceptor afterCompletion: Completed in " + duration + " ms with status " + response.getStatus());
    }
}
Registering the Interceptor
java
package com.csbyte.taskpulse.requestlifecycle;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final ExecutionTimeInterceptor executionTimeInterceptor;

    public WebConfig(ExecutionTimeInterceptor executionTimeInterceptor) {
        this.executionTimeInterceptor = executionTimeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(executionTimeInterceptor)
                .addPathPatterns("/api/**");
    }
}

Business Logic Layer:

java
package com.csbyte.taskpulse.requestlifecycle;


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable String id) throws ProductNotFoundException {
        return productService.getProductById(id)
                .map(ResponseEntity::ok)
                .orElseThrow(() -> new ProductNotFoundException(id));
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product product) {
        Product savedProduct = productService.createProduct(product);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct);
    }
}
java
package com.csbyte.taskpulse.requestlifecycle;


import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Optional<Product> getProductById(String id) {
        return productRepository.findById(id);
    }

    public Product createProduct(Product product) {
        return productRepository.save(product);
    }
}
java
package com.csbyte.taskpulse.requestlifecycle;

import org.springframework.data.repository.CrudRepository;

public interface ProductRepository extends CrudRepository<Product, String> {
}

Exception Handling:

java
package com.csbyte.taskpulse.requestlifecycle;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.time.LocalDateTime;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleProductNotFound(ProductNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(),
                ex.getMessage(),
                LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "An unexpected error occurred.",
                LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

Dependencies Used:

java
dependencies {
	implementation 'org.springframework.boot:spring-boot-h2console'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-webmvc'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
	testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Properties File:

java
spring.application.name=taskpulse

# Enable H2 In-Memory Database
spring.datasource.url=jdbc:h2:mem:productdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# Automatically create schema on startup
spring.jpa.hibernate.ddl-auto=update

# Enable H2 Web Console (Optional: View DB at http://localhost:8080/h2-console)
spring.h2.console.enabled=true
This shows the overall request flow lifecycle in a Spring Boot application.