Custom Global Exception Handling in Spring Boot

This article explains how to configure Spring Boot to handle default and custom exceptions globally, covering logging of error mappings, adding Fastjson and Freemarker dependencies, configuring properties, defining error entity and custom exception classes, creating an error template, implementing a @ControllerAdvice handler, and testing with sample controllers and JSON responses.

Top Architect
Top Architect
Top Architect
Custom Global Exception Handling in Spring Boot

When a Spring Boot application starts, the console logs show mappings for the {[/error]} path, which is used to display error information in JSON or HTML format depending on the request type.

2018-12-18 09:36:24.627  INFO 19040 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" ...
2018-12-18 09:36:24.632  INFO 19040 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" ...

Default Exception Handling

When an AJAX request triggers an error, Spring Boot returns a JSON response such as:

{
    "timestamp":"2018-12-18T01:50:51.196+0000",
    "status":404,
    "error":"Not Found",
    "message":"No handler found for GET /err404",
    "path":"/err404"
}

For a normal browser request, an HTML error page is displayed.

Custom Exception Handling – Steps

1. Add dependencies:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.54</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

2. Add configuration to application.properties or application.yml to throw exceptions for unmapped paths and disable static resource mappings:

# Throw exception when no handler is found
spring.mvc.throw-exception-if-no-handler-found=true
# Disable static resource mappings
spring.resources.add-mappings=false

3. Create an error information entity:

/**
 * Information entity
 */
public class ExceptionEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    private String message;
    private int code;
    private String error;
    private String path;
    @JSONField(format = "yyyy-MM-dd hh:mm:ss")
    private Date timestamp = new Date();
    // getters and setters omitted for brevity
}

4. Define custom exceptions:

/**
 * Custom base exception
 */
public class BasicException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private int code = 0;
    public BasicException(int code, String message) {
        super(message);
        this.code = code;
    }
    public int getCode() { return this.code; }
}

/**
 * Business exception
 */
public class BusinessException extends BasicException {
    private static final long serialVersionUID = 1L;
    public BusinessException(int code, String message) {
        super(code, message);
    }
}

5. Create an error.ftl template (placed under /src/main/resources/templates/) to render error details in HTML.

<!DOCTYPE html>
<html>
<head>
    <meta name="robots" content="noindex,nofollow" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    <style>
        h2{color:#4288ce;font-weight:400;padding:6px 0;margin:6px 0 0;font-size:18px;border-bottom:1px solid #eee;}
        .exception-var table{width:100%;max-width:500px;margin:12px 0;box-sizing:border-box;table-layout:fixed;word-wrap:break-word;}
        .exception-var table td{padding:0 6px;vertical-align:top;word-break:break-all;}
        .exception-var table td:first-child{width:28%;font-weight:bold;white-space:nowrap;}
    </style>
</head>
<body>
    <div class="exception-var">
        <h2>Exception Datas</h2>
        <table>
            <tbody>
                <tr><td>Code</td><td>${(exception.code)!}</td></tr>
                <tr><td>Time</td><td>${(exception.timestamp?datetime)!}</td></tr>
                <tr><td>Path</td><td>${(exception.path)!}</td></tr>
                <tr><td>Exception</td><td>${(exception.error)!}</td></tr>
                <tr><td>Message</td><td>${(exception.message)!}</td></tr>
            </tbody>
        </table>
    </div>
</body>
</html>

6. Implement a global exception handler using @ControllerAdvice:

/**
 * Global exception handler
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ModelAndView errorHandler(HttpServletRequest request, NoHandlerFoundException exception, HttpServletResponse response) {
        return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.NOT_FOUND.value(), exception.getMessage());
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ModelAndView errorHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException exception, HttpServletResponse response) {
        return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.METHOD_NOT_ALLOWED.value(), exception.getMessage());
    }

    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public ModelAndView errorHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException exception, HttpServletResponse response) {
        return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), exception.getMessage());
    }

    @ExceptionHandler(value = Exception.class)
    public ModelAndView errorHandler(HttpServletRequest request, Exception exception, HttpServletResponse response) {
        return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage());
    }

    @ExceptionHandler(value = BasicException.class)
    private ModelAndView errorHandler(HttpServletRequest request, BasicException exception, HttpServletResponse response) {
        return commonHandler(request, response, exception.getClass().getSimpleName(), exception.getCode(), exception.getMessage());
    }

    @ExceptionHandler(value = BindException.class)
    @ResponseBody
    public ExceptionEntity validExceptionHandler(BindException exception, HttpServletRequest request, HttpServletResponse response) {
        Map<String, String> errors = new HashMap<>();
        for (FieldError error : exception.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        ExceptionEntity entity = new ExceptionEntity();
        entity.setMessage(JSON.toJSONString(errors));
        entity.setPath(request.getRequestURI());
        entity.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        entity.setError(exception.getClass().getSimpleName());
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return entity;
    }

    private ModelAndView commonHandler(HttpServletRequest request, HttpServletResponse response, String error, int httpCode, String message) {
        ExceptionEntity entity = new ExceptionEntity();
        entity.setPath(request.getRequestURI());
        entity.setError(error);
        entity.setCode(httpCode);
        entity.setMessage(message);
        return determineOutput(request, response, entity);
    }

    private ModelAndView determineOutput(HttpServletRequest request, HttpServletResponse response, ExceptionEntity entity) {
        if (!(request.getHeader("accept").contains("application/json") ||
              (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest")))) {
            ModelAndView modelAndView = new ModelAndView("error");
            modelAndView.addObject("exception", entity);
            return modelAndView;
        } else {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setCharacterEncoding("UTF8");
            response.setHeader("Content-Type", "application/json");
            try {
                response.getWriter().write(ResultJsonTools.build(
                        ResponseCodeConstant.SYSTEM_ERROR,
                        ResponseMessageConstant.APP_EXCEPTION,
                        JSONObject.parseObject(JSON.toJSONString(entity))
                ));
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
}

7. Test the setup with a simple controller that deliberately throws different exceptions:

@RestController
public class TestController {
    @RequestMapping(value = "err")
    public void error() {
        throw new BusinessException(400, "业务异常错误信息");
    }
    @RequestMapping(value = "err2")
    public void error2() {
        throw new NullPointerException("手动抛出异常信息");
    }
    @RequestMapping(value = "err3")
    public int error3() {
        int a = 10 / 0;
        return a;
    }
}

Sample JSON responses for the above endpoints (AJAX requests) are shown, demonstrating the unified error format with fields such as code, error, message, and timestamp. Browser requests render the error.ftl HTML page.

References: "Microservices Distributed Architecture Development Practice" by Gong Peng, and related online articles.

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.

JavaException HandlingSpring BootrestControllerAdviceGlobal Exception
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.