Master Unified Exception Handling in Spring Boot: Clean Code with Assertions and Enums
This article explains how to replace repetitive try‑catch blocks in Java Spring applications with a unified exception handling strategy using @ControllerAdvice, custom Assert utilities, and enum‑based error codes, providing clean, maintainable code and consistent API responses.
Background
In Java development, handling exceptions often leads to repetitive try { ... } catch { ... } finally { ... } blocks that clutter code and reduce readability. The article compares a messy try‑catch style with a clean controller implementation.
Elegant controller code eliminates redundant exception handling.
What Is Unified Exception Handling
Spring 3.2 introduced @ControllerAdvice which can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute. The @ExceptionHandler annotation defines a method that handles specific exceptions thrown from any controller, allowing centralized processing.
Goal
Eliminate over 95% of try catch blocks by using Assert statements to validate business conditions, focusing on business logic rather than boilerplate error handling.
Unified Exception Handling in Practice
Using Assert Instead of Throwing Exceptions
Spring’s org.springframework.util.Assert provides methods like Assert.notNull(object, "message") which internally throws an IllegalArgumentException if the condition fails.
@Test
public void test1() {
User user = userDao.selectById(userId);
Assert.notNull(user, "User does not exist.");
}
@Test
public void test2() {
User user = userDao.selectById(userId);
if (user == null) {
throw new IllegalArgumentException("User does not exist.");
}
}Custom Assert interfaces can create domain‑specific exceptions.
public interface Assert {
BaseException newException(Object... args);
BaseException newException(Throwable t, Object... args);
default void assertNotNull(Object obj) {
if (obj == null) {
throw newException(obj);
}
}
default void assertNotNull(Object obj, Object... args) {
if (obj == null) {
throw newException(args);
}
}
}Enum‑Based Error Codes
Define an enum implementing BusinessExceptionAssert that supplies error codes and messages.
public enum ResponseEnum implements BusinessExceptionAssert {
BAD_LICENCE_TYPE(7001, "Bad licence type."),
LICENCE_NOT_FOUND(7002, "Licence not found.");
private int code;
private String message;
// getters omitted
}Usage in service layer:
private void checkNotNull(Licence licence) {
ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}Unified Exception Handler Class
@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
public class UnifiedExceptionHandler {
private static final String ENV_PROD = "prod";
@Autowired
private UnifiedMessageSource unifiedMessageSource;
@Value("${spring.profiles.active}")
private String profile;
public String getMessage(BaseException e) {
String code = "response." + e.getResponseEnum();
String message = unifiedMessageSource.getMessage(code, e.getArgs());
return (message == null || message.isEmpty()) ? e.getMessage() : message;
}
@ExceptionHandler(BusinessException.class)
@ResponseBody
public ErrorResponse handleBusinessException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
@ExceptionHandler(BaseException.class)
@ResponseBody
public ErrorResponse handleBaseException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
@ExceptionHandler({
NoHandlerFoundException.class,
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
HttpMediaTypeNotAcceptableException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
MissingServletRequestPartException.class,
AsyncRequestTimeoutException.class
})
@ResponseBody
public ErrorResponse handleServletException(Exception e) {
log.error(e.getMessage(), e);
int code = CommonResponseEnum.SERVER_ERROR.getCode();
try {
ServletResponseEnum servletEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
code = servletEnum.getCode();
} catch (IllegalArgumentException ex) {
log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
}
if (ENV_PROD.equals(profile)) {
code = CommonResponseEnum.SERVER_ERROR.getCode();
BaseException be = new BaseException(CommonResponseEnum.SERVER_ERROR);
return new ErrorResponse(code, getMessage(be));
}
return new ErrorResponse(code, e.getMessage());
}
@ExceptionHandler(BindException.class)
@ResponseBody
public ErrorResponse handleBindException(BindException e) {
log.error("Parameter binding exception", e);
return wrapperBindingResult(e.getBindingResult());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
log.error("Parameter validation exception", e);
return wrapperBindingResult(e.getBindingResult());
}
private ErrorResponse wrapperBindingResult(BindingResult bindingResult) {
StringBuilder msg = new StringBuilder();
for (ObjectError error : bindingResult.getAllErrors()) {
msg.append(", ");
if (error instanceof FieldError) {
msg.append(((FieldError) error).getField()).append(": ");
}
msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());
}
return new ErrorResponse(ArgumentResponseEnum.VALID_ERROR.getCode(), msg.substring(2));
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ErrorResponse handleException(Exception e) {
log.error(e.getMessage(), e);
if (ENV_PROD.equals(profile)) {
int code = CommonResponseEnum.SERVER_ERROR.getCode();
BaseException be = new BaseException(CommonResponseEnum.SERVER_ERROR);
return new ErrorResponse(code, getMessage(be));
}
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
}
}Handling 404 as an Exception
By default Spring forwards 404 to /error. To make it throw an exception, add to application.properties:
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=falseNow NoHandlerFoundException is caught by handleServletException and a unified error response is returned.
Unified Response Structure
All API responses contain code and message. Successful responses also include data. Base classes:
class BaseResponse { int code; String message; }
class CommonResponse<T> extends BaseResponse { T data; }
class ErrorResponse extends BaseResponse { }
class QueryDataResponse<T> extends CommonResponse<QueryData<T>> { }Convenient wrappers R and QR simplify usage: new R<>(data), new QR<>(queryData).
Validation and Testing
The article demonstrates how custom assertions, enum‑based errors, and the unified handler work in practice: missing resources, invalid licence types, parameter binding errors, and unexpected database errors are all captured and returned with consistent code / message payloads.
Conclusion
Combining assertions with enum‑driven error codes and a centralized @ControllerAdvice handler greatly reduces boilerplate, improves readability, and provides a uniform error‑response format. For production environments, the handler can hide internal details and return generic messages, while still supporting internationalization via UnifiedMessageSource.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
