0 results found
DeCo
SpringBoot异常处理汇总

前言

在实际开发中,我们会遇到很多异常,在发生异常的时候Spring Boot默认提供了错误页面展示给用户。看似比较友好,其实页面很丑。
上面讲的是做页面开发的时候遇到的问题,还有一种情况就是用来开发Rest接口,当错误的时候我们希望返回给用户的是我们接口的标准格式,不是返回一段html代码。
接下来分别给大家介绍下解决方案:

页面处理

首先我们来看页面错误的处理情况,当我们的程序内部报错的时候或者访问的页面找不到的时候,我们可以看到下面的错误页面:

先查看SpringBoot官方文档

SpringBoot官方文档

大致意思就是Spring Boot本身是内置了一个异常处理机制的, 会判断请求头的参数来区分要返回 JSON 数据还是错误页面. 源码为: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController, 他会处理/error 请求. 核心处理代码如下:

@RequestMapping(
    produces = {"text/html"}
)
// 请求头是text/html则找到错误页面并返回
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    // 1.获取Http状态码
    HttpStatus status = this.getStatus(request);
    // 2.调用getErrorAttributes获取响应的map集合
    Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    // 3.设置响应头的状态码
    response.setStatus(status.value());
    // 4. 获取错误页面的路径
    ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
    // 5. 没有获取到资源则返回默认error页面
    return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
     // 调用 getErrorAttributes 获取响应的 map 结果集.
    Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
    // 获取 HTTP 错误状态码
    HttpStatus status = this.getStatus(request);
    // 返回给页面 JSON 信息.
    return new ResponseEntity(body, status);
}

这两个方法的共同点是: 他们都调用了 this.getErrorAttributes(…) 方法来获取响应信息.

然后来看看他默认情况下对于 AJAX 请求和 HTML 请求, 分别的返回结果是怎样的:
html错误页面
Ajax错误页面
对于返回错误页面, 其中还调用了一个非常重要的方法: this.resolveErrorView(request, response, status, model) 方法,分析核心代码

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
		Map<String, Object> model) {
	// 通过 HTTP 错误状态码获取ModelAndView
	ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
	// 如果没有获取到,并且状态码包含SERIES_VIEWS键名中(SERIES_VIEWS -> HttpStatus内部的枚举Series的Map)
	if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
		// 通过key => status.series() 的值(4xx,5xx)获取视图
		modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
	}
	return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
	// 加上 error/ 路径
	String errorViewName = "error/" + viewName;
	// 先通过模板引擎获取视图
	// 遍历所有Sprng容器中 TemplateAvailabilityProvider 的实现类(JspTemplateAvailabilityProvider,TemplateAvailabilityProvider。。。)一个一个获取
	// 例如是使用JSP模板, 配置如:spring.mvc.view.prefix=/WEB-INF/page/ spring.mvc.view.suffix=.jsp
	// 查找路径 classpath:/WEB-INF/page/404.jsp 。。。
	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
			.getProvider(errorViewName, this.applicationContext);
	if (provider != null) {
		return new ModelAndView(errorViewName, model);
	}
	// 通过SpringBoot的默认资源获取视图
	return resolveResource(errorViewName, model);
}

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
	// "classpath:/META-INF/resources/" -> "classpath:/resources/" -> "classpath:/static/" -> "classpath:/public/
	// 如果配置了  spring.resources.static-locations=/aaa,/bbb 则使用自己配置的
	for (String location : this.resourceProperties.getStaticLocations()) {
		try {
			Resource resource = this.applicationContext.getResource(location);
			// 例如 classpath:/META-INF/resources/404.html
			resource = resource.createRelative(viewName + ".html");
			if (resource.exists()) {
				return new ModelAndView(new HtmlResourceView(resource), model);
			}
		}
		catch (Exception ex) {
		}
	}
	return null;
}

总结:它的作用就是根据 HTTP 状态码来去找错误页面, 如 500 错误会去找 /error/500.html, 403 错误回去找 /error/403.html, 如果找不到则再找 /error/4xx.html 或 /error/5xx.html 页面. 还找不到的话, 如果都没有配置, 则会使用 Spring Boot 默认的页面. 即:
html错误页面
这样就可以通过在资源目录下增加404.html、500.html、5xx.html。。。等来达到自定义错误页面的效果

