Mastering Custom Exception Handling in Spring Boot: A Complete Guide
This article walks through Spring Boot's default error mapping, shows how to return JSON for AJAX requests and HTML pages for browsers, and provides step‑by‑step code for creating custom exception classes, an error response entity, a Freemarker error template, and a global @ControllerAdvice handler with detailed configuration and testing examples.
Preface
When the application starts, Spring Boot registers two mappings for {[/error]} . When an exception occurs, Spring Boot forwards to /error and renders either JSON or an HTML page 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]}" ...Spring Boot basics are omitted; see the recommended best‑practice repository for a full tutorial.
Default Exception Handling
When an AJAX request triggers an error, Spring Boot returns JSON:
{
"timestamp": "2018-12-18T01:50:51.196+0000",
"status": 404,
"error": "Not Found",
"message": "No handler found for GET /err404",
"path": "/err404"
}When a normal browser request triggers an error, an HTML page is rendered:
Custom Exception Handling
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> fastjsonprovides JSON serialization, while spring-boot-starter-freemarker supplies the template engine for rendering error pages.
Add Configuration
# Throw exception when no handler is found (so 404 can be caught)
spring.mvc.throw-exception-if-no-handler-found=true
# Disable automatic resource mappings
spring.resources.add-mappings=false spring:
mvc:
throw-exception-if-no-handler-found: true
resources:
add-mappings: falseCreate Error Response 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
}Create 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 used in the application */
public class BusinessException extends BasicException {
private static final long serialVersionUID = 1L;
public BusinessException(int code, String message) {
super(code, message);
}
}Create error.ftl Template (placed in /src/main/resources/templates/ )
<!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 caption{font-size:16px;font-weight:bold;text-align:left;padding:6px 0;}
.exception-var table tbody{font-size:13px;font-family:Consolas,"Liberation Mono",Courier,"微软雅黑";}
.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;}
.exception-var table td pre{margin:0;}
</style>
</head>
<body>
<div class="exception-var">
<h2>Exception Data</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>Write Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
/** 404 */
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ModelAndView errorHandler(HttpServletRequest request, NoHandlerFoundException ex, HttpServletResponse response) {
return commonHandler(request, response, ex.getClass().getSimpleName(), HttpStatus.NOT_FOUND.value(), ex.getMessage());
}
/** 405 */
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ModelAndView errorHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException ex, HttpServletResponse response) {
return commonHandler(request, response, ex.getClass().getSimpleName(), HttpStatus.METHOD_NOT_ALLOWED.value(), ex.getMessage());
}
/** 415 */
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ModelAndView errorHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException ex, HttpServletResponse response) {
return commonHandler(request, response, ex.getClass().getSimpleName(), HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), ex.getMessage());
}
/** 500 */
@ExceptionHandler(Exception.class)
public ModelAndView errorHandler(HttpServletRequest request, Exception ex, HttpServletResponse response) {
return commonHandler(request, response, ex.getClass().getSimpleName(), HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
}
/** Business exception */
@ExceptionHandler(BasicException.class)
private ModelAndView errorHandler(HttpServletRequest request, BasicException ex, HttpServletResponse response) {
return commonHandler(request, response, ex.getClass().getSimpleName(), ex.getCode(), ex.getMessage());
}
/** Validation exception */
@ExceptionHandler(BindException.class)
@ResponseBody
public ExceptionEntity validExceptionHandler(BindException ex, HttpServletRequest request, HttpServletResponse response) {
Map<String, String> errors = new HashMap<>();
for (FieldError err : ex.getBindingResult().getFieldErrors()) {
errors.put(err.getField(), err.getDefaultMessage());
}
ExceptionEntity entity = new ExceptionEntity();
entity.setMessage(JSON.toJSONString(errors));
entity.setPath(request.getRequestURI());
entity.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
entity.setError(ex.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 mv = new ModelAndView("error");
mv.addObject("exception", entity);
return mv;
} 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;
}
}
}Annotations Explained @ControllerAdvice: Marks a class as a global exception handler. @ExceptionHandler: Specifies which exception types the method handles. BindException: Thrown by Hibernate Validator when form validation fails.
Test Controller
@RestController
public class TestController {
@RequestMapping("err")
public void error() {
throw new BusinessException(400, "业务异常错误信息");
}
@RequestMapping("err2")
public void error2() {
throw new NullPointerException("手动抛出异常信息");
}
@RequestMapping("err3")
public int error3() {
int a = 10 / 0;
return a;
}
}Sample JSON responses for AJAX requests:
# /err
{
"msg": "应用程序异常",
"code": -1,
"status_code": 0,
"data": {
"path": "/err",
"code": 400,
"error": "BusinessException",
"message": "业务异常错误信息",
"timestamp": "2018-12-18 11:09:00"
}
}
# /err2
{ ... }
# /err3
{ ... }
# /err404
{ ... }Sample HTML page rendered for browser requests (screenshots omitted for brevity).
Reference project: spring‑cloud‑zuul example
Further reading: 《微服务 分布式架构开发实战》 龚鹏 著; Jianshu article
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.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
