Elegant Spring Boot Exception Handling: Assertions, Enums & @ControllerAdvice
This article demonstrates how to eliminate 95% of try-catch blocks in Spring Boot by combining custom assertions with enum-based error codes, unified @ControllerAdvice exception handling, and a standardized response structure for clean, maintainable error management.
Background: The Problem with Scattered try-catch Blocks
Software development inevitably involves handling numerous exceptions. The author notes that over half of development time is spent on exception handling, leading to verbose try {...} catch {...} finally {...} blocks that reduce readability. The article contrasts "ugly" controller code littered with try-catch against a clean controller that delegates all exception handling elsewhere.
What Is Unified Exception Handling?
Spring 3.2 introduced @ControllerAdvice, which works with @ExceptionHandler, @InitBinder, and @ModelAttribute. An @ExceptionHandler method in a controller handles specified exceptions for that controller only, causing redundancy across controllers. A BaseController base class reduces duplication but introduces coupling and single-inheritance limitations in Java. @ControllerAdvice solves this by applying exception handlers globally to all controllers without inheritance. The author categorizes exceptions by phase: pre-controller exceptions (e.g., 404, method-not-supported, media-type mismatches, missing parameters, type mismatches, message read/write failures) and service-layer exceptions (business logic errors).
Goal: Eliminate 95% of try-catch with Assertions
The target is to replace explicit try-catch with elegant Assert (assertion) style checks, letting developers focus on business logic.
Replacing throw exception with Assert
Spring's org.springframework.util.Assert provides methods like Assert.notNull(obj, message) that internally throw IllegalArgumentException. The author shows a test comparison: Assert.notNull(user, "用户不存在.") vs. an explicit if (user == null) throw new IllegalArgumentException(...) block. The assertion version is cleaner.
Building a Custom Assertion Framework with Enums
To throw domain-specific exceptions (e.g., UserNotFoundException with code 7001) instead of generic ones, the author designs: IResponseEnum interface with getCode() and getMessage(). BaseException holding a response enum, args, and message. BusinessException extends BaseException. Assert interface with default methods assertNotNull(obj) and assertNotNull(obj, args) that delegate exception creation to newException(args) and newException(Throwable, args). BusinessExceptionAssert extends IResponseEnum, Assert providing default newException implementations that format messages via MessageFormat and instantiate BusinessException.
An enum ResponseEnum implements BusinessExceptionAssert defines constants like BAD_LICENCE_TYPE(7001, "Bad licence type.") and LICENCE_NOT_FOUND(7002, "Licence not found."). Each enum instance becomes a reusable assertion: ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence). Adding a new error case only requires a new enum constant — no new exception classes.
Common enums ( CommonResponseEnum, ArgumentResponseEnum, ServletResponseEnum) cover generic errors (server busy, validation, 404, etc.).
Defining the Unified Exception Handler
The UnifiedExceptionHandler class is annotated with @ControllerAdvice, @Component, @ConditionalOnWebApplication, and @ConditionalOnMissingBean. It injects a UnifiedMessageSource for i18n and reads spring.profiles.active to detect production.
Key handler methods: handleBusinessException(BusinessException) and handleBaseException(BaseException) — log and return ErrorResponse(code, message) using the exception's enum code and i18n message. handleServletException(Exception) — catches a list of Spring MVC exceptions ( NoHandlerFoundException, HttpRequestMethodNotSupportedException, HttpMediaTypeNotSupportedException, MissingPathVariableException, MissingServletRequestParameterException, TypeMismatchException, HttpMessageNotReadableException, HttpMessageNotWritableException, HttpMediaTypeNotAcceptableException, ServletRequestBindingException, ConversionNotSupportedException, MissingServletRequestPartException, AsyncRequestTimeoutException). It maps the exception class name to ServletResponseEnum for a specific code; falls back to SERVER_ERROR. In production, it masks details and returns a generic server error. handleBindException(BindException) and handleValidException(MethodArgumentNotValidException) — delegate to wrapperBindingResult which concatenates all field errors into a single message, returning VALID_ERROR code. handleException(Exception) — catch-all for unknown exceptions (e.g., database errors). In production, returns generic SERVER_ERROR; otherwise returns the exception message.
Making 404 Throw an Exception
By default, Spring forwards 404 to /error and renders a Whitelabel Error Page. To let @ExceptionHandler catch it, configure:
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=falseThen NoHandlerFoundException is thrown and handled by handleServletException.
Unified Response Structure
All responses share code and message. BaseResponse is the base. CommonResponse<T> adds data. ErrorResponse for errors. QueryDataResponse extends CommonResponse with data typed as QueryData containing totalCount, pageNo, pageSize, records. Short aliases R and QR simplify returns: new R<>(data), new QR<>(queryData).
Verification with a Sample Service
A LicenceService (using MyBatis-Plus) demonstrates usage: - queryDetail calls checkNotNull(licence) which invokes ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence). - getLicences validates licenceType via ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(licenceTypeEnum). - addLicence uses @Transactional.
Test cases show: 1. Requesting non-existent licence ID → returns LICENCE_NOT_FOUND (code 7002). 2. Invalid licence type → returns BAD_LICENCE_TYPE (code 7001). 3. Accessing unknown endpoint → 404 handled via NoHandlerFoundException. 4. Wrong HTTP method → HttpRequestMethodNotSupportedException. 5. Missing required request param → BindException / MethodArgumentNotValidException with aggregated field errors. 6. Invalid POST body → validation errors collected. 7. Adding unmapped entity field → database exception caught by handleException, returning SERVER_ERROR (masked in production).
Production Masking & Internationalization
In production, handleServletException and handleException return a generic "network error" message instead of raw stack traces. The getMessage method looks up i18n keys like response.{enumName} via UnifiedMessageSource, falling back to the exception's default message.
Summary
Combining assertions, enums implementing BusinessExceptionAssert, and a global @ControllerAdvice handler captures most exceptions cleanly. Each module only defines its own enum constants. Remaining gaps (Spring Cloud Security auth errors, gateway fallback, cross-module/remote call errors) are noted for future articles. An alternative @RestControllerAdvice example using a simple JsonResult is also provided.
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.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
