Understanding @ControllerAdvice: Usage and Underlying Implementation in Spring MVC
The article explains how @ControllerAdvice, a specialization of @Component, enables global exception handling, data binding, and model attribute preprocessing in Spring MVC, demonstrates practical code examples for @ExceptionHandler, @InitBinder, and @ModelAttribute, and dives into the DispatcherServlet internals that make these features work.
Spring MVC provides the @ControllerAdvice annotation as a specialization of @Component, allowing developers to extract common controller logic—such as global exception handling, data binding, and model attribute preprocessing—into separate classes, thereby reducing duplication and improving maintainability.
2. @ExceptionHandler
Without global handling, a null‑pointer in a controller method results in a 500 response like:
@GetMapping("/111")
public void test111() {
User user = null;
String userNo = user.getUserNo();
System.out.println(userNo);
}The response body contains an unfriendly JSON error. Using @ControllerAdvice, a GlobalExceptionHandler class can centralise handling of various exceptions:
package com.shepherd.basedemo.advice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.HashMap;
import java.util.Map;
/**
* @author fjzheng
* @version 1.0
* @date 2024/6/13 14:41
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* Global exception handling
*/
@ResponseBody
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(Exception.class)
public ResponseVO exceptionHandler(Exception e) {
if (e instanceof BizException) {
BizException bizException = (BizException) e;
if (bizException.getCode() == null) {
bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());
}
return ResponseVO.failure(bizException.getCode(), bizException.getMessage());
} else if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
Map<String, String> map = new HashMap<>();
BindingResult result = ex.getBindingResult();
result.getFieldErrors().forEach(item -> {
String message = item.getDefaultMessage();
String field = item.getField();
map.put(field, message);
});
log.error("数据校验出现错误:", e);
return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);
} else if (e instanceof HttpRequestMethodNotSupportedException) {
log.error("请求方法错误:", e);
return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求方法不正确");
} else if (e instanceof MissingServletRequestParameterException) {
log.error("请求参数缺失:", e);
MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;
return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数缺少: " + ex.getParameterName());
} else if (e instanceof MethodArgumentTypeMismatchException) {
log.error("请求参数类型错误:", e);
MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;
return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数类型不正确:" + ex.getName());
} else if (e instanceof NoHandlerFoundException) {
NoHandlerFoundException ex = (NoHandlerFoundException) e;
log.error("请求地址不存在:", e);
return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());
} else {
// System exceptions such as NullPointerException
log.error("【系统异常】", e);
return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());
}
}
}After adding this advice, the same endpoint returns a uniform JSON structure that is easier for the front‑end to process.
3. @InitBinder
The @InitBinder annotation allows custom data binding before a controller method executes. Without it, a GET request that binds a User object with a Date field fails with a BindException because Spring cannot convert the string to java.util.Date:
@GetMapping("/222")
public void test222(User user) {
System.out.println(user);
}
@Data
public class User {
private Long id;
private Date birthday;
}Adding an @InitBinder method registers a CustomDateEditor to handle the conversion:
@ControllerAdvice
public class GlobalAdviceHandler {
@InitBinder
public void initBinder(WebDataBinder binder) {
// Custom data binding logic
binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), false));
}
}Note that @InitBinder only applies to GET/POST parameters bound via method arguments, not to JSON bodies. For JSON, annotate the date field with
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")or configure the global Jackson date format in application.yml:
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
locale: zh_CN
time-zone: GMT+8
default-property-inclusion: non_null4. @ModelAttribute
@ModelAttributecan populate method parameters or model attributes before controller execution. An example provides a login user object from a holder:
@ControllerAdvice
public class GlobalAdviceHandler {
@ModelAttribute("loginUser")
public LoginUser setLoginUser() {
return RequestUserHolder.getCurrentUser();
}
}Controllers can then receive the user via:
@PostMapping("/student")
public ResponseVO<Long> addStudent(@ModelAttribute("loginUser") LoginUser loginUser, @RequestBody Student student) {
return ResponseVO.success(studentService.addStudent(loginUser, student));
}Directly calling RequestUserHolder.getCurrentUser() is also possible; the choice depends on whether the developer prefers passing the context as a method argument.
5. @ControllerAdvice Implementation Principle
Spring MVC’s core dispatcher is DispatcherServlet. During startup it invokes #initStrategies(ApplicationContext context) to initialise components such as MultipartResolver, LocaleResolver, HandlerMappings, HandlerAdapters, and HandlerExceptionResolvers:
protected void initStrategies(ApplicationContext context) {
// Initialise MultipartResolver
initMultipartResolver(context);
// Initialise LocaleResolver
initLocaleResolver(context);
// Initialise ThemeResolver
initThemeResolver(context);
// Initialise HandlerMappings
initHandlerMappings(context);
// Initialise HandlerAdapters
initHandlerAdapters(context);
// Initialise HandlerExceptionResolvers
initHandlerExceptionResolvers(context);
// Initialise RequestToViewNameTranslator
initRequestToViewNameTranslator(context);
// Initialise ViewResolvers
initViewResolvers(context);
// Initialise FlashMapManager
initFlashMapManager(context);
}Each request passes through
#doDispatch(HttpServletRequest request, HttpServletResponse response), which selects a handler, obtains the appropriate HandlerAdapter, and invokes the handler method. The RequestMappingHandlerAdapter is responsible for processing @ControllerAdvice beans:
@Override
public void afterPropertiesSet() {
// Initialise ControllerAdvice related beans first
initControllerAdviceCache();
// Initialise argument resolvers
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
// Initialise initBinder argument resolvers
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
// Initialise return value handlers
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// <1> Scan beans annotated with @ControllerAdvice and sort them
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
// Iterate over ControllerAdviceBean array
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// Scan @ModelAttribute methods without @RequestMapping
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}
// Scan @InitBinder methods
Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(adviceBean, binderMethods);
}
// Add RequestBodyAdvice / ResponseBodyAdvice implementations
if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}
// Populate requestResponseBodyAdvice list
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
}These steps illustrate how Spring MVC discovers @ControllerAdvice beans, caches their @ExceptionHandler, @InitBinder, and @ModelAttribute methods, and integrates them into the request processing pipeline.
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.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
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.
