Master Unified Exception Handling in Spring with @ControllerAdvice and Assert
This article explains how to replace repetitive try‑catch blocks in Java Spring applications with a clean, centralized exception handling strategy using @ControllerAdvice, custom Assert utilities, and enum‑based error codes, while also showing how to return consistent error responses across services.
Background
In software development, handling exceptions consumes a large portion of time, leading to many repetitive try { ... } catch { ... } finally { ... } blocks that hurt readability. The article compares two coding styles and argues for a cleaner approach.
Ugly try‑catch blocks
These blocks appear in both Controller and Service layers, making the code hard to read.
What is Unified Exception Handling?
Spring 3.2 introduced @ControllerAdvice which can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute. The key annotation for exception handling is @ExceptionHandler, which allows a method in a class annotated with @ControllerAdvice to handle specific exceptions thrown from any controller.
Without a unified approach, each controller would need its own exception handling method, leading to duplicated code and tight coupling.
Goal
Eliminate more than 95% of try catch blocks by using Assert statements to validate business conditions, focusing on business logic instead of repetitive error handling.
Practical Unified Exception Handling
Replace throw exception with Assert
Spring provides org.springframework.util.Assert. Example:
@Test
public void test1() {
...
User user = userDao.selectById(userId);
Assert.notNull(user, "User does not exist.");
...
}
@Test
public void test2() {
// alternative
User user = userDao.selectById(userId);
if (user == null) {
throw new IllegalArgumentException("User does not exist.");
}
}The Assert class essentially wraps the if check and throws an exception when the condition fails.
Custom Assert Interface
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 can create domain‑specific exceptions instead of generic ones.
Enum‑Based Error Codes
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 BusinessException(IResponseEnum responseEnum, Object[] args, String message, Throwable cause) {
super(responseEnum, args, message, cause);
}
}
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 int code;
private String message;
}Each enum instance carries an error code and message, allowing concise validation like ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);.
Unified Exception Handler Class
@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 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)) {
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)) {
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), custom business exceptions, and unknown exceptions. In production, detailed messages are hidden to avoid exposing internal details.
404 Handling
By default Spring forwards 404 to /error. To make it throw an exception, set:
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=falseThen NoHandlerFoundException can be captured by the unified handler.
Unified Response Structure
All API responses share code and message. Successful responses also contain data. Base classes:
public class BaseResponse { private int code; private String message; }
public class CommonResponse<T> extends BaseResponse { private T data; }
public class ErrorResponse extends BaseResponse { }
public class QueryDataResponse<T> extends CommonResponse<QueryData<T>> { }Convenient wrappers R<T> and QR<T> simplify usage.
Verification Demo
A sample project using MyBatis‑Plus demonstrates the flow:
@Service
public class LicenceService extends ServiceImpl<LicenceMapper, Licence> {
@Autowired
private OrganizationClient organizationClient;
public LicenceDTO queryDetail(Long licenceId) {
Licence licence = this.getById( licenceId );
ResponseEnum.LICENCE_NOT_FOUND.assertNotNull( licence );
OrganizationDTO org = ClientUtil.execute(() -> organizationClient.getOrganization(licence.getOrganizationId()));
return toLicenceDTO(licence, org);
}
public QueryData<SimpleLicenceDTO> getLicences(LicenceParam licenceParam) {
LicenceTypeEnum type = LicenceTypeEnum.parseOfNullable(licenceParam.getLicenceType());
ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(type);
// query logic ...
}
@Transactional(rollbackFor = Throwable.class)
public LicenceAddRespData addLicence(LicenceAddRequest request) {
Licence licence = new Licence();
// set fields ...
this.save(licence);
return new LicenceAddRespData(licence.getLicenceId());
}
private void checkNotNull(Licence licence) {
ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}
}Running the service shows that missing licences, invalid licence types, 404 routes, unsupported HTTP methods, parameter validation errors, and database errors are all captured and returned with a consistent code/message payload.
Production Considerations
In production, unknown or servlet exceptions are mapped to a generic "Network error" message to avoid leaking internal details.
Conclusion
Combining Assert utilities, enum‑based error codes, and a centralized @ControllerAdvice handler dramatically reduces boilerplate try‑catch code and provides uniform error responses. For advanced scenarios (security, gateway failures, remote calls) additional handlers may be needed, but the presented pattern forms a solid foundation for clean backend development.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
