Understanding the Working Principle of @ControllerAdvice in Spring MVC

This article explains how the @ControllerAdvice annotation is discovered and utilized within Spring MVC, detailing the initialization process, the caching of @ExceptionHandler, @ModelAttribute, and @InitBinder methods, and how these global configurations are applied by RequestMappingHandlerAdapter during request handling.

Top Architect
Top Architect
Top Architect
Understanding the Working Principle of @ControllerAdvice in Spring MVC

In Spring MVC, the combination of annotations such as @ControllerAdvice and others enables developers to apply global customizations to controller classes. This article explores how @ControllerAdvice is discovered, how its related methods are cached, and how the cached information is later used during request processing.

1. How the @ControllerAdvice Annotation Is Discovered

When the container starts, it defines a bean of type RequestMappingHandlerAdapter for the DispatcherServlet. This bean implements InitializingBean, so its #afterPropertiesSet method is invoked during initialization, which calls #initControllerAdviceCache.

@Override
public void afterPropertiesSet() {
    // Do this first, it may add ResponseBody advice beans
    initControllerAdviceCache();
    // ... other code omitted
}

The #initControllerAdviceCache method scans the application context for all beans annotated with @ControllerAdvice, sorts them, and records them in three properties of the RequestMappingHandlerAdapter instance: requestResponseBodyAdvice – stores beans that implement RequestBodyAdvice or ResponseBodyAdvice together with @ControllerAdvice. modelAttributeAdviceCache – stores methods annotated with @ModelAttribute from @ControllerAdvice beans. initBinderAdviceCache – stores methods annotated with @InitBinder from @ControllerAdvice beans.

2. Using the Information Defined by @ControllerAdvice

2.1 requestResponseBodyAdvice Usage

The method #getDefaultArgumentResolvers creates default argument resolvers for RequestMappingHandlerAdapter. It adds RequestResponseBodyMethodProcessor, RequestPartMethodArgumentResolver, and HttpEntityMethodProcessor, each receiving the requestResponseBodyAdvice instance.

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
    // ... omitted code
    resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    return resolvers;
}

This method is called from #afterPropertiesSet of RequestMappingHandlerAdapter to initialise the argument resolver chain.

2.2 modelAttributeAdviceCache Usage

The method #getModelFactory builds a ModelFactory. It retrieves global @ModelAttribute methods from modelAttributeAdviceCache and combines them with local controller methods.

private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
    // ... omitted code
    Set<Method> methods = this.modelAttributeCache.get(handlerType);
    if (methods == null) {
        methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
        this.modelAttributeCache.put(handlerType, methods);
    }
    List<InvocableHandlerMethod> attrMethods = new ArrayList<>();
    // Global methods from @ControllerAdvice
    this.modelAttributeAdviceCache.forEach((clazz, methodSet) -> {
        if (clazz.isApplicableToBeanType(handlerType)) {
            Object bean = clazz.resolveBean();
            for (Method method : methodSet) {
                attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
            }
        }
    });
    // Local controller methods
    for (Method method : methods) {
        Object bean = handlerMethod.getBean();
        attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
    }
    return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}

2.3 initBinderAdviceCache Usage

The method #getDataBinderFactory creates a WebDataBinderFactory. It gathers global @InitBinder methods from initBinderAdviceCache and local controller methods, then builds an InitBinderDataBinderFactory.

private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
    // ... omitted code
    Set<Method> methods = this.initBinderCache.get(handlerType);
    if (methods == null) {
        methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
        this.initBinderCache.put(handlerType, methods);
    }
    List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
    // Global @InitBinder methods from @ControllerAdvice
    this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
        if (clazz.isApplicableToBeanType(handlerType)) {
            Object bean = clazz.resolveBean();
            for (Method method : methodSet) {
                initBinderMethods.add(createInitBinderMethod(bean, method));
            }
        }
    });
    // Local @InitBinder methods
    for (Method method : methods) {
        Object bean = handlerMethod.getBean();
        initBinderMethods.add(createInitBinderMethod(bean, method));
    }
    return createDataBinderFactory(initBinderMethods);
}

Both #getModelFactory and #getDataBinderFactory are invoked inside #invokeHandlerMethod of RequestMappingHandlerAdapter when a request is processed.

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    try {
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
        // ... omitted code
    } finally {
        // ... omitted code
    }
}

Thus, the global configurations defined by @ControllerAdvice —including @ExceptionHandler, @ModelAttribute, and @InitBinder methods—are collected, cached, and later applied by RequestMappingHandlerAdapter during the handling of each request.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaannotationsSpring MVCexceptionhandlingControllerAdvice
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.