前言
在实际项目中,通常会有各种异常抛出,如果我们不加以捕捉并自定义统一的response(返回信息),那么用户就可能看到莫名奇怪的错误。比如常见的 NullPointerException, 你直接返回这个错误信息,相信用户也是一头雾水。
还有就是统一捕捉异常并自定义报错信息也有利于我们排查,迅速定位问题所在。
我们先来看下如果不做全局异常配置会是怎样,如下例所示,在 controller 层的请求里加个 try catch,然后根据不同异常处理,如果有很多请求,那么每个请求都像这样加个 try catch 层,就比较繁杂,而且要捕捉的异常可能很多都相同,这样代码重复就比较多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @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) { return ResponseCode.fail(400, "Bad request."); } catch (Exception e) { return ResponseCode.error("System error"); }
return ResponseCode.success("Login success!", jsonObject); } }
|
那怎样优雅地捕捉异常呢,这就需要用到 Spring 的两个注解 @ExceptionHandler 和 @ControllerAdvice。
@ExceptionHandler 作用于方法之上,捕捉 controller 类抛出的指定的异常,例如上面的登陆请求可以简化为如下例子,这样就避免了写重复的try catch 代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @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 类继承这个类即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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,两者搭配使用,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @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 依赖如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| <?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
1 2 3 4 5 6 7 8 9 10 11 12
| package top.yekongle.exception.exception;
public class RestInputException extends RuntimeException { private static final long serialVersionUID = 1L;
public RestInputException(String message) { super(message); } }
|
ApiExceptionConfig.java, 全局异常配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| 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;
@Slf4j @ControllerAdvice public class ApiExceptionConfig {
@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public ApiResponse MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package top.yekongle.exception.dto;
import lombok.Data; import javax.validation.constraints.NotEmpty;
@Data public class TestDto { @NotEmpty(message = "名字不能为空!") private String name; @NotEmpty(message = "地址不能为空!") private String address; }
|
Person.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package top.yekongle.exception.model;
import lombok.Data; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull;
@Data public class Person { @NotEmpty(message = "姓名不能为空!") private String name; @NotNull(message = "年龄不能为空!") private Integer age; }
|
下面是结果返回相关类
ApiStatus.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package top.yekongle.exception.response;
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; }
public int getCode() { return this.code; }
public String getMsg() { return this.msg; } }
|
ApiResponse.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package top.yekongle.exception.response;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import lombok.AllArgsConstructor; import lombok.Data;
@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
1 2 3 4 5 6 7 8 9 10 11 12
| package top.yekongle.exception.service;
import top.yekongle.exception.model.Person;
import java.util.Map;
public interface PersonService { Map<String, Object> register(Person person); }
|
PersonServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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;
@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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| 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;
@Slf4j @RestController @RequestMapping("/test") public class TestController { @Autowired private PersonService personService;
@PostMapping("/validate") public ApiResponse testValidate(@RequestBody @Validated TestDto testDto) { return ApiResponse.success(testDto); }
@PostMapping("/business") public ApiResponse init(@RequestBody Person person) { Map resultMap = personService.register(person); return ApiResponse.success(resultMap); }
@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 , 希望对小伙伴们有帮助哦。