Introduction to Custom Exceptions
While Spring Boot provides robust exception handling mechanisms, sometimes you need to define your own exceptions specific to your application's business logic. This allows for more precise error identification, handling, and reporting. Custom exceptions improve code readability and maintainability by clearly signaling specific error conditions within your application.
Why Create Custom Exceptions?
- Specificity: Built-in exceptions are often too generic. Custom exceptions represent specific errors within your domain.
- Clarity: They make your code easier to understand. Seeing
ProductNotFoundExceptionimmediately tells you what went wrong, compared to a genericException. - Granular Handling: You can catch and handle custom exceptions differently than general exceptions, allowing for tailored responses.
- Maintainability: As your application grows, custom exceptions help organize and manage error handling logic.
Creating a Custom Exception
Creating a custom exception in Java is straightforward. You typically extend the Exception class (for checked exceptions) or RuntimeException (for unchecked exceptions).
Checked vs. Unchecked Exceptions:
- Checked Exceptions: Must be explicitly handled (using
try-catchblocks or declared in the method signature usingthrows). They represent recoverable errors. - Unchecked Exceptions: Don't need to be handled explicitly. They usually represent programming errors (like
NullPointerException) or unrecoverable conditions.
For most business logic errors, extending RuntimeException is a good choice, as it keeps your code cleaner by avoiding the need for excessive throws declarations.
Example:
Let's create a custom exception called ResourceNotFoundException:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
Explanation:
- We extend
RuntimeExceptionmaking it an unchecked exception. - We provide constructors to accept a message (for describing the error) and an optional cause (for chaining exceptions). The
super(message)call passes the message to theRuntimeExceptionconstructor.
Using the Custom Exception
Now, let's use this exception in a Spring Boot service:
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product with ID " + id + " not found"));
}
}
Explanation:
- The
getProductByIdmethod attempts to retrieve a product by its ID. - If the product is not found (
productRepository.findById(id)returns an emptyOptional), we throw ourResourceNotFoundExceptionwith a descriptive message. We use a lambda expression() -> new ResourceNotFoundException(...)to create the exception instance.
Handling the Custom Exception Globally
To handle the ResourceNotFoundException (and other custom exceptions) globally, we can use a @ControllerAdvice and @ExceptionHandler.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public @ResponseBody ErrorResponse handleResourceNotFoundException(ResourceNotFoundException ex) {
return new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
}
// You can add more @ExceptionHandler methods for other custom exceptions
}
// Simple Error Response class
class ErrorResponse {
private int status;
private String message;
public ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
}
Explanation:
@ControllerAdvice: Marks this class as a controller advice, allowing it to handle exceptions globally.@ExceptionHandler(ResourceNotFoundException.class): Specifies that this method should handleResourceNotFoundExceptionexceptions.@ResponseStatus(HttpStatus.NOT_FOUND): Sets the HTTP status code to 404 (Not Found).@ResponseBody: Indicates that the return value should be serialized directly into the response body.ErrorResponse: A simple class to encapsulate the error status and message. You can customize this to include more details as needed.
Testing the Custom Exception
You can write a unit test to verify that the exception is thrown correctly and handled by the global exception handler.
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProductServiceTest {
@Autowired
private ProductService productService;
@MockBean
private ProductRepository productRepository;
@Test(expected = ResourceNotFoundException.class)
public void testGetProductById_ProductNotFound() {
// Mock the repository to return an empty Optional
when(productRepository.findById(1L)).thenReturn(Optional.empty());
productService.getProductById(1L);
}
}
Explanation:
@SpringBootTest: Indicates that this is a Spring Boot integration test.@RunWith(SpringRunner.class): Uses the Spring Runner to manage the test context.@Autowired: Injects theProductServiceand mocks theProductRepository.@MockBean: Replaces the actualProductRepositorywith a mock implementation.when(productRepository.findById(1L)).thenReturn(Optional.empty()): Configures the mock repository to return an emptyOptionalwhenfindById(1L)is called.expected = ResourceNotFoundException.class: Asserts that aResourceNotFoundExceptionis thrown whenproductService.getProductById(1L)is called.
Best Practices
- Meaningful Names: Choose exception names that clearly describe the error condition.
- Specific Messages: Provide informative error messages that help developers diagnose the problem.
- Exception Chaining: Use the
causeparameter in the constructor to chain exceptions, preserving the original error information. - Avoid Overuse: Don't create custom exceptions for every possible error condition. Use built-in exceptions when appropriate.
- Document Your Exceptions: Clearly document your custom exceptions so that other developers understand how to use and handle them.
This tutorial provides a foundation for creating and handling custom exceptions in Spring Boot. By following these principles, you can build more robust, maintainable, and user-friendly applications.