Spring全局异常处理你可能不知道的那些事儿(细而全)

异常处理主要分为两大块

进入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()));
    }
}

至此,已大功告成。如有不足之处,希望评论区留言。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
Spring框架中,可以通过全局异常处理机制来统一处理应用程序中的异常情况,避免异常信息直接暴露给用户,提高用户体验和系统的健壮性。以下是一个简单的示例来说明如何实现全局异常处理。 首先,创建一个自定义的全局异常处理器类,该类需要实现`HandlerExceptionResolver`接口。可以通过继承`AbstractHandlerExceptionResolver`类来简化实现。 ```java import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; public class GlobalExceptionHandler implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 处理异常逻辑 ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName("error"); // 设置错误页面 modelAndView.addObject("errorMsg", ex.getMessage()); // 将异常信息传递给错误页面 return modelAndView; } } ``` 接下来,需要在Spring配置文件中注册该全局异常处理器: ```xml <bean id="globalExceptionHandler" class="com.example.GlobalExceptionHandler" /> ``` 最后,可以在控制器中抛出异常,在全局异常处理器中进行统一处理: ```java @Controller public class UserController { @RequestMapping("/user/{id}") public String getUser(@PathVariable("id") int id) { // 模拟抛出异常 if (id <= 0) { throw new IllegalArgumentException("Invalid user ID"); } // ... } } ``` 当控制器方法中抛出异常时,全局异常处理器会捕获该异常并执行相应的处理逻辑。在上述示例中,异常会被处理并渲染到名为"error"的错误页面中。 这样,无论在哪个控制器方法中发生异常,都可以通过全局异常处理器进行统一的异常处理

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流沙QS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值