REST接口处理

在开发rest接口时,我们往往会定义统一的返回格式,列如:

{
    "errcode": "0",
    "errmsg": "ok",
    "data": {
        "city_name": "深圳",
        "area_code": "0755",
        "servicer": "CTCC",
        "province_name": "广东"
    }
}

但是如果调用方请求我们的api时把接口地址写错了,浏览器请求就会得到一个404错误页面,ajax请求会返回默认格式json数据,最友好的方式就是返回固定的JSON格式

{
    "timestamp": "2019-04-22T14:48:37.416+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/rest/test11"
}

SpringBoot官方文档

可以使用 @ControllerAdvice注解自定义要为特定控制器或异常类型返回的JSON文档

  • 定义返回JSON数据实体类
    @Data
    public class BaseResponse implements Serializable {
        private static final long serialVersionUID = 1L;
        /**
         * 状态码:0-成功,其他-失败
         */
        public String errcode = "0";
        /**
         * 成功返回:ok,失败返回具体原因
         */
        public String errmsg = "ok";
        /**
         * 返回的数据结果集
         */
        public Object data;
    
        public BaseResponse() {
        }
        ....
    }
  • 创建自定义Controller异常捕获类
    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        //捕获指定异常
        @ExceptionHandler(ArithmeticException.class)
        @ResponseBody
        public BaseResponse arithmeticExceptionHandler(ArithmeticException e){
            BaseResponse response = new BaseResponse();
            response.setErrmsg("ArithmeticException");
            response.setErrcode("-1");
            return response;
        }
    
        //捕获其他异常
        @ResponseBody
        @ExceptionHandler(Exception.class)
        public BaseResponse exceptionHandler(Exception e){
            BaseResponse response = new BaseResponse();
            response.setErrmsg("其他异常");
            response.setErrcode("-1");
            return response;
        }
    }

    使用 @ControllerAdvice + @ExceptionHandler 会发现能够捕获到Controller中抛出的异常,但是没有找到对应的handler是无法捕获到,仍然是走的BasicErrorController的errorHtml或者error方法。下面来分析SpringBoot中异常的分类

异常的分类

在一个Spring Boot项目中,我们可以把异常分为两种,第一种是请求到达Controller层之前,第二种是到达Controller层之后项目代码中发生的错误。
而第一种又可以分为两种错误类型:1. 路径错误 2. 类似于请求方式错误,参数类型不对等类似错误。
异常的分类
@ExceptionHandler只捕获到第二种异常,如果想要拦截404错误的话,需要在spring boot中做如下设置

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

这两行配置的主要意思是告诉spring,如果你请求的地址,没有找到对应的handler,则抛出异常,默认配置是fase,同时也要关掉静态资源的映射,不然并不会起作用.

// 捕获404异常
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseBody
public BaseResponse noHandlerFoundException(NoHandlerFoundException e){
    BaseResponse response = new BaseResponse();
    response.setErrmsg("NoHandlerFoundException");
    response.setErrcode("-1");
    return response;
}

按照以上的配置,我们可以轻易的将404错误也变成一个异常,然后走统一的异常处理方式,但是这样做是有代价的,就是也会关闭掉spring boot的默认资源mapping功能.
(大家可查阅spring boot官方文档,来了解如何处理静态资源)
测试时又发现了一个问题,页面请求404会被捕获并返回自定义处理的结果,只能是页面或者json数据,这种方式适用于前端分离的项目。
如果项目中有html页面请求做统一处理,又需要对ajax请求返回固定的JSON格式数据这种方式就无法满足了
我将这种需求分成两种情况

  • A.两种Controller分别处理html页面请求和处理ajax请求
  • B.在一个Controller中有处理html页面请求和处理ajax请求的Mapping(应该很少吧。)

A情况解决方案

