前言
在实际项目中,通常会有各种异常抛出,如果我们不加以捕捉并自定义统一的response(返回信息),那么用户就可能看到莫名奇怪的错误。比如常见的 NullPointerException, 你直接返回这个错误信息,相信用户也是一头雾水。
还有就是统一捕捉异常并自定义报错信息也有利于我们排查,迅速定位问题所在。
我们先来看下如果不做全局异常配置会是怎样,如下例所示,在 controller 层的请求里加个 try catch,然后根据不同异常处理,如果有很多请求,那么每个请求都像这样加个 try catch 层,就比较繁杂,而且要捕捉的异常可能很多都相同,这样代码重复就比较多。
@RestController
@RequestMapping("/api")
public class LoginController {
@PostMapping("/login")
public ResponseCode login(@RequestBody User User) {
JSONObject jsonObject = null;
try {
logger.info("Request data:" + User.toString());
jsonObject = loginService.valifyLogin(User);
} catch (RestInputException e) {
// Exception 处理1
return ResponseCode.fail(400, "Bad request.");
} catch (Exception e) {
// Exception 处理2
return ResponseCode.error("System error");
}
return ResponseCode.success("Login success!", jsonObject);
}
}
那怎样优雅地捕捉异常呢,这就需要用到 Spring 的两个注解 @ExceptionHandler 和 @ControllerAdvice。
@ExceptionHandler 作用于方法之上,捕捉 controller 类抛出的指定的异常,例如上面的登陆请求可以简化为如下例子,这样就避免了写重复的try catch 代码。
@RestController
@RequestMapping("/api")
public class LoginController {
@PostMapping("/login")
public ResponseCode login(@RequestBody User User) {
JSONObject jsonObject = loginService.valifyLogin(User);
return ResponseCode.success("Login success!", jsonObject);
}
@ExceptionHandler(RestInputException.class)
@ResponseBody
public ApiResponse handleRestInputException(RestInputException e) {
log.info("Business error: [{}]", e.getMessage());
return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ApiResponse handleExceptions(Exception e) {
log.error("System error", e);
return ApiResponse.error(ApiStatus.INTERNAL_SERVER_ERROR.getCode(), ApiStatus.INTERNAL_SERVER_ERROR.getMsg());
}
}
但如果有多个 controller 类,每个类还是可能要写重复的 @ExceptionHandler 注解方法,这时可以写一个 BaseController 类,其他 controller 类继承这个类即可。
public class BaseController {
@ExceptionHandler(RestInputException.class)
@ResponseBody
public ApiResponse handleRestInputException(RestInputException e) {
log.info("Business error: [{}]", e.getMessage());
return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ApiResponse handleExceptions(Exception e) {
log.error("System error", e);
return ApiResponse.error(ApiStatus.INTERNAL_SERVER_ERROR.getCode(), ApiStatus.INTERNAL_SERVER_ERROR.getMsg());
}
}
@RestController
@RequestMapping("/api")
public class LoginController extends BaseController {/***/}
像上面这样可以为不同业务的 controller 继承一个对应的 BaseController, 里面写相关业务异常的处理就行了 但如果想要一个全局的异常配置呢,这样就要用到 @ControllerAdvice + @ExceptionHandler,两者搭配使用,如下所示
@Slf4j
@ControllerAdvice
public class ApiExceptionConfig {
@ExceptionHandler(RestInputException.class)
@ResponseBody
public ApiResponse handleRestInputException(RestInputException e) {
log.info("Business error: [{}]", e.getMessage());
return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ApiResponse handleExceptions(Exception e) {
log.error("System error", e);
return ApiResponse.error(ApiStatus.INTERNAL_SERVER_ERROR.getCode(), ApiStatus.INTERNAL_SERVER_ERROR.getMsg());
}
}
值得注意的是它们间的优先级,@Controller + @ExceptionHandler 优先级高,@ControllerAdvice + @ExceptionHandler 次之,如果它们定义了相同的异常,优先级高先捕捉异常,而且被捕捉处理了,优先级低的就不再执行。
下面使用 @ControllerAdvice + @ExceptionHandler 方式创建一个全局异常配置项目。
创建项目
项目结构图如下:
pom 依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.yekongle</groupId>
<artifactId>springboot-exception-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-exception-sample</name>
<description>Exception project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
代码编写
异常相关
RestInputException.java
package top.yekongle.exception.exception;
/**
* @Description 自定义异常
* */
public class RestInputException extends RuntimeException {
private static final long serialVersionUID = 1L;
public RestInputException(String message) {
super(message);
}
}
ApiExceptionConfig.java, 全局异常配置
package top.yekongle.exception.config;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import top.yekongle.exception.exception.RestInputException;
import top.yekongle.exception.response.ApiResponse;
import top.yekongle.exception.response.ApiStatus;
/**
* @Description 全局异常配置类
* @Slf4j lombok 注解,自动生成 logger 类
* */
@Slf4j
@ControllerAdvice
public class ApiExceptionConfig {
//处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ApiResponse MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
/*String message = null;
for (ObjectError objectError : e.getBindingResult().getAllErrors()) {
if (message == null) {
message = objectError.getDefaultMessage();
} else {
message = message + objectError.getDefaultMessage();
}
}*/
String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
log.info("Bad request: [{}]", message);
return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), message);
}
@ExceptionHandler(RestInputException.class)
@ResponseBody
public ApiResponse handleRestInputException(RestInputException e) {
log.info("Business error: [{}]", e.getMessage());
return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ApiResponse handleExceptions(Exception e) {
log.error("System error", e);
return ApiResponse.error(ApiStatus.INTERNAL_SERVER_ERROR.getCode(), ApiStatus.INTERNAL_SERVER_ERROR.getMsg());
}
}
Bean相关
TestDto.java
package top.yekongle.exception.dto;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
/**
* 请求实体
* @Data lombok 注解,自动生成 getter setter 方法
* @NotEmpty 参数校验注解
* */
@Data
public class TestDto {
@NotEmpty(message = "名字不能为空!")
private String name;
@NotEmpty(message = "地址不能为空!")
private String address;
}
Person.java
package top.yekongle.exception.model;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* @Description 业务实体类
* */
@Data
public class Person {
@NotEmpty(message = "姓名不能为空!")
private String name;
@NotNull(message = "年龄不能为空!")
private Integer age;
}
下面是结果返回相关类
ApiStatus.java
package top.yekongle.exception.response;
/**
* @Description 自定义返回状态
*/
public enum ApiStatus {
SUCCESS(200, "Success"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
BAD_REQUEST(400, "Bad Request");
private final int code;
private final String msg;
ApiStatus(int code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* Return the integer value of this status code.
*/
public int getCode() {
return this.code;
}
/**
* Return the reason phrase of this status code.
*/
public String getMsg() {
return this.msg;
}
}
ApiResponse.java
package top.yekongle.exception.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @Description 自定义response实体类
*/
@JsonInclude(Include.NON_NULL)
@Data
@AllArgsConstructor
public class ApiResponse {
private Integer code;
private String msg;
private Object data;
public static ApiResponse success(Object data) {
return new ApiResponse(ApiStatus.SUCCESS.getCode(), ApiStatus.SUCCESS.getMsg(), data);
}
public static ApiResponse error(Integer code, String msg) {
return new ApiResponse(code, msg, null);
}
}
业务逻辑类
PersonService.java
package top.yekongle.exception.service;
import top.yekongle.exception.model.Person;
import java.util.Map;
/**
* @Description 业务接口
*/
public interface PersonService {
Map<String, Object> register(Person person);
}
PersonServiceImpl.java
package top.yekongle.exception.service.impl;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
import top.yekongle.exception.exception.RestInputException;
import top.yekongle.exception.model.Person;
import top.yekongle.exception.service.PersonService;
/**
* @Description 业务实现类
*/
@Service
public class PersonServiceImpl implements PersonService {
@Override
public Map<String, Object> register(Person person) {
Map<String, Object> resultMap = null;
if (person.getAge().intValue() < 18) {
throw new RestInputException("未成年人不能注册哦!");
}
resultMap = new HashMap<>();
resultMap.put("ID", 666);
return resultMap;
}
}
控制类
TestController.java
package top.yekongle.exception.controller;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.yekongle.exception.dto.TestDto;
import top.yekongle.exception.model.Person;
import top.yekongle.exception.response.ApiResponse;
import top.yekongle.exception.service.PersonService;
/**
* @Description 测试异常控制类
*/
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private PersonService personService;
/**
* 测试捕捉参数验证 exception
* */
@PostMapping("/validate")
public ApiResponse testValidate(@RequestBody @Validated TestDto testDto) {
return ApiResponse.success(testDto);
}
/**
* 测试捕捉业务验证 exception
* */
@PostMapping("/business")
public ApiResponse init(@RequestBody Person person) {
Map resultMap = personService.register(person);
return ApiResponse.success(resultMap);
}
/**
* 测试捕捉系统 exception
* */
@PostMapping("/error")
public ApiResponse testValidate() {
TestDto testDto = null;
log.info("Result:" + testDto.toString());
return ApiResponse.success(null);
}
}
运行演示
运行项目,用 postman 测试
访问 http://127.0.0.1:8080/test/validate
访问 http://localhost:8080/test/business
访问 http://localhost:8080/test/error
可见,全局异常配置生效,并返回了自定义的异常信息。
项目已上传至 Github: https://github.com/yekongle/springboot-code-samples/tree/master/springboot-exception-sample , 希望对小伙伴们有帮助哦。