Master Unified Exception Handling in Spring: Clean Code with Assertions and Enums
This article explains how to replace repetitive try‑catch blocks in Java backend development by using Spring's @ControllerAdvice for centralized exception handling, leveraging Assert assertions and custom enum‑based error codes to create clean, maintainable code and consistent API responses.
What Is Unified Exception Handling
Since Spring 3.2, the @ControllerAdvice annotation combined with @ExceptionHandler enables developers to define global exception handlers that apply to all controllers, eliminating the need for duplicated try‑catch blocks in each controller or service.
Goal
The aim is to remove more than 95% of try‑catch statements by using Assert (assertions) for business validation, focusing on business logic while the framework handles error propagation.
Implementation
Replace try‑catch with Assert
Using Assert.notNull makes null‑checks concise and readable compared with explicit
if (obj == null) { throw new IllegalArgumentException(...); }statements.
@Test
public void test1() {
User user = userDao.selectById(userId);
Assert.notNull(user, "用户不存在.");
// ...
}
public void test2() {
User user = userDao.selectById(userId);
if (user == null) {
throw new IllegalArgumentException("用户不存在.");
}
// ...
}Assert Source Code
public abstract class Assert {
public static void notNull(@Nullable Object object, String message) {
if (object == null) {
throw new IllegalArgumentException(message);
}
}
// other overloads omitted for brevity
}Custom Exception Enum
Define an enum that implements BusinessExceptionAssert and holds an error code and message. Each enum constant represents a specific business error.
public interface IResponseEnum {
int getCode();
String getMessage();
}
public interface BusinessExceptionAssert extends IResponseEnum, Assert {
default BaseException newException(Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg);
}
default BaseException newException(Throwable t, Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg, t);
}
}
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;
// getters omitted
}Unified Exception Handler
A class annotated with @ControllerAdvice defines methods for handling business exceptions, servlet‑related exceptions, binding errors, validation errors, and generic exceptions. Each method logs the error and returns a consistent ErrorResponse containing a code and message.
@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
public class UnifiedExceptionHandler {
// environment flag
private static final String ENV_PROD = "prod";
@Autowired
private UnifiedMessageSource unifiedMessageSource;
@Value("${spring.profiles.active}")
private String profile;
private 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 ignored) {
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("参数绑定校验异常", e);
return wrapperBindingResult(e.getBindingResult());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
log.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());
}
}Making 404 Throw an Exception
Set spring.mvc.throw-exception-if-no-handler-found=true and spring.resources.add-mappings=false in application.properties so that a missing URL results in NoHandlerFoundException, which is handled by handleServletException.
Unified Response Structure
Define a base response class with code and message. CommonResponse adds a data field, while QueryDataResponse includes pagination fields. Shortcut classes R<T> and QR<T> simplify response creation.
Testing the Handlers
Various HTTP requests (GET, POST, invalid URLs, missing parameters, validation failures) demonstrate how custom enums like ResponseEnum.LICENCE_NOT_FOUND and ResponseEnum.BAD_LICENCE_TYPE produce consistent error codes and messages, how 404 is captured, and how unknown database errors are wrapped into a generic server‑error response.
Conclusion
By combining assertions, enum‑based error codes, and a centralized @ControllerAdvice handler, Java backend projects can dramatically reduce boilerplate try‑catch code, improve readability, and provide uniform API error responses across all layers.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
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.
