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.