异常处理主要分为两大块
进入Controller层的:
此部分可以借助Spring提供的全局异常处理机制来处理
以及进入Controller之前的:
如Filter中的异常,此部分异常无法到达Controller层,因此Spring提供的全局异常处理机制无法捕获。此部分处理有两种方式:网上大部分处理的方式为,在Filter中抛出异常的地方,重定向到指定的一个Controller层去,这样就可以借助Spring的全局异常处理器(@ControllerAdvice)来进行处理,此处不再赘述。主要谈的是第二种,即实现 ErrorController 接口的异常处理。
1.全局异常处理
借助Spring提供的注解 @ControllerAdvice
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
/**
* 系统全局异常处理
*
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
public static final String DEFAULT_ERROR_CODE = "E";
private static final String DEFAULT_ERROR_MSG = "业务繁忙,请稍后再试";
// 自定义的校验注解
private final static Set<String> CUSTOMER_VALID_ANNOTATION = new HashSet<>();
/**
* 参数校验异常处理
* <p>
* 主要拦截 使用@Valid或@Validated注解对参数校验后的字段的统一处理
* 通过{@linkplain BindingResult}拿到错误信息,错了完成统一返回
*
* @param ex
* @return
*/
@ExceptionHandler(BindException.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
public ResultVO<?> handleException(BindException ex) {
log.error("请求参数错误", ex);
BindingResult bindingResult = ex.getBindingResult();
StringBuilder msg = new StringBuilder();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
String annName = fieldError.getCode();
if (!CUSTOMER_VALID_ANNOTATION.contains(annName)) {
msg.append("[").append(fieldError.getField()).append("]");
}
msg.append(fieldError.getDefaultMessage()).append(" ");
}
return ResultVO.failed(ResultCode.P00002, msg.toString());
}
/**
* 处理Servlet异常
*
* @param ex
* @return
*/
@ExceptionHandler(ServletException.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
public ResultVO<?> handleServletException(ServletException ex) {
log.error("请求方式异常", ex);
// 文件为空
if (ex instanceof MissingServletRequestPartException) {
MissingServletRequestPartException e = (MissingServletRequestPartException) ex;
String message = String.format("[%s]参数不能为空", e.getRequestPartName());
return ResultVO.failed(ResultCode.P00000, message);
}
// 请求方式异常
if (ex instanceof HttpRequestMethodNotSupportedException) {
HttpRequestMethodNotSupportedException e = (HttpRequestMethodNotSupportedException) ex;
List<String> supportMethods = Arrays.asList(Optional.ofNullable(e.getSupportedMethods()).orElse(new String[0]));
String message = String.format("不支持[%s]请求方式,仅支持%s", e.getMethod(), supportMethods);
return ResultVO.failed(ResultCode.B00000, message);
}
// 参数错误
if (ex instanceof MissingServletRequestParameterException) {
MissingServletRequestParameterException e = (MissingServletRequestParameterException) ex;
String message = String.format("[%s{%s}]不能为空", e.getParameterName(), e.getParameterType());
return ResultVO.failed(ResultCode.B00000, message);
}
return ResultVO.failed(ResultCode.E, DEFAULT_ERROR_MSG);
}
/**
* shiro异常处理
*/
@ExceptionHandler(ShiroException.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
public ResultVO<?> handleShiroException(ShiroException ex) {
String message;
if (ex instanceof UnauthorizedException) {
message = "无权限操作";
log.error("无权限操作 - {}", message, ex);
} else if (ex instanceof AuthorizationException) {
message = "无权限操作";
log.error("权限认证异常 - {}", message, ex);
} else {
message = ex.getMessage();
log.error("权限认证异常 - {}", message, ex);
}
return ResultVO.failed(ResultCode.U00004, message);
}
/**
* 参数格式异常处理
*
* @param ex
* @return
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
public ResultVO<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
String message = ex.getMessage();
log.error("参数格式错误:{}", message, ex);
return ResultVO.failed(ResultCode.P00004);
}
/**
* 参数格式异常处理
*
* @param ex
* @return
*/
@ExceptionHandler({IllegalStateException.class})
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
public ResultVO<?> handleIllegalStateException(IllegalStateException ex) {
Throwable cause;
String message = null;
if ((cause = ex.getCause()) != null) {
message = cause.getMessage();
}
message = message == null ? ex.getMessage() : message;
log.error("请求异常:{}", message, ex);
return ResultVO.failed(ResultCode.B00000, message);
}
/**
* 业务异常处理
*
* @param ex
* @return
*/
@ExceptionHandler({BusinessException.class, PlatformException.class})
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
public ResultVO<?> handleHttpMessageNotReadableException(PlatformException ex) {
String message = ex.getMessage();
log.error("参数格式错误:{}", message, ex);
return ResultVO.failed(ex.getResultCode(), message);
}
/**
* 其他全局异常处理
*
* @param request
* @param response
* @param ex
* @param handle
* @return
*/
@ExceptionHandler(Throwable.class)
@ResponseStatus(value = HttpStatus.OK)
public ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Throwable ex, HandlerMethod handle) {
ModelAndView modelAndView;
if (handle.getBean().getClass().isAnnotationPresent(RestController.class) || handle.hasMethodAnnotation(ResponseBody.class)) {
modelAndView = new ModelAndView(new MappingJackson2JsonView());
this.handleSpecialException(ex, handle, modelAndView);
modelAndView.addObject("data", null);
} else {
modelAndView = new ModelAndView();
modelAndView.setViewName("error/error");
this.handleSpecialException(ex, handle, modelAndView);
PrintWriter writer = new PrintWriter(new StringWriter());
ex.printStackTrace(writer);
}
return modelAndView;
}
private void handleSpecialException(Throwable e, HandlerMethod handle, ModelAndView modelAndView) {
log.error("全局异常", e);
modelAndView.addObject("code", DEFAULT_ERROR_CODE);
modelAndView.addObject("message", DEFAULT_ERROR_MSG);
}
}
2.全局异常无法处理的异常处理(Filter中Controller之前的异常)本案例基本可开箱即用
此部分为重头戏,因为在网上很少找到资料,或者找到的都是只言片语,要么无法使用,要么不全面。
SPringBoot官方已提供了一种处理机制,参考 org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
,采用@Bean的方式注入:
SpringBoot官方实现示例
package org.springframework.boot.autoconfigure.web.servlet.error;
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
private final ServerProperties serverProperties;
public ErrorMvcAutoConfiguration(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
//......
}
org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController,为什么采用这种方式,是因为要注入额外的3个属性:
org.springframework.boot.web.servlet.error.ErrorAttributes errorAttributes,
org.springframework.beans.factory.ObjectProvider<org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver> errorViewResolvers;
org.springframework.boot.autoconfigure.web.ServerProperties serverProperties
//注入serverProperties的目的是为了获得ErrorProperties
org.springframework.boot.autoconfigure.web.ErrorProperties errorProperties = serverProperties.getError()
这就是为什么不能直接使用 @Conponent只能的注入了,因为 ErrorProperties 及 ErrorAttributes 无法使用有参构造或@Autowired注入。
说清楚了。参考官方的注入BasicErrorController的方式,注入我们自定义的Bean
第一步:定义继承自BasicErrorController的实现处理类(为了复用别人已经实现了的code,也可以实现实现接口 org.springframework.boot.web.servlet.error.ErrorController)
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
import xin.cosmos.basic.define.ResultVO;
import xin.cosmos.basic.exception.BusinessException;
import xin.cosmos.basic.exception.PlatformException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 过滤器/servlet异常处理
* <p>
* 主要处理经过过滤器但尚未到达controller的异常
* <p>
* {@linkplain GlobalExceptionHandler} 全局异常处理没法处理过虑器中抛出的异常
* 和执行顺序有关:
* <p>
* filter -> interceptor -> controllerAdvice -> aspect -> controller
* <p>
* 当controller返回异常时,也会按照controller -> aspect -> controllerAdvice -> interceptor -> filter来依次抛出
* <p>
* 注意:此方法不能直接在类上使用{@linkplain org.springframework.stereotype.Controller}
* <p>
* 或 {@linkplain org.springframework.web.bind.annotation.RestController} 标注,
* <p>
* 原因是{@linkplain ErrorProperties}和{@linkplain ErrorAttributes}无法注入。
* <p>
* 采用如同SpringBoot注入{@linkplain BasicErrorController}的注入方式一样,采用{@linkplain org.springframework.context.annotation.Bean}的方式注入。
* <p>
* 可参考{@linkplain ErrorMvcAutoConfiguration}
* <p>
* 该类在{@linkplain xin.cosmos.basic.config.ServletErrorConfiguration}中注入spring容器
*/
@Slf4j
@RequestMapping("${server.error.path:${error.path:/error}}")
public class ServletErrorHandler extends BasicErrorController {
private static final String CODE_NAME = "code";
private static final String MSG_NAME = "message";
private static final String DATA_NAME = "data";
public ServletErrorHandler(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorProperties, errorViewResolvers);
}
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
WebRequest webRequest = new ServletWebRequest(request);
final Map<String, Object> body = getErrorAttributes(request, getAllErrorAttributeOptions());
log.error("Request Path: {}, Servlet Error: {}", body.get("path"), body.get("trace"));
// 异常错误处理
ResultVO<Object> defaultError = ResultVO.failed((String) body.get("error"));
Map<String, Object> errorMap = new LinkedHashMap<>();
String exception = (String) body.get("exception");
if (PlatformException.class.getTypeName().equals(exception) || BusinessException.class.getTypeName().equals(exception)) {
errorMap.put(CODE_NAME, defaultError.getCode());
errorMap.put(MSG_NAME, body.get("message"));
errorMap.put(DATA_NAME, defaultError.getData());
return new ResponseEntity<>(errorMap, HttpStatus.OK);
}
errorMap.put(CODE_NAME, defaultError.getCode());
errorMap.put(MSG_NAME, defaultError.getMessage());
errorMap.put(DATA_NAME, defaultError.getData());
return new ResponseEntity<>(errorMap, HttpStatus.OK);
}
@Override
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML));
Map<String, Object> map = new HashMap<>();
map.put("title", status.getReasonPhrase() + "page");
map.put("code", model.get("status"));
map.put("message", model.get("error"));
response.setStatus(status.value());
log.error("{}", model);
return new ModelAndView("error/error", map, HttpStatus.OK);
}
ErrorAttributeOptions getAllErrorAttributeOptions() {
return ErrorAttributeOptions.of(ErrorAttributeOptions.Include.EXCEPTION, ErrorAttributeOptions.Include.MESSAGE, ErrorAttributeOptions.Include.BINDING_ERRORS, ErrorAttributeOptions.Include.STACK_TRACE);
}
}
第二步:采用@Bean的方式将定义的类注入Spring容器(此处参考SpringBoot的实现方式,避免踩坑)
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import xin.cosmos.basic.handler.ServletErrorHandler;
import javax.servlet.Servlet;
import java.util.stream.Collectors;
/**
* 自定义Servlet异常处理配置类
* 注入Bean的形式参考{@linkplain ErrorMvcAutoConfiguration}
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
// 在主WebMvcAutoConfiguration之前加载,以便错误视图可用
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ServerProperties.class, WebMvcProperties.class})
public class ServletErrorConfiguration {
private final ServerProperties serverProperties;
public ServletErrorConfiguration(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
/**
* Servlet自定义异常处理器
* @param errorAttributes
* @param errorViewResolvers
* @return
*/
@Bean
public ServletErrorHandler servletFilterErrorController(
ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new ServletErrorHandler(errorAttributes,
this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
}
至此,已大功告成。如有不足之处,希望评论区留言。