Mastering Unified Exception Handling in Spring: Clean Code, Better Errors
This article explains how to replace noisy try‑catch blocks with Spring's @ControllerAdvice and custom Assert utilities, using enums for error codes, unified response structures, and practical MyBatis‑Plus examples to achieve clean, maintainable backend exception handling.
In software development, handling exceptions often results in repetitive try { ... } catch { ... } finally { ... } blocks that clutter code and reduce readability.
What Is Unified Exception Handling
Spring 3.2 added the @ControllerAdvice annotation, which works together with @ExceptionHandler, @InitBinder, and @ModelAttribute to apply exception handling across all controllers, eliminating the need to duplicate handlers in each controller class.
Goal
The aim is to eliminate more than 95% of explicit try catch blocks and replace them with Assert (assertion) checks that focus on business logic while automatically throwing appropriate exceptions.
Unified Exception Handling in Practice
Using Assert (Assertion) Instead of Throwing Exceptions
Spring provides org.springframework.util.Assert for concise null checks and other validations. Example test methods illustrate the readability difference between Assert.notNull(user, "User not found.") and a manual
if (user == null) { throw new IllegalArgumentException("User not found."); }check.
@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 class simply wraps the null‑check and throws an IllegalArgumentException when the condition fails.
Enum‑Based Exception Definitions
Instead of creating a separate exception class for each error scenario, define an enum that holds an error code and message. Implement a BaseException that receives the enum and formats the final message.
public interface IResponseEnum {
int getCode();
String getMessage();
}
public class BusinessException extends BaseException {
public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
super(responseEnum, args, message);
}
// other constructors omitted
}
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, validation becomes a one‑liner:
ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);Unified Exception Handler Class
The UnifiedExceptionHandler class is annotated with @ControllerAdvice and defines multiple @ExceptionHandler methods to process business exceptions, generic base exceptions, servlet‑related exceptions, binding/validation errors, and any other unknown exceptions. It also distinguishes production and development environments to hide internal messages in production.
@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(value = BusinessException.class)
@ResponseBody
public ErrorResponse handleBusinessException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
// other handlers omitted for brevity
}The handler groups exceptions into three categories: pre‑controller servlet exceptions, custom business/base exceptions, and unknown exceptions.
Exception Classification Diagram
Typical Servlet Exceptions Handled
NoHandlerFoundException – 404 not found
HttpRequestMethodNotSupportedException – wrong HTTP method
HttpMediaTypeNotSupportedException – unsupported Content‑Type
MissingPathVariableException – missing @PathVariable
MissingServletRequestParameterException – missing request parameter
TypeMismatchException – type conversion failure
HttpMessageNotReadableException – request body cannot be parsed
HttpMessageNotWritableException – response body cannot be serialized
Unified Response Structure
All API responses share a base class BaseResponse containing code and message. Successful responses extend it with a data field (e.g., CommonResponse, QueryDataResponse). Helper classes R and QR provide concise constructors like new R<>(data) or new QR<>(queryData).
Verification Example
A sample Spring Boot project using MyBatis‑Plus demonstrates the unified handling. Service methods such as queryDetail(Long licenceId), getLicences(LicenceParam licenceParam), and addLicence(LicenceAddRequest request) use ResponseEnum assertions to validate inputs. The article includes screenshots of API calls that trigger various exceptions (missing licence, bad licence type, 404, unsupported method, validation errors, database errors) and shows the consistent JSON error format returned.
Other screenshots illustrate 404 handling, method‑not‑supported errors, validation failures, and database exceptions, confirming that every error is captured and returned with a unified code / message payload.
Production‑Environment Considerations
When the active profile is prod, the handler replaces detailed internal messages with generic ones (e.g., "Network error") to avoid exposing stack traces to end users.
Conclusion
Combining assertions, enum‑based error definitions, and a centralized @ControllerAdvice provides a clean, maintainable way to handle most backend exceptions in Spring applications. For advanced scenarios such as security, gateway fallback, or remote‑call failures, additional handlers may be required.
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 Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