这种种情况的解决可以使用注解的方式解决,通过注解的方式实现处理异常主要有以下两种方式:

  • 1 @ControllerAdvice+@ExceptionHandler:配置对全局异常进行处理(ControllerAdvice还有很多配置这里就不讲了)
  • 2 @Controller + @ExceptionHandler:配置对当前所在Controller的异常进行处理

    @ExceptionHandler:此注解注解到类的方法上,当此注解里定义的异常抛出时,此方法会被执行。如果@ExceptionHandler所在的类是@Controller,则此方法只作用在此类。如果@ExceptionHandler所在的类是@ControllerAdvice,则此方法会作用在全局。全局前面有演示

@RestController
@RequestMapping("/emp")
public class EmployeeExController {

    @GetMapping("/{id}")
    public BaseResponse getEmployee(@PathVariable("id") int id) throws Exception {
        int i = 1 / 0;
        return new BaseResponse();
    }
    /**
     * 此方法只处理本类抛出的 ArithmeticException 异常
     */
    @ExceptionHandler(ArithmeticException.class)
    public BaseResponse handleEmployeeNotFoundException(Exception e) {
        return new BaseResponse("-1", e.getMessage(),"类级别异常处理");
    }
}

####测试
执行URL:
http://127.0.0.1:8080/emp/1
输出

{
"errcode": "-1",
"errmsg": "/ by zero",
"data": "类级别异常处理"
}

并且类级别异常处理要优先与全局处理.这样我们写两个基类。分别进行异常统一处理

public class BaseController {
    /**
     * 统一异常处理
     */
    @ExceptionHandler(value = {Exception.class})
    public String exceptionHandler(Exception e) {
        return "/error";
    }
}
public class BaseRestController {
    /**
     * 统一异常处理
     */
    @ExceptionHandler(value = {Exception.class})
    public BaseResponse exceptionHandler(Exception e) {
        return new BaseResponse(-1, e.getMessage(), "BaseRestController");
    }
}

注意:父类处理过的异常子类不能再处理,不然会报错

B情况解决方案

上面有介绍在spring中,专门处理error的类是BasicErrorController,这个类的两个核心方法,
一个方法处理html请求的返回,会默认返回一个错误页面
另外一个则是处理json请求的,会返回json格式的错误信息,我们看到的默认输出结果,就是这个方法生成的
我们现在知道了我们要关注的地方,但是我们如何改造它呢?

细心的spring早就想到了可能存在的客制化需求,所以已经为我们留好了口子.

在源文件里ErrorMvcAutoConfiguration是用来配置BasicErrorController的,如下是核心方法,

这个方法上面标记了@ConditionalOnMissingBean,也就是说我们只需要实现一个ErrorController接口,注入到上下文中就可以了

@ConditionalOnMissingBean的意思 : 当上下文中存在某一个bean,则不初始化当前被标记的bean

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
	return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
		    this.errorViewResolvers);
}

