欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot全局异常处理(续)

程序员文章站 2022-06-03 17:49:14
...

在上一篇《SpringBoot全局异常处理》中介绍了两种处理全局异常的方案,今天我们继续来探讨一下如何更好地处理异常,比如:404之类的错误。

首先来怀念一下默认的错误页面,默哀三分钟。
SpringBoot全局异常处理(续)

哈哈,SpringBoot默认的错误页面就是一个白底页面加了一些错误信息。我们今天要解决的问题就是在之前处理异常的基础上来修改默认的错误页面,即自定义诸如404、500酱紫的错误页面,关于处理这些问题的博客数不胜数,但是经我测试了一些之后,不是太满意,所以另辟蹊径,整理一下。

第一种方案:配置:spring.mvc.throw-exception-if-no-handler-found
这种方式是网上提到的比较多的,可以将该参数设置为true,当请求找不到对应的HandlerMethod时抛出NoHandlerFoundException异常,这样通过我们上一篇博客的处理方式就可以捕获到该异常,然后进行处理,但是很多情况下,该配置并不会生效。依然会显示默认的错误页面,针对这种方案,我总结了一下,如果需要还配置生效抛出异常。
我们需要做的就是同时配置spring.resources.add-mappings: false,这样的话当出现404错误的时候就会抛出异常,我们就可以针对异常进行处理。
但是使用这种做法后,我们会发现,页面上的静态资源无法加载了,为啥呢?因为我们配置了add-mappings为false,导致了静态资源不处理,所以当我们还要处理静态资源的时候,我们就可以默认add-mappings配置为true,不进行修改,通过同时配置spring.tatic-path-patternspring.resources.static-locations实现指定的资源路径交由SpringBoot处理,这样可以解决大部分问题,但是有个小毛病就是,当我们的访问我们配置的资源路径出现404的时候,依然会显示默认页面,所以总体来说,这种方案不太理想。

第二种方案:实现ErrorController
通过自定义ErrorController,我们可以重写默认的错误逻辑,举个栗子:

package com.jianggujin.hr.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class BaseErrorController implements ErrorController
{
   @Override
   public String getErrorPath()
   {
      return "error";
   }

   @RequestMapping(value = "/error")
   public String error(HttpServletRequest request, HttpServletResponse response)
   {
      return getErrorPath();
   }
}

这样,当系统出现错误的时候,就会执行我们自己的Controller,从而跳转到我们自定义的error页面。这只是最简单的实现方式,有时候我们需要针对不同的状态码,跳转到不同的页面,那么我们可以添加我们实际的逻辑如下:

package com.jianggujin.hr.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class BaseErrorController implements ErrorController
{
   @Override
   public String getErrorPath()
   {
      return "error";
   }

   @RequestMapping(value = "/error")
   public String error(HttpServletRequest request, HttpServletResponse response)
   {
      if (response.getStatus() == HttpStatus.NOT_FOUND.value())
      {
         return "404";
      }
      return getErrorPath();
   }
}

使用这种方式,可操作性更大一些点,也没有特殊的配置需要注意。我们还已将这种方式与前一篇文章结合起来,出现404状态码时直接抛出NoHandlerFoundException异常,然后统一拦截处理。

综合本篇文章与之前的《SpringBoot全局异常处理》,只是为大家提供了处理异常以及错误页面的思路,大家可以*发挥进行组合扩展,下面附上一份针对前一篇文章中的方案二中的AbstractHandlerExceptionResolver抽象类,增加NoHandlerFoundException异常的处理。

package com.jianggujin.hr.util;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;

/**
 * 异常解析
 * 
 * @author jianggujin
 *
 */
public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver
{
   protected static final Log logger = LogFactory.getLog(AbstractHandlerExceptionResolver.class);

   @Override
   public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
         Exception e)
   {
      ModelAndView modelView = null;
      try
      {
         onException(e);
      }
      catch (Exception e1)
      {
         logger.error("ExceptionResolver onException", e1);
      }
      try
      {
         // 404 处理
         if (e instanceof NoHandlerFoundException)
         {
            modelView = onPageNotFoundError(request, response, e);
            return modelView;
         }
      }
      catch (Exception e1)
      {
         logger.error("ExceptionResolver onPageNotFoundError", e1);
      }
      // 处理器方法,才会执行异常处理
      if (handler instanceof HandlerMethod)
      {
         HandlerMethod method = (HandlerMethod) handler;

         boolean isApi = (method.hasMethodAnnotation(ResponseBody.class)
               || method.getBeanType().isAnnotationPresent(RestController.class));
         try
         {
            // api
            if (isApi)
            {
               modelView = onApiError(request, response, e, method);
            }
            else
            {
               modelView = onPageError(request, response, e, method);
            }
         }
         catch (Exception e1)
         {
            logger.error("ExceptionResolver onError", e1);
         }
      }
      return modelView;
   }

   /**
    * 出现异常时回调
    * 
    */
   protected abstract void onException(Exception e) throws Exception;

   /**
    * api请求出错
    * 
    */
   protected abstract ModelAndView onApiError(HttpServletRequest request, HttpServletResponse response, Exception e,
         HandlerMethod method) throws Exception;

   /**
    * 页面请求出错
    * 
    */
   protected abstract ModelAndView onPageError(HttpServletRequest request, HttpServletResponse response, Exception e,
         HandlerMethod method) throws Exception;

   /**
    * 页面未找到
    * 
    * @param request
    * @param response
    * @param e
    * @return
    */
   protected abstract ModelAndView onPageNotFoundError(HttpServletRequest request, HttpServletResponse response,
         Exception e);
}

我本人比较喜欢这种全局异常处理结合实现ErrorController的处理方式。

2018年03月02日补充开始

在实际使用中发现如果使用DeleteMapping请求,当出现错误的时候异常处理会比较诡异,重定向的时候无效,如果禁用ErrorPageFilter,出现异常的时候会提示405,所以针对异常,我们还可以通过AOP进行处理,这里针对DeleteMapping注解的方法进行特殊的处理,以下示例仅为DEMO。

package com.jianggujin.hr;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping;

import com.jianggujin.hr.model.result.BaseResult;
import com.jianggujin.hr.util.ErrorException;

/**
 * 全局切面,处理{@link DeleteMapping}注解方法,出现异常时会很诡异,用切面处理异常
 * 
 * @author jianggujin
 *
 */
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1) // 如果设置为HIGHEST_PRECEDENCE会报错
public class WebAspect {
   protected static final Log logger = LogFactory.getLog(WebAspect.class);

   @Pointcut("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
   public void pointcut() {
   }

   /**
    * 环绕通知<br/>
    * 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
    * 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
    */
   @Around("pointcut()")
   public Object doAroundAdvice(ProceedingJoinPoint joinPoint) {
      BaseResult result = null;
      try {
         Object retVal = joinPoint.proceed();
         if (retVal instanceof BaseResult) {
            result = (BaseResult) retVal;
         } else if (retVal != null) {
            result = new BaseResult(result);
         }
      } catch (Throwable throwable) {
         if (throwable instanceof ErrorException) {
            ErrorException customException = (ErrorException) throwable;
            result = new BaseResult(customException.getError(), customException.getData());
         } else {
            logger.error("app exception", throwable);
            result = BaseResult.ERROR;
         }
      }
      return result;
   }
}

2018年03月02日补充结束