当服务端发生异常(Exception)时,我们往往不希望程序异常终止,也不希望暴露过多的异常信息给客户端,这时候就需要一个统一的异常处理机制,来保证客户端能够收到友好的提示。
异常(exception) 是程序运行过程中发生的事件, 该事件可以中断程序指令的正常执行流程.
1)当Java程序运行时出现问题时,系统能够自动检测到,并立即生成一个与该问题对应的异常对象;
2)然后把该异常对象提交给Java虚拟机;
3)Java虚拟机会自动寻找相应的处理代码来处理这个异常,如果没有找到,则程序终止;
4)为了避免程序异常终止,我们可以编写代码来捕获和处理可能出现的异常。
实现方法
当前比较流行的统一异常处理方式为:
1) 自定义异常类,用于收敛已知异常;
2)使用@ControllerAdvice和@ExceptionHandler编写异常处理Handler,实现处理Interceptor内部(包括controller层、Service层、Dao层以及Spring系统内定义的部分)异常。Springboot执行流程参见:《Springboot工作原理》
3)返回统一封装好的更加友好的提示信息给客户端。
自定义异常类
@Getter
public class ResponseException extends RuntimeException {
private ResponseStatus responseStatus;
private String request;
public ResponseException(ResponseStatus responseStatus) {
super(responseStatus.getEc() + "," + responseStatus.getEm());
this.responseStatus = responseStatus;
}
public ResponseException(ResponseStatus responseStatus, String request) {
this(responseStatus);
this.request = request;
}
public ResponseException(ResponseStatus responseStatus, Throwable cause) {
super(responseStatus.getEc() + "," + responseStatus.getEm(), cause);
this.responseStatus = responseStatus;
}
public ResponseException(ResponseStatus responseStatus, Throwable cause, String request) {
this(responseStatus, cause);
this.request = request;
}
}
Response状态码协议
@Getter
public enum ResponseStatus {
/**
* 成功
*/
SUCCESS(0, "OK"),
/**
* 客户端请求的语法错误,服务器无法理解
*/
BAD_REQUEST(400, "Bad Request"),
/**
* 服务器内部错误
*/
INTERNAL_SERVICE_ERROR(500, "Internal Server Error"),
private int ec;
private String em;
ResponseStatus(int ec, String em) {
this.ec = ec;
this.em = em;
}
public static ResponseStatus getResponseStatus(int ec) {
for (ResponseStatus status : ResponseStatus.values()) {
if (status.getEc() == ec) {
return status;
}
}
return null;
}
@Override
public String toString() {
return "[" + ec + "]" + "[" + em + "]";
}
}
编写异常处理Handler
异常处理Handler一般干三件事:异常监控统计、记录异常日志、返回统一封装好的更加友好的提示信息给客户端。
1)Controller层、service层、dao层抛出的异常,捕获后可以直接return封装好的提示信息(ResponseDto)给客户端;
2)但是,进入Controller之前发生的异常(例如 Filter异常),这种异常的返回需要操作I/O流来返回。
例如,某个Controller有一个Post方法,接收一个json对象,如果实际传输的json对象格式和需要的json对象不一致;
消息进入Controller之前就回抛出序列化异常(HttpMessageNotReadableException),这个异常Controller层是捕获不到的;
但是我们希望捕获该异常,并且返回自己定义的ResponseDto对象,这时便可以使用ServletOutputStream的write方法返回。
@Slf4j
@ControllerAdvice
public class ResponseExceptionHandler {
/**
* 客户端全局异常捕捉处理
* 捕获自定义异常 ResponseException
*/
@ResponseBody
@ExceptionHandler(value = {ResponseException.class})
public ResponseDto<Map<String, Object>> handleException(ResponseException e, HttpServletRequest request, ServletResponse response) {
if (ResponseStatus.BAD_REQUEST == e.getResponseStatus()) {
// 异常计数
AppStateMonitor.BAD_REQUEST_COUNTER.inc();
// 打印异常日志
LogUtil.error(log, e, "{0}[{1}][{2}]", ResponseStatus.BAD_REQUEST.toString(), request.getRequestURI(), e.getRequest());
// 返回统一封装好的提示给客户端
return new ResponseDto(ResponseStatus.BAD_REQUEST.getEc(), ResponseStatus.BAD_REQUEST.getEm(), null);
} else if (ResponseStatus.INTERNAL_SERVICE_ERROR == e.getResponseStatus()) {
AppStateMonitor.PROC_FAIL_COUNTER.inc();
LogUtil.error(log, e, "{0}[{1}][{2}]", ResponseStatus.INTERNAL_SERVICE_ERROR.toString(), request.getRequestURI(), e.getRequest());
return new ResponseDto(ResponseStatus.INTERNAL_SERVICE_ERROR.getEc(), ResponseStatus.INTERNAL_SERVICE_ERROR.getEm(), null);
}
return null;
}
/**
* 客户端全局异常捕捉处理
* 捕获 Controller 外层、Interceptor 内层 异常,下面异常是 SpringFramework 中定义的异常
* HttpMessageNotReadableException
* MissingServletRequestParameterException
* MaxUploadSizeExceededException
* HttpRequestMethodNotSupportedException
*/
@ExceptionHandler(value = {HttpMessageNotReadableException.class, MissingServletRequestParameterException.class, MaxUploadSizeExceededException.class, HttpRequestMethodNotSupportedException.class})
public void springFrameworkException(Exception e, HttpServletRequest request, ServletResponse response) throws Exception {
AppStateMonitor.PROC_FAIL_COUNTER.inc();
AppStateMonitor.ERR_CLI_COUNTER.inc();
LogUtil.error(log, "{0}[{1}][{2}]", ResponseStatus.BAD_REQUEST.toString(), request.getRequestURI(), e.getMessage());
if (e instanceof MissingServletRequestParameterException) {
AppStateMonitor.PARAM_ERR_COUNTER.inc();
} else if (e instanceof HttpMessageNotReadableException) {
AppStateMonitor.SERIAL_ERR_COUNTER.inc();
} else if (e instanceof MaxUploadSizeExceededException) {
AppStateMonitor.FILE_SIZE_LIMIT_COUNTER.inc();
} else if (e instanceof HttpRequestMethodNotSupportedException) {
AppStateMonitor.PARAM_ERR_COUNTER.inc();
}
// 使用ServletOutputStream的write方法返回异常处理信息
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream servletOutputStream = response.getOutputStream();
servletOutputStream.write(new ServletFilterDto(ResponseStatus.BAD_REQUEST.getEc(), ResponseStatus.BAD_REQUEST.getEm()).toString().getBytes());
}
}
定义传输层数据传输对象
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseDto {
/**
* 状态码
*/
private int ec;
/**
* 提示信息
*/
private String em;
public BaseDto(int ec, String em) {
this.ec = ec;
this.em = em;
}
@Override
public String toString() {
return JsonUtil.toJSON(this);
}
}
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ServletFilterDto extends BaseDto {
/**
* 时间戳
*/
protected long timesec;
public ServletFilterDto(int ec, String em) {
super(ec, em);
this.timesec = System.currentTimeMillis();
}
@Override
public String toString() {
return JsonUtil.toJSON(this);
}
}
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseDto<T> extends ServletFilterDto {
/**
* 返回数据
*/
private T data;
public ResponseDto(int ec, String em) {
super(ec, em);
}
public ResponseDto(int ec, String em, T data) {
this(ec, em);
this.data = data;
}
@Override
public String toString() {
return JsonUtil.toJSON(this);
}
}
调用示例
@Slf4j
@RestController
@RequestMapping("/api/exception")
public class ExceptionController extends BaseController {
@PostMapping("/controller")
public ResponseDto<String> controller() {
String requestBody = getRequestBody(request);
if (StringUtils.isEmpty(requestBody)) {
throw new ResponseException(ResponseStatus.BAD_REQUEST, ":empty request body:" + requestBody);
}
return succuss();
}
@PostMapping("/filter")
public ResponseDto<String> filter(@RequestBody Person person) {
return succuss();
}
}
Controller基类
@Slf4j
public abstract class BaseController {
/**
* 该成员变量是线程安全的
* 1.在controller中注入的request是jdk动态代理对象,ObjectFactoryDelegatingInvocationHandler的实例.
* 当我们调用成员域request的方法的时候其实是调用了objectFactory的getObject()对象的相关方法.这里的objectFactory是RequestObjectFactory.
* 2.RequestObjectFactory的getObject其实是从RequestContextHolder的threadlocal中去取值的.
* 3.请求刚进入springmvc的dispatcherServlet的时候会把request相关对象设置到RequestContextHolder的threadlocal中去.
*/
@Autowired
protected HttpServletRequest request;
/**
* 获取消息体
*/
protected String getRequestBody() {
return getRequestBody(this.request);
}
protected String getRequestBody(HttpServletRequest request) {
try {
return HttpServletHelper.getRequestBody(request);
} catch (UnsupportedEncodingException e) {
throw new ResponseException(ResponseStatus.BAD_REQUEST, e, "unsupport encoding exception");
} catch (IOException e) {
throw new ResponseException(ResponseStatus.BAD_REQUEST, e, "io exception");
} catch (Exception e) {
throw new ResponseException(ResponseStatus.BAD_REQUEST, e, "exception");
}
}
/**
* 返回成功
*/
protected <T> ResponseDto<T> succuss() {
return succuss(null);
}
protected <T> ResponseDto<T> succuss(T t) {
return new ResponseDto<>(ResponseStatus.SUCCESS.getEc(), ResponseStatus.SUCCESS.getEm(), t);
}
}
测试结果
Contorller层异常处理
异常日志
2019-09-09 12:44:06.501 ERROR 91853 --- [ XNIO-2 task-7] c.p.r.w.e.h.ResponseExceptionHandler
: [400][Bad Request][/api/exception/controller][:empty request body:]
com.pesen.roc.web.exception.ResponseException: 400,Bad Request
at com.pesen.roc.web.connectorhttp.web.controller.api.ExceptionController.controller(ExceptionController.java:28) ~[classes/:na]
...
Controller层外部Interceptor层内部异常处理
异常日志
2019-09-09 12:37:22.967 ERROR 91853 --- [ XNIO-2 task-3] c.p.r.w.e.h.ResponseExceptionHandler
: [400][Bad Request][/api/exception/filter][JSON parse error: Unexpected character ('ï' (code 239)):
was expecting comma to separate Object entries; nested exception is com.fasterxml.jackson.core.JsonParseException
: Unexpected character ('ï' (code 239)): was expecting comma to separate Object entries
at [Source: (PushbackInputStream); line: 2, column: 18]]
异常日志
2019-09-09 12:40:15.101 ERROR 91853 --- [ XNIO-2 task-5] c.p.r.w.e.h.ResponseExceptionHandler
: [400][Bad Request][/api/exception/filter][Required request body is missing
: public com.pesen.roc.core.dto.ResponseDto<java.lang.String> com.pesen.roc.web.connectorhttp.web.controller.api.ExceptionController.filter(com.pesen.roc.web.connectorhttp.web.controller.api.Person)]