0%

Spring Boot2 实战系列之 exception 配置

前言

在实际项目中,通常会有各种异常抛出,如果我们不加以捕捉并自定义统一的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) {
// 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 代码。

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;

/**
* @Description 自定义异常
* */
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;

/**
* @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

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 lombok 注解,自动生成 getter setter 方法
* @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;

/**
* @Description 业务实体类
* */
@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;

/**
* @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

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;

/**
* @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

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;

/**
* @Description 业务接口
*/
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;

/**
* @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

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;

/**
* @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 , 希望对小伙伴们有帮助哦。

坚持原创技术分享,您的支持将鼓励我继续创作!