Architectural Analysis of Spring Boot Exception Resolution Mechanism

Global exception handling in Spring Boot allows centralized management of errors across the application layer. The typical implementation involves a configuration class annotated with @ControllerAdvice, where methods are marked using @ExceptionHandler to specify target error types.

@Component
@Order(2)
public class GlobalErrorConfig {

    @ExceptionHandler({Exception.class})
    public ResponseEntity<Object> mapExceptionResponse(
            HttpServletResponse response,
            Exception ex) {
        
        try {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            return new ResponseEntity<>("System Error", HttpStatus.INTERNAL_SERVER_ERROR);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Initialization Phase

During application startup, the WebMvcConfigurationSupport component initializes the primary exception resolution mechanism. The method responsible for constructing the resolver chain ensures that custtom advice is registered alongside default strategies.

public HandlerExceptionResolver createHandlerChain(
        ContentNegotiationManager negotiationManager) {
    List<HandlerExceptionResolver> resolverChain = new ArrayList<>();
    
    configureDefaultResolvers(resolverChain);
    
    if (resolverChain.isEmpty()) {
        populateStandardResolvers(resolverChain, negotiationManager);
    }
    
    extendWithCustomResolvers(resolverChain);
    
    HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
    composite.setOrder(0);
    composite.setExceptionResolvers(resolverChain);
    return composite;
}

Within the standard population logic, ExceptionHandlerExceptionResolver is instantiated. This specific resolver is tasked with detecting beans annotated with @ControllerAdvice and mapping exception handlers to their corresponding exception types.

protected void populateStandardResolvers(
        List<HandlerExceptionResolver> resolverList,
        ContentNegotiationManager manager) {

    ExceptionHandlerExceptionResolver errorResolver = createErrorHandler();
    errorResolver.setContentNegotiationManager(manager);
    errorResolver.setMessageConverters(getMessageConverters());
    
    // Initialize cache for advice beans
    errorResolver.afterPropertiesSet();
    
    resolverList.add(errorResolver);
    resolverList.add(new ResponseStatusExceptionResolver());
}

The critical step occurs inside afterPropertiesSet(). It triggers initExceptionHandlerAdviceCache() which scans the application context for beans matching the ControllerAdviceBean criteria.

private void initExceptionHandlerAdviceCache() {
    ApplicationContext context = getApplicationContext();
    if (context == null) {
        return;
    }

    // Locate all controller advice beans
    List<ControllerAdviceBean> annotatedBeans = ControllerAdviceBean.findAnnotatedBeans(context);
    
    for (ControllerAdviceBean adviceBean : annotatedBeans) {
        Class<?> targetType = adviceBean.getBeanType();
        if (targetType == null) {
            throw new IllegalStateException("Unresolvable bean type");
        }
        
        // Create resolver for this specific bean type
        ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(targetType);
        if (resolver.hasExceptionMappings()) {
            handlerAdviceCache.put(adviceBean, resolver);
        }
    }
}

Inside ExceptionHandlerMethodResolver, the constructor iterates over methods annotated with @ExceptionHandler. It builds a mapping table linking specific exception classes to the selected method implementations. If multiple methods handle the same exception type within the same class, an ambiguity error is thrown.

public ExceptionHandlerMethodResolver(Class<?> handlerType) {
    MethodFilter filter = method -> 
        AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
        
    for (Method method : MethodIntrospector.selectMethods(handlerType, filter)) {
        for (Class<? extends Throwable> excType : detectExceptionMappings(method)) {
            addExceptionMapping(excType, method);
        }
    }
}

Request Processing Flow

When an HTTP request arrives, the DispatcherServlet processes it. If a runtime exception occurs during handler execution, control passes to processDispatchResult. This method delegates to processHandlerException once an error is detected.

private void processDispatchResult(
        HttpServletRequest request,
        HttpServletResponse response,
        HandlerExecutionChain handlerChain,
        ModelAndView mv,
        Exception exception) throws Exception {

    if (exception != null) {
        Object actualHandler = (handlerChain != null ? handlerChain.getHandler() : null);
        
        // Resolve the exception to a view or response
        mv = resolveHandlerException(request, response, actualHandler, exception);
        
        if (mv != null) {
            // Proceed with error view rendering
            errorView = true;
        }
    }
}

To determine which exception handler applies, the system executes a search strategy managed within the adapter logic.

protected ServletInvocableHandlerMethod selectHandlerMethod(
        HandlerMethod handlerMethod,
        Exception exception) {

    Class<?> controllerClass = null;
    if (handlerMethod != null) {
        controllerClass = handlerMethod.getBeanType();
        ExceptionHandlerMethodResolver localResolver = 
            localExceptionCache.get(controllerClass);
        
        if (localResolver == null) {
            localResolver = new ExceptionHandlerMethodResolver(controllerClass);
            localExceptionCache.put(controllerClass, localResolver);
        }
        
        Method candidate = localResolver.resolveMethod(exception);
        if (candidate != null) {
            return new ServletInvocableHandlerMethod(handlerMethod.getBean(), candidate);
        }
    }

    // Fallback to global ControllerAdvice cache
    for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : 
            globalAdviceCache.entrySet()) {
        
        ControllerAdviceBean adviceEntry = entry.getKey();
        
        if (adviceEntry.isApplicableToBeanType(controllerClass)) {
            ExceptionHandlerMethodResolver res = entry.getValue();
            Method selected = res.resolveMethod(exception);
            if (selected != null) {
                return new ServletInvocableHandlerMethod(
                    adviceEntry.resolveBean(), selected);
            }
        }
    }
    return null;
}

The applicability check relies on predicate evaluation defined in ControllerAdviceBean. By default, without explicit configuration, the advice applies to all controllers. Explicit scoping is determined by checking base package prefixes, assignable super-classes, or presence of specific annotations.

public boolean evaluateScope(@Nullable Class<?> targetClass) {
    if (!hasSelectors()) {
        return true;
    } else if (targetClass != null) {
        // Check configured base packages
        for (String rootPackage : this.basePackages) {
            if (targetClass.getName().startsWith(rootPackage)) {
                return true;
            }
        }
        
        // Check assignable types
        for (Class<?> clazz : this.assignableTypes) {
            if (ClassUtils.isAssignable(clazz, targetClass)) {
                return true;
            }
        }
        
        // Check annotation presence
        for (Class<? extends Annotation> annClass : this.annotations) {
            if (AnnotationUtils.findAnnotation(targetClass, annClass) != null) {
                return true;
            }
        }
    }
    return false;
}

Tags: spring-boot exception-handling source-code-analysis mvc-framework controller-advice

Posted on Thu, 18 Jun 2026 18:13:08 +0000 by keiron77