How to Eliminate 95% of Try‑Catch Blocks with Unified Exception Handling in Spring
This article explains how to replace repetitive try‑catch blocks in Spring applications by using @ControllerAdvice, custom Assert utilities, and enum‑based exception definitions, providing a clean, unified error‑handling mechanism with consistent response structures and environment‑aware messaging.
Background
In Java development, handling exceptions often leads to abundant try { … } catch { … } finally { … } blocks, which make code verbose and hard to read. The article compares a messy controller with many try‑catch statements to a clean, elegant one, and argues for a strategy that removes most of these blocks.
What Is Unified Exception Handling?
Spring 3.2 introduced @ControllerAdvice, which can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute. By annotating a class with @ControllerAdvice, you can define exception‑handling methods that apply to all controllers, avoiding duplicated handling logic in each controller.
Exceptions are categorized into two stages: before entering the controller and within the service layer.
Goal
Eliminate more than 95% of try‑catch blocks by using an elegant Assert (assertion) approach to validate business logic, allowing developers to focus on core functionality.
Unified Exception Handling in Practice
Replace throw with Assert
Spring provides org.springframework.util.Assert. The article shows a JUnit test using Assert.notNull(user, "User not found.") and compares it with a traditional
if (user == null) { throw new IllegalArgumentException("User not found."); }approach.
@Test
public void test1(){
// ...
User user = userDao.selectById(userId);
Assert.notNull(user, "用户不存在.");
// ...
}
@Test
public void test2(){
// 另一种写法
User user = userDao.selectById(userId);
if (user == null) {
throw new IllegalArgumentException("用户不存在.");
}
}The Assert class simply wraps the null‑check and throws an exception when the condition fails. The article then defines a custom Assert interface that creates domain‑specific exceptions.
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);
}
}
}Two enums implement this interface, providing error codes and messages:
public interface IResponseEnum {
int getCode();
String getMessage();
}
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;
// getters omitted
}Using the enum, you can write concise validation code such as:
ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);Enum‑Based Exception Definition
The enum holds a code and a message, similar to typical response enums. A BusinessException class extends BaseException and receives the enum, arguments, and formatted message.
public class BusinessException extends BaseException {
public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
super(responseEnum, args, message);
}
// other constructors omitted
}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 servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
code = servletExceptionEnum.getCode();
} catch (IllegalArgumentException ex) {
log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
}
if (ENV_PROD.equals(profile)) {
code = CommonResponseEnum.SERVER_ERROR.getCode();
BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
String message = getMessage(baseException);
return new ErrorResponse(code, message);
}
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 baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
String message = getMessage(baseException);
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), message);
}
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
}
}The handler groups exceptions into three categories: pre‑controller (e.g., 404, method not supported), custom business exceptions, and unknown exceptions. In production, detailed messages are hidden and a generic “network error” is returned.
Handling 404 as an Exception
By default Spring forwards 404 to /error. To make it throw an exception, add the following properties:
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=falseNow NoHandlerFoundException can be caught by handleServletException and a unified error response is sent to the front‑end.
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 contains totalCount, pageNo, pageSize, and records.
Validation Example
In a service method, the enum‑based Assert validates input:
ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(licenceTypeEnum);If validation fails, the corresponding BusinessException (e.g., BAD_LICENCE_TYPE) is thrown and handled uniformly.
Testing the Unified Handler
Request a non‑existent licence ID ( /licence/5) → receives LICENCE_NOT_FOUND error.
Request a licence list with an invalid type ( /licence/list?licenceType=ddd) → receives BAD_LICENCE_TYPE error.
Access an undefined endpoint ( /licence/list/ddd) → 404 is captured as NoHandlerFoundException.
Send a request with an unsupported HTTP method → HttpRequestMethodNotSupportedException is captured.
Submit a request with missing required parameters → ArgumentResponseEnum.VALID_ERROR aggregates field‑level messages.
Trigger an unknown exception by adding a new field to the entity without updating the DB schema → database error is caught by the generic handler.
All these scenarios return a JSON object containing code and message, demonstrating that the unified approach consistently handles diverse error conditions.
Production‑Environment Considerations
When the active profile is prod, the handler replaces detailed exception messages with a generic "Network error" to avoid exposing internal details.
Internationalization
The getMessage method looks up a message key response.{enumName} in a message source, allowing error messages to be localized based on the request locale.
Conclusion
By combining custom Assert utilities, enum‑based error codes, and a @ControllerAdvice handler, most try‑catch blocks can be removed, resulting in cleaner code and a consistent error‑response contract. The approach is extensible to additional exception types such as security or gateway errors.
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.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
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.
