Introduction
This article primarily explains how to implement global exception handling in a SpringBoot project.
Preparation for SpringBoot Global Exception Handling
Note: If you want to directly obtain the project, you can skip to the bottom and download the project code via the link.
Development Prerequisites
Environment Requirements
- JDK: 1.8
- SpringBoot: 1.5.17.RELEASE
First, the Maven dependencies:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.17.RELEASE</version>
<relativePath />
</parent>
<dependencies>
<!-- Spring Boot Web dependency (core) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
</dependencies>
The configuration file does not need many changes; global exception handling can be achieved directly in the code.
Code Implementation
While SpringBoot projects already have basic exception handling, it is often not suitable for developers. Therefore, we need to uniformly catch and handle these exceptions. SpringBoot provides the @ControllerAdvice annotation, which enables global exception catching. We can define a custom method with the @ExceptionHandler annotation and specify the exception type to handle these caught exceptions uniformly.
Let's look at how to use this annotation through the following example.
Example Code:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(value = Exception.class)
public String exceptionHandler(Exception e) {
System.out.println("Unknown exception! Cause:" + e);
return e.getMessage();
}
}
In the example above, we perform simple secondary processing on the caught exception and return the exception information. While this allows us to know the cause of the exception, it may not be user-friendly enough. Therefore, we can implement a more suitable format using custom exception classes and enums.
Custom Base Interface
First, define a base interface that custom error description enums must implement.
Code:
public interface BaseErrorInfoInterface {
/** Get the error code */
String getResultCode();
/** Get the error description */
String getResultMsg();
}
Custom Enum
Next, create a custom enum that implements this interface.
Code:
public enum CommonEnum implements BaseErrorInfoInterface {
// Data operation error definitions
SUCCESS("200", "Success!"),
BODY_NOT_MATCH("400", "Request data format mismatch!"),
SIGNATURE_NOT_MATCH("401", "Request digital signature mismatch!"),
NOT_FOUND("404", "Resource not found!"),
INTERNAL_SERVER_ERROR("500", "Internal server error!"),
SERVER_BUSY("503", "Server busy, please try again later!");
/** Error code */
private final String resultCode;
/** Error description */
private final String resultMsg;
CommonEnum(String resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}
@Override
public String getResultCode() {
return resultCode;
}
@Override
public String getResultMsg() {
return resultMsg;
}
}
Custom Exception Class
Now, create a custom exception class to handle business exceptions.
Code:
public class BizException extends RuntimeException {
private static final long serialVersionUID = 1L;
/** Error code */
protected String errorCode;
/** Error message */
protected String errorMsg;
public BizException() {
super();
}
public BizException(BaseErrorInfoInterface errorInfoInterface) {
super(errorInfoInterface.getResultCode());
this.errorCode = errorInfoInterface.getResultCode();
this.errorMsg = errorInfoInterface.getResultMsg();
}
public BizException(BaseErrorInfoInterface errorInfoInterface, Throwable cause) {
super(errorInfoInterface.getResultCode(), cause);
this.errorCode = errorInfoInterface.getResultCode();
this.errorMsg = errorInfoInterface.getResultMsg();
}
public BizException(String errorMsg) {
super(errorMsg);
this.errorMsg = errorMsg;
}
public BizException(String errorCode, String errorMsg) {
super(errorCode);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BizException(String errorCode, String errorMsg, Throwable cause) {
super(errorCode, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
@Override
public String getMessage() {
return errorMsg;
}
@Override
public Throwable fillInStackTrace() {
return this;
}
}
Custom Response Format
Define a unified response format for data transfer.
Code:
public class ResultBody {
/** Response code */
private String code;
/** Response message */
private String message;
/** Response data */
private Object result;
public ResultBody() {
}
public ResultBody(BaseErrorInfoInterface errorInfo) {
this.code = errorInfo.getResultCode();
this.message = errorInfo.getResultMsg();
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
/** Success response */
public static ResultBody success() {
return success(null);
}
/** Success response with data */
public static ResultBody success(Object data) {
ResultBody rb = new ResultBody();
rb.setCode(CommonEnum.SUCCESS.getResultCode());
rb.setMessage(CommonEnum.SUCCESS.getResultMsg());
rb.setResult(data);
return rb;
}
/** Error response with error info interface */
public static ResultBody error(BaseErrorInfoInterface errorInfo) {
ResultBody rb = new ResultBody();
rb.setCode(errorInfo.getResultCode());
rb.setMessage(errorInfo.getResultMsg());
rb.setResult(null);
return rb;
}
/** Error response with custom code and message */
public static ResultBody error(String code, String message) {
ResultBody rb = new ResultBody();
rb.setCode(code);
rb.setMessage(message);
rb.setResult(null);
return rb;
}
/** Error response with default code */
public static ResultBody error(String message) {
ResultBody rb = new ResultBody();
rb.setCode("-1");
rb.setMessage(message);
rb.setResult(null);
return rb;
}
@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}
Custom Global Exception Handler
Finally, create a custom global exception handler class.
Code:
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/** Handle custom business exceptions */
@ExceptionHandler(value = BizException.class)
@ResponseBody
public ResultBody handleBizException(HttpServletRequest req, BizException e) {
logger.error("Business exception occurred! Reason: {}", e.getErrorMsg());
return ResultBody.error(e.getErrorCode(), e.getErrorMsg());
}
/** Handle null pointer exceptions */
@ExceptionHandler(value = NullPointerException.class)
@ResponseBody
public ResultBody handleNullPointerException(HttpServletRequest req, NullPointerException e) {
logger.error("Null pointer exception occurred!", e);
return ResultBody.error(CommonEnum.BODY_NOT_MATCH);
}
/** Handle other exceptions */
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResultBody handleException(HttpServletRequest req, Exception e) {
logger.error("Unknown exception!", e);
return ResultBody.error(CommonEnum.INTERNAL_SERVER_ERROR);
}
}
Since the focus is on implementing global exception handling and its testing, we only need to add an entity class and a controller class.
Entity Class
Again, a generic user entity.
Code:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private int age;
public User() {}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}
Controller Layer
The controller is straightforward, implementing basic CRUD with RESTful style. However, some exceptions are deliberately introduced to test global exception handling. These include custom exceptions, null pointer exceptions, and an unexpected exception (e.g., type conversion exception).
Code:
@RestController
@RequestMapping(value = "/api")
public class UserRestController {
@PostMapping("/user")
public boolean insert(@RequestBody User user) {
System.out.println("Starting insert...");
if (user.getName() == null) {
throw new BizException("-1", "User name cannot be empty!");
}
return true;
}
@PutMapping("/user")
public boolean update(@RequestBody User user) {
System.out.println("Starting update...");
// Deliberately cause null pointer exception
String str = null;
str.equals("111");
return true;
}
@DeleteMapping("/user")
public boolean delete(@RequestBody User user) {
System.out.println("Starting delete...");
// Deliberately cause an exception (NumberFormatException)
Integer.parseInt("abc123");
return true;
}
@GetMapping("/user")
public List<User> findByUser(User user) {
System.out.println("Starting query...");
List<User> userList = new ArrayList<>();
User user2 = new User();
user2.setId(1);
user2.setName("xuwujing");
user2.setAge(18);
userList.add(user2);
return userList;
}
}
Application Antry
The main entry class is typical for a SpringBoot project.
Code:
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
System.out.println("Application is running...");
}
}
Functional Testing
After starting the application, use Postman for API testing.
First, test query functionality with a GET request.
GET http://localhost:8181/api/user
Response:
{"code":"200","message":"Success!","result":[{"id":1,"name":"xuwujing","age":18}]}
The program works correctly without interference from global exception handling.
Next, test if custom exceptions are properly caught and handled.
Use POST request.
POST http://localhost:8181/api/user
Body (JSON):
{"id":1}
Response:
{"code":"-1","message":"User name cannot be empty!","result":null}
The custom exception is caught and processed to return a formatted error response.
Now, test null pointer exception handling. In the global exception handler, we have both a specific handler for NullPointerException and a generic handler for Exception. Which one will be triggered?
Use PUT request.
PUT http://localhost:8181/api/user
Body (JSON):
{"id":1,"name":"xuwujing","age":18}
Response:
{"code":"400","message":"Request data format mismatch!","result":null}
The null pointer exception is caught by its specific hendler, showing that global exception handling prioritizes the most specific handler.
Finally, test an unspecified exception (e.g., NumberFormatException).
Use DELETE request.
DELETE http://localhost:8181/api/user
Body (JSON):
{"id":1}
Response:
{"code":"500","message":"Internal server error!","result":null}
The generic Exception handler catches this exception.
All tests are successful. Additionally, global exception handling can also be used for page redirection by removing the @ResponseBody annotation and returning a view path. Note that GlobalExceptionHandler uses @ControllerAdvice, not @RestControllerAdvice. If @RestControllerAdvice is used, the response will be automatically converted to JSON format (similar to @RestController vs @Controller). The choice depends on your needs.
Other Information
The explanation of graceful global exception handling in SpringBoot is complete. Please feel free to correct any errors.
Project Repository
SpringBoot global exception handling project:
Complete SpringBoot study collection: