Master Unified Exception Handling in Spring with @ControllerAdvice and Assert
This article explains how to replace repetitive try‑catch blocks in Spring applications by using @ControllerAdvice for global exception handling, custom Assert utilities, and enum‑based error codes, resulting in cleaner, more maintainable backend code and consistent API responses.
What is Unified Exception Handling
In Java Spring projects, developers often end up writing many try { … } catch { … } finally { … } blocks, which makes the code noisy and hard to read. A unified approach moves exception handling out of individual controllers and services, improving readability and maintainability.
Using @ControllerAdvice
Spring 3.2 introduced @ControllerAdvice, which can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute. By annotating a class with @ControllerAdvice, you can define methods that handle specific exception types for all controllers.
Replacing try‑catch with Assert
Spring provides org.springframework.util.Assert for concise null checks. The article shows a test example:
@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.");
}
}The Assert.notNull call is more elegant than the explicit if check.
Custom Assert Interface
To throw domain‑specific exceptions, the article defines a custom Assert interface with default methods:
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);
}
}
}Implementations decide which concrete exception to throw.
Enum‑Based Error Codes
Instead of creating many exception classes, an enum can hold code and message pairs. Example:
public interface IResponseEnum {
int getCode();
String getMessage();
}
public class BusinessException extends BaseException {
public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
super(responseEnum, args, message);
}
}
public interface BusinessExceptionAssert extends IResponseEnum, Assert {
@Override
default BaseException newException(Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg);
}
@Override
default BaseException newException(Throwable t, Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg, t);
}
}
@Getter
@AllArgsConstructor
public enum ResponseEnum implements BusinessExceptionAssert {
BAD_LICENCE_TYPE(7001, "Bad licence type."),
LICENCE_NOT_FOUND(7002, "Licence not found.");
private final int code;
private final String message;
}Using the enum, you can write ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence); instead of manual checks.
Unified Exception Handler Class
The core of the solution is a @ControllerAdvice class that handles different exception groups:
@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
@ConditionalOnMissingBean(UnifiedExceptionHandler.class)
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().toString();
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 servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
code = servletExceptionEnum.getCode();
} catch (IllegalArgumentException e1) {
log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
}
if (ENV_PROD.equals(profile)) {
BaseException be = new BaseException(CommonResponseEnum.SERVER_ERROR);
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), getMessage(be));
}
return new ErrorResponse(code, e.getMessage());
}
@ExceptionHandler(BindException.class)
@ResponseBody
public ErrorResponse handleBindException(BindException e) {
log.error("Parameter binding validation error", e);
return wrapperBindingResult(e.getBindingResult());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
log.error("Parameter binding validation error", 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)) {
BaseException be = new BaseException(CommonResponseEnum.SERVER_ERROR);
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), getMessage(be));
}
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
}
}The handler groups exceptions into three categories: pre‑controller (e.g., 404, method not allowed), service‑layer custom exceptions, and unknown exceptions.
Unified Response Structure
All API responses share a base class with code and message. Successful responses add a data field, and paginated results use a QueryDataResponse that extends CommonResponse. Short aliases R and QR are provided for brevity.
Testing the Solution
Various scenarios are demonstrated:
Fetching a non‑existent licence triggers LICENCE_NOT_FOUND.
Providing an invalid licence type triggers BAD_LICENCE_TYPE.
Accessing an undefined URL results in a 404 handled by handleServletException.
Method‑not‑allowed, missing parameters, and type mismatches are all captured and returned with appropriate codes.
When a database column is missing, the unknown exception handler returns a generic server‑error code, and in production mode the message is replaced with a user‑friendly "Network error".
All tests show that the unified handler returns consistent code / message JSON payloads.
Conclusion
By combining @ControllerAdvice, a custom Assert utility, and enum‑based error definitions, most exceptions in a Spring backend can be handled centrally, reducing boilerplate, improving readability, and providing a uniform API contract. The approach can be packaged as a common library for reuse across projects.
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 Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