实现B情况解决方案

  • 创建一个类ExceptionErrorController继承至BasicErrorController

    public class ExceptionErrorController extends BasicErrorController {
    
        public ExceptionErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
            super(errorAttributes, errorProperties);
        }
    
        public ExceptionErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
            super(errorAttributes, errorProperties, errorViewResolvers);
        }
    
        @Override
        public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
            return super.errorHtml(request, response);
        }
    
        // 继承BasicErrorController的方式返回类型就无法改变了。。
        @Override
        public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
            Map<String, Object> body = getErrorAttributes(request,
                    isIncludeStackTrace(request, MediaType.ALL));
            HttpStatus status = getStatus(request);
            Map<String, Object> map = new HashMap<>();
            if (status == HttpStatus.NOT_FOUND) {
                //404处理
                map.put("errcode","404");
                map.put("errmsg",body.get("path").toString() + " NoFound");
            } else {
                //非404处理
                Object object = request.getAttribute("javax.servlet.error.exception");
                if (object instanceof Exception) {
                    //上下文中能拿到异常的情况
                    Exception exception = (Exception) object;
                    map.put("errcode","-1");
                    map.put("errmsg",exception.getMessage());
                } else {
                    //上下文中拿不到异常的情况
                    map.put("errcode","-1");
                    map.put("errmsg","unknow exception");
                }
            }
            return new ResponseEntity<>(map,status);
        }
    }
    @Configuration
    @ConditionalOnWebApplication
    @ConditionalOnClass({Servlet.class, DispatcherServlet.class})
    @AutoConfigureBefore(WebMvcAutoConfiguration.class)
    @EnableConfigurationProperties(ResourceProperties.class)
    public class ExceptionErrorControllerConfig {
    
        /**
         * errorViewResolvers
         */
        @Autowired(required = false)
        private List<ErrorViewResolver> errorViewResolvers;
    
        /**
         * serverProperties
         */
        @Autowired(required = false)
        private ServerProperties serverProperties;
    
    
        @Bean
        public ExceptionErrorController exceptionController(ErrorAttributes errorAttributes){
            return new ExceptionErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
        }
    }
  • 创建一个类ExceptionController继承至AbstractErrorController(此基类实现了ErrorController)
    编写如下两个方法,其实就是跟默认实现的方法一样,只是内容可以定制
    然后类中其他的方法,就按照默认的实现照抄就行了

    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class ExceptionController extends AbstractErrorController {
        private final ErrorProperties errorProperties;
    
        public ExceptionController(ErrorAttributes errorAttributes,
                                    ErrorProperties errorProperties) {
            this(errorAttributes, errorProperties, Collections.emptyList());
        }
    
        public ExceptionController(ErrorAttributes errorAttributes,
                                    ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
            super(errorAttributes, errorViewResolvers);
            Assert.notNull(errorProperties, "ErrorProperties must not be null");
            this.errorProperties = errorProperties;
        }
    
        @Override
        public String getErrorPath() {
            return this.errorProperties.getPath();
        }
    
        @RequestMapping(produces = "text/html")
        public ModelAndView errorHtml(HttpServletRequest request,
                                      HttpServletResponse response) {
            HttpStatus status = getStatus(request);
            Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                    request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
            response.setStatus(status.value());
            ModelAndView modelAndView = resolveErrorView(request, response, status, model);
            return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
        }
    
        @RequestMapping
        @ResponseBody
        public ResponseEntity<BaseResponse> error(HttpServletRequest request) {
            Map<String, Object> body = getErrorAttributes(request,
                    isIncludeStackTrace(request, MediaType.ALL));
            HttpStatus status = getStatus(request);
            BaseResponse response = new BaseResponse();
            if (status == HttpStatus.NOT_FOUND) {
                //404处理
                response.setErrcode("404");
                response.setErrmsg(body.get("path").toString() + " NoFound");
            } else {
                //非404处理
                Object object = request.getAttribute("javax.servlet.error.exception");
                if (object instanceof Exception) {
                    //上下文中能拿到异常的情况
                    Exception exception = (Exception) object;
                    response.setErrcode("-1");
                    response.setErrmsg(exception.getMessage());
                } else {
                    //上下文中拿不到异常的情况
                    response.setErrcode("-1");
                    response.setErrmsg("unknow exception");
                }
            }
            return new ResponseEntity<>(response, status);
        }
    
        protected boolean isIncludeStackTrace(HttpServletRequest request,
                                              MediaType produces) {
            ErrorProperties.IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
            if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
                return true;
            }
            if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {
                return getTraceParameter(request);
            }
            return false;
        }
    
        protected ErrorProperties getErrorProperties() {
            return this.errorProperties;
        }
    }
    @Configuration
    @ConditionalOnWebApplication
    @ConditionalOnClass({Servlet.class, DispatcherServlet.class})
    @AutoConfigureBefore(WebMvcAutoConfiguration.class)
    @EnableConfigurationProperties(ResourceProperties.class)
    public class ExceptionControllerConfig {
    
        /**
         * errorViewResolvers
         */
        @Autowired(required = false)
        private List<ErrorViewResolver> errorViewResolvers;
    
        /**
         * serverProperties
         */
        @Autowired(required = false)
        private ServerProperties serverProperties;
    
    
        @Bean
        public ExceptionController exceptionController(ErrorAttributes errorAttributes){
            return new ExceptionController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
        }
    }
请杯咖啡呗~
支付宝
微信
本文作者:DeCo
版权声明:本文首发于DeCo的博客,转载请注明出处!