Advanced Spring MVC Techniques: Centralized Exception Management, Request Interception, and Data Validation

Centralized Exception Management

Handling errors uniformly across a web application prevents stack traces from leaking to clients and ensures consistent HTTP response formats. Spring provides a declarative approach to route exceptions to specific handler methods.

Global Error Routing with @RestControllerAdvice

By applying @RestControllerAdvice to a dedicated class, developers can intercept exceptions thrown by any controller in the context. This approach decouples error logic from business handlers and allows for standardized JSON error payloads.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;

@RestControllerAdvice
public class GlobalErrorMapper {

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleMissing(NotFoundException ex) {
        Map<String, Object> payload = Map.of(
            "code", HttpStatus.NOT_FOUND.value(),
            "detail", ex.getLocalizedMessage()
        );
        return new ResponseEntity<>(payload, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalStateException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ResponseEntity<String> handleConflict(IllegalStateException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body("Operation rejected: " + ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseEntity<Map<String, String>> handleFallback(Exception ex) {
        // In production, route this to a logging framework
        Map<String, String> fallback = Map.of("error", "Unrecoverable system failure");
        return ResponseEntity.internalServerError().body(fallback);
    }
}

The @ExceptionHandler directive binds specific exception types to their corresponding methods. When multiple handlers match, Spring selects the most specific one. The @ResponseStatus annotation overrides the default HTTP status, ensuring clients receive accurate failure codes.

Request Interception Mechanism

Interceptors operate within the Spring MVC dispatch fllow, executing logic before controller invocation, after controller execution, or upon request completion. They are ideal for cross-cutting concerns like authentication, audit logging, and performance tracking.

Implementing the Interceptor Lifecycle

Custom interceptors must implement HandlerInterceptor. The three lifecycle hooks provide distinct execution windows:

import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ApiAuditFilter implements HandlerInterceptor {

    private static final String TIMESTAMP_KEY = "req_initiated_at";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        request.setAttribute(TIMESTAMP_KEY, System.nanoTime());
        
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false; // Halts the dispatch chain
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, org.springframework.web.servlet.ModelAndView modelAndView) {
        Long start = (Long) request.getAttribute(TIMESTAMP_KEY);
        if (start != null) {
            long elapsedMs = (System.nanoTime() - start) / 1_000_000;
            request.setAttribute("processing_duration_ms", elapsedMs);
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        Long start = (Long) request.getAttribute(TIMESTAMP_KEY);
        if (start != null) {
            System.out.printf("Completed %s in %dms%n", request.getRequestURI(), (System.nanoTime() - start) / 1_000_000);
        }
    }
}

Interceptor Registration

Interceptors remain inactive until explicitly mapped to URL patterns within the MVC configuration. Implementing WebMvcConfigurer allows declarative path matching and exclusion rules.

import org.springframework.context.annotation.Bean;
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 RoutingConfig implements WebMvcConfigurer {

    @Bean
    public ApiAuditFilter auditFilter() {
        return new ApiAuditFilter();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(auditFilter())
                .addPathPatterns("/api/v2/**")
                .excludePathPatterns("/api/v2/status", "/api/v2/public/**");
    }
}

Input Data Validation

Accepting raw HTTP payloads without validation exposes applications to data corruption and security vulnerabilities. Spring integrates seamlessly with the Jakarta Bean Validation API (formerly JSR-303/380), enabling declarative constraint enforcement directly on data transfer objects.

Defining Domain Constraints

Validation rules are attached to model fields using standard annotations. These rules are evaluated automatically when the framework binds request data to Java objects.

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

public class CustomerProfile {

    @NotBlank(message = "Full name cannot be empty")
    private String fullName;

    @Email(message = "Must follow standard email format")
    private String contactEmail;

    @Min(value = 18, message = "Age must be 18 or higher")
    private Integer age;

    // Standard accessors required for framework binding
}

Triggering and Processing Validation Results

Applying @Valid before the request body parameter instructs the dispatcher to run the validation engine. Validation failures are captured in a BindingResult object, which must immediately follow the validated parameter in the method signature to prevent automatic HTTP 400 responses.

import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;
import java.util.List;
import java.util.stream.Collectors;

@RestController
public class RegistrationEndpoint {

    @PostMapping("/clients")
    public ResponseEntity<Object> onboardClient(
            @Valid @RequestBody CustomerProfile profile,
            BindingResult validationState) {

        if (validationState.hasErrors()) {
            List<String> violations = validationState.getFieldErrors().stream()
                    .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
                    .collect(Collectors.toList());
            
            return ResponseEntity.badRequest().body(violations);
        }

        return ResponseEntity.ok("Profile accepted for processing");
    }
}

Alternatively, validation failures can be routed to a global exception handler by intercepting MethodArgumentNotValidException, allowing controllers to remain free of manual result checking logic.

Tags: spring-mvc exception-handling interceptor-pattern bean-validation web-framework

Posted on Mon, 29 Jun 2026 17:49:26 +0000 by tj71587