在项目中使用SpringMVC全局异常处理

当程序发生错误时,返回错误内容,会搞乱了调用者代码。调用者必须在调用之后即刻检查错误,不幸的是,这个步骤很容易被遗忘。
建议在发生错误时抛出异常,调用代码很整洁,绮逻辑不会被错误处理搞乱。
————–《代码整洁之道》


SpringMVC提供了一个全局异常处理机制,使用比较简单,网上也有很多介绍的文章,本文主要举例说明在我们项目组是如何使用的。

完整的引入全局异常处理机制,包含以下四个类:

  • CustomRestExceptionHandler(继承ResponseEntityExceptionHandler)用来监听处理全局异常
  • CustomException(继承RuntimeException)自定义异常类
  • ExceptionEnum 异常枚举类
  • ErrorDTO 异常响应DTO,规范异常发生时的接口返回值

CustomException和ExceptionEnum的配合使用,主要是为了统一管理所有的异常,这对异常码的统一与复用十分重要

异常处理逻辑:
1、开发人员在ExceptionEnum中定义列举所有的异常(异常码、异常信息),这样主要是方便管理所有异常码
2、在代码错误处理逻辑中,new一个CustomException并throw,创建CustomException的参数建议使用ExceptionEnum的值
3、异常如果没有没catch并处理的话,会抛到controller处理层,这时会被CustomRestExceptionHandler捕获到
4、CustomRestExceptionHandler将异常封装到ErrorDTO,并作为接口响应值返回给调用接口者

使用一个简单的工程来举例说明
工程结构

CustomRestExceptionHandler

使用@ControllerAdvice注解声明全局异常处理类,使用@ExceptionHandler注解声明处理自定义异常,使用@Override覆盖通用异常处理

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 com.markey.markeyauth.errorhandle;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {

//处理自定义异常
@ExceptionHandler(CustomException.class)
public ResponseEntity<Object> handleCustomerException(CustomException ex) {

final ErrorDTO customeError = new ErrorDTO(ex.getErrorCode(), ex.getLocalizedMessage());

return new ResponseEntity<Object>(customeError, new HttpHeaders(), ex.getHttpStatus());

}

//处理通用异常,这里举例说明如何覆盖处理 请求方法不支持的异常
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
// TODO Auto-generated method stub
final ErrorDTO customeError = new ErrorDTO(status.value(), "HttpRequestMethodNotSupported");
return new ResponseEntity<Object>(customeError, new HttpHeaders(), status);
}
}

自定义异常类

自定义异常类包括三个参数:请求响应状态码,异常码和异常信息(可根据工程实际情况修改)

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
package com.markey.markeyauth.errorhandle;

import org.springframework.http.HttpStatus;

public class CustomException extends RuntimeException {

/**
*
*/
private static final long serialVersionUID = -2486443993341553686L;

private HttpStatus httpStatus;
private int errorCode;
public CustomException(HttpStatus httpStatus, int errorCode, String message) {
super(message);
this.httpStatus = httpStatus;
this.errorCode = errorCode;

// TODO Auto-generated constructor stub
}

public CustomException(String message, int errorCode, Exception e) {

super(message, e.getCause());
// TODO Auto-generated constructor stub
}

public HttpStatus getHttpStatus() {
return httpStatus;
}
public void setHttpStatus(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
}
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
}

ExceptionEnum 异常枚举类

异常枚举类和自定义异常类配合使用,参数也是三个:请求响应状态码,异常码和异常信息

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
package com.markey.markeyauth.errorhandle;

import org.springframework.http.HttpStatus;

public enum ExceptionEnum {

LOGIN_USERNAME_ERROR(10001, "login fail", HttpStatus.BAD_REQUEST),
LOGIN_PASSWORD_ERROR(10002, "login fail", HttpStatus.BAD_REQUEST);

private int error_code;
private String error_desc;
private HttpStatus httpStatus;

ExceptionEnum(int code, String msg, HttpStatus status) {
this.error_code = code;
this.error_desc = msg;
this.httpStatus = status;
}

public int getError_code() {
return error_code;
}
public void setError_code(int error_code) {
this.error_code = error_code;
}
public String getError_desc() {
return error_desc;
}
public void setError_desc(String error_desc) {
this.error_desc = error_desc;
}
public HttpStatus getHttpStatus() {
return httpStatus;
}
public void setHttpStatus(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
}
}

异常响应DTO

统一接口返回body体的内容,方便接口调用者处理,这里定义了两个字段error_code、error_desc(对应了异常枚举值中的错误码和错误信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.markey.markeyauth.errorhandle;

public class ErrorDTO {
private int error_code;
private String error_desc;

public ErrorDTO(int error_code, String error_desc) {
super();
this.error_code = error_code;
this.error_desc = error_desc;
}
public int getError_code() {
return error_code;
}
public void setError_code(int error_code) {
this.error_code = error_code;
}
public String getError_desc() {
return error_desc;
}
public void setError_desc(String error_desc) {
this.error_desc = error_desc;
}
}

在一个简单的controller中测试一下:

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
package com.markey.markeyauth.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.markey.markeyauth.domain.User;
import com.markey.markeyauth.errorhandle.CustomException;
import com.markey.markeyauth.errorhandle.ExceptionEnum;
import com.markey.markeyauth.service.UserService;

@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
UserService userService;
/*
* 访问这个接口时抛出一个自定义异常,表示用户名错误
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
public List<User> getAllUser()
{
throw new CustomException(ExceptionEnum.LOGIN_USERNAME_ERROR.getHttpStatus(),
ExceptionEnum.LOGIN_USERNAME_ERROR.getError_code(),
ExceptionEnum.LOGIN_USERNAME_ERROR.getError_desc());
}
}

测试一下

启动springboot
使用postman访问接口get http://localhost:8080/users/ 预期结果:返回json,包含error_code和error_desc

效果如下:

工程结构

使用post http://localhost:8080/users/ 预期结果:返回json,包含error_code和error_desc
因为controller自定义了get方法,所以post请求是方法的,在CustomRestExceptionHandler覆盖了原生的请求方法非法处理

效果如下:

工程结构

备注

上述举例主要是针对restful接口的异常处理,如果想要在捕获异常后返回ModelAndView,而不是json消息,可以让CustomRestExceptionHandler继承HandlerExceptionResolver,并覆盖resolveException()方法