Unified Exception Handling in Spring MVC Applications

Introduction

Robust exception management is critical for any web application. Without a centralized strategy, business logic becomes clutttered with repetitive error-handling code. Spring MVC provides multiple mechanisms to manage exceptions consistently across your application.

Method 1: Global Exception Handling with @ControllerAdvice

The most effective approach leverages @ControllerAdvice to create a dedicated exception handler that intercepts errors from all controllers.

Basic View-Based Exception Handler

package com.example.app.error;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@ControllerAdvice
public class ApplicationExceptionHandler {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
    
    @ExceptionHandler(Exception.class)
    public ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        logErrorDetails(exception);
        ModelAndView modelView = new ModelAndView();
        modelView.setViewName("error");
        return modelView;
    }
    
    private void logErrorDetails(Exception exception) {
        StringBuilder errorMessage = new StringBuilder();
        errorMessage.append("Unhandled Exception Occurred\n");
        errorMessage.append(exception.getClass().getCanonicalName());
        
        if (exception.getMessage() != null) {
            errorMessage.append(": ").append(exception.getMessage());
        }
        
        for (StackTraceElement element : exception.getStackTrace()) {
            errorMessage.append("\n\tat ").append(element);
        }
        
        LOGGER.error(errorMessage.toString(), exception);
    }
}

JSON Response for REST APIs

For RESTful services, return structured eror data by adding @ResponseBody:

@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleExceptionAsJson(Exception exception) {
    logErrorDetails(exception);
    
    Map<String, Object> errorResponse = new HashMap<>();
    errorResponse.put("timestamp", System.currentTimeMillis());
    errorResponse.put("status", "error");
    errorResponse.put("message", "An unexpected error occurred");
    
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}

Configuration and DispatcherServlet Behavior

The @ControllerAdvice annotation includes @Component, enabling automatic component scanning. When an unmapped URL is requested, the global handler may not trigger because DispatcherServlet invokes noHandlerFound() which, by default, sends a 404 response without throwing an exception.

To enable exception propagation for unmapped URLs, set throwExceptionIfNoHandlerFound to true:

Java Configuration Approach

package com.example.app.config;

import jakarta.servlet.ServletRegistration;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{RootConfiguration.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebConfiguration.class};
    }

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        boolean parameterSet = registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
        if (!parameterSet) {
            throw new IllegalStateException("Failed to configure DispatcherServlet");
        }
    }
}

Alternative: Direct Servlet Configuration

@Override
protected void registerDispatcherServlet(ServletContext servletContext) {
    super.registerDispatcherServlet(servletContext);
    FrameworkServlet servlet = (FrameworkServlet) servletContext.getAttribute(getServletName());
    servlet.setThrowExceptionIfNoHandlerFound(true);
}

XML Configuraton Approach

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>throwExceptionIfNoHandlerFound</param-name>
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

Static Resource Handling Conflict

When throwExceptionIfNoHandlerFound is enabled, you must disable the default static resource servlet to prevent it from intercepting unmapped requests. Remove the following configuration:

Java Config Removal

// Remove this method from your WebMvcConfigurer implementation
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
}

XML Config Removal

<!-- Remove this from your Spring MVC configuration -->
<mvc:default-servlet-handler />

Custom Static Resource Filter

After disabling the default handler, serve static resources through a custom filter:

package com.example.app.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class ResourceDeliveryFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String resourcePath = httpRequest.getServletPath();
        
        String absolutePath = httpRequest.getServletContext().getRealPath(resourcePath);
        
        try (InputStream inputStream = new java.io.FileInputStream(absolutePath);
             OutputStream outputStream = response.getOutputStream()) {
            
            byte[] buffer = new byte[4096];
            int bytesRead;
            
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            
            outputStream.flush();
        }
    }
}

Register the filter programmatically:

public class AppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        FilterRegistration.Dynamic resourceFilter = servletContext.addFilter("resourceFilter", ResourceDeliveryFilter.class);
        resourceFilter.addMappingForUrlPatterns(null, false, "/static/*");
    }
}

Method 2: Controller-Level @ExceptionHandler

Define exception handling within individual controllers or a base class:

package com.example.app.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

public abstract class BaseWebController {
    
    protected final Logger log = LoggerFactory.getLogger(getClass());
    
    @ExceptionHandler(Exception.class)
    public ModelAndView handleException(Exception exception) {
        log.error("Exception caught in controller: {}", exception.getMessage(), exception);
        return new ModelAndView("error");
    }
    
    @RequestMapping("*")
    public String handleUnknownRequest() {
        return "error";
    }
}

Method 3: SimpleMappingExceptionResolver Bean

Configure exception-to-view mapping through a bean definition:

@Bean
public SimpleMappingExceptionResolver exceptionMappingResolver() {
    SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
    
    Properties mappings = new Properties();
    mappings.put("com.example.app.error.ResourceNotFoundException", "not-found");
    mappings.put("com.example.app.error.AccessDeniedException", "access-denied");
    resolver.setExceptionMappings(mappings);
    
    Properties statusCodes = new Properties();
    statusCodes.put("not-found", "404");
    statusCodes.put("access-denied", "403");
    resolver.setStatusCodes(statusCodes);
    
    resolver.setDefaultErrorView("generic-error");
    resolver.setDefaultStatusCode(500);
    
    return resolver;
}

Combine with a catch-all request mapping for unhandled URLs:

@RequestMapping("*")
public String handleUnmappedRequest() {
    return "error";
}

Method 4: HTTP Status Code Mapping with @ResponseStatus

Annotate custom exceptions to automatically translate them into HTTP status codes:

package com.example.app.error;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Throw this exception from your business logic:

public User retrieveUser(Long id) {
    User user = userRepository.findById(id);
    if (user == null) {
        throw new ResourceNotFoundException("User with ID " + id + " does not exist");
    }
    return user;
}

Tags: spring-mvc exception-handling ControllerAdvice ExceptionHandler dispatcheservlet

Posted on Sun, 12 Jul 2026 16:13:43 +0000 by j9sjam3