Handling exceptions gracefully in a Spring Boot 3 REST application is crucial for providing clear and consistent error responses to the clients. In this section, we’ll explore how to handle exceptions for REST controllers using @ControllerAdvice
and @ExceptionHandler
annotations, and customize error responses.
Setting Up the Project
Ensure you have the necessary dependencies in your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Step 1: Create a Custom Exception
Define a custom exception that can be thrown by your REST controllers:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Step 2: Create a Global Exception Handler
Create a class annotated with @ControllerAdvice
to handle exceptions globally:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.NOT_FOUND.value());
body.put("error", "Not Found");
body.put("message", ex.getMessage());
body.put("path", request.getDescription(false));
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleGlobalException(Exception ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Internal Server Error");
body.put("message", ex.getMessage());
body.put("path", request.getDescription(false));
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Step 3: Create a REST Controller
Create a REST controller that throws the custom exception:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class MyRestController {
@GetMapping("/resource/{id}")
public String getResource(@PathVariable String id) {
if ("1".equals(id)) {
return "Resource found";
} else {
throw new ResourceNotFoundException("Resource with id " + id + " not found");
}
}
}
Step 4: Customize Error Attributes
To customize error attributes for exceptions globally, you can extend DefaultErrorAttributes
as shown in the previous section. Here’s how you can integrate it into your REST application:
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Map;
@Component
public class CustomizedErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
// Customize the error attributes here
errorAttributes.put("locale", webRequest.getLocale().toString());
errorAttributes.put("customMessage", "This is a custom error message");
Throwable error = getError(webRequest);
if (error != null) {
errorAttributes.put("exception", error.getClass().getName());
errorAttributes.put("message", error.getMessage());
}
return errorAttributes;
}
}
Step 5: Run Your Application
Run your Spring Boot application. When you access a non-existent resource, for example http://localhost:8080/api/resource/2
, the global exception handler will catch the ResourceNotFoundException
and return a customized error response.
Using zalando problems
Step 1: Add Dependencies
To start, add the necessary dependencies to your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web</artifactId>
<version>0.27.0</version>
</dependency>
Step 2: Configure Problem Details
You can configure Problem Details globally for your application by creating a ProblemConfig
class. This class can also be used to customize the Problem Details as needed.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zalando.problem.ProblemModule;
import org.zalando.problem.violations.ConstraintViolationProblemModule;
@Configuration
public class ProblemConfig {
@Bean
public ProblemModule problemModule() {
return new ProblemModule();
}
@Bean
public ConstraintViolationProblemModule constraintViolationProblemModule() {
return new ConstraintViolationProblemModule();
}
}
Step 3: Create a Custom Exception
Create a custom exception that you want to handle using Problem Details:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Step 4: Create a Global Exception Handler
Use @ControllerAdvice
to create a global exception handler that converts exceptions to Problem Details:
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.zalando.problem.Problem;
import org.zalando.problem.Status;
import org.zalando.problem.spring.web.advice.ProblemHandling;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler implements ProblemHandling {
@ExceptionHandler(ResourceNotFoundException.class)
public Problem handleResourceNotFoundException(ResourceNotFoundException ex, NativeWebRequest request) {
return Problem.builder()
.withType(URI.create("https://example.com/not-found"))
.withTitle("Resource not found")
.withStatus(Status.NOT_FOUND)
.withDetail(ex.getMessage())
.build();
}
@ExceptionHandler(Exception.class)
public Problem handleGlobalException(Exception ex, NativeWebRequest request) {
return Problem.builder()
.withType(URI.create("https://example.com/internal-server-error"))
.withTitle("Internal Server Error")
.withStatus(Status.INTERNAL_SERVER_ERROR)
.withDetail(ex.getMessage())
.build();
}
}
Step 5: Create a REST Controller
Create a REST controller that uses the custom exception:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class MyRestController {
@GetMapping("/resource/{id}")
public String getResource(@PathVariable String id) {
if ("1".equals(id)) {
return "Resource found";
} else {
throw new ResourceNotFoundException("Resource with id " + id + " not found");
}
}
}
Step 6: Run Your Application
Run your Spring Boot application. When you access a non-existent resource, for example, http://localhost:8080/api/resource/2
, the global exception handler will catch the ResourceNotFoundException
and return a Problem Details response:
{
"type": "https://example.com/not-found",
"title": "Resource not found",
"status": 404,
"detail": "Resource with id 2 not found"
}
Spring mvc Problem Details
The ProblemDetailsExceptionHandler
is a mechanism provided by Spring Boot 3 to handle exceptions and convert them to RFC 7807 Problem Details responses. This allows you to provide more structured and standardized error responses to clients.
Here’s how you can use ProblemDetailsExceptionHandler
in a Spring Boot 3 application:
Step 1: Enable Problem Details
Enable Problem Details in your application.properties
file:
spring.mvc.problemdetails.enabled=true
spring.webflux.problemdetails.enabled=true
These properties enable Problem Details for both Spring MVC and Spring WebFlux.
Step 2: Create a Custom Exception
Define a custom exception that you want to handle using Problem Details:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Step 3: Use ProblemDetailsExceptionHandler
Create a ProblemDetailsExceptionHandler
to handle exceptions and convert them to Problem Details responses:
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.http.ProblemDetail;
import java.net.URI;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problemDetail.setTitle("Resource not found");
problemDetail.setType(URI.create("https://example.com/not-found"));
return problemDetail;
}
@ExceptionHandler(Exception.class)
public ProblemDetail handleGlobalException(Exception ex, WebRequest request) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
problemDetail.setTitle("Internal Server Error");
problemDetail.setType(URI.create("https://example.com/internal-server-error"));
return problemDetail;
}
}
In this example, the handleResourceNotFoundException
method handles ResourceNotFoundException
and converts it to a ProblemDetail
response. The handleGlobalException
method catches any other exceptions and converts them to ProblemDetail
responses as well.
Step 4: Create a REST Controller
Create a REST controller that uses the custom exception:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class MyRestController {
@GetMapping("/resource/{id}")
public String getResource(@PathVariable String id) {
if ("1".equals(id)) {
return "Resource found";
} else {
throw new ResourceNotFoundException("Resource with id " + id + " not found");
}
}
}
Step 6: Run Your Application
Run your Spring Boot application. When you access a non-existent resource, for example, http://localhost:8080/api/resource/2
, the global exception handler will catch the ResourceNotFoundException
and return a Problem Details response:
{
"type": "https://example.com/not-found",
"title": "Resource not found",
"status": 404,
"detail": "Resource with id 2 not found"
}