SpringBoot异常处理机制及源码解析

SpringBoot 异常处理官方文档

启动一个springboot 项目后,访问一个不存在的页面,浏览器则产生一个错误的白页,而使用客户端工具返回的是一个json格式的数据。
浏览器
在这里插入图片描述
常见的400/500错误也是如此。

为什么同一个地址,不同的客户端访问会产生不同的响应呢?

The BasicErrorController can be used as a base class for a custom ErrorController.

官方文档中有这么一句话。在SpringBoot 中是BasicErrorController 是一个自定义ErrorController的基类来是实现错误的。

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

该类是一个控制器,系统中出现的所有异常全部进到/error这个url下,及该控制器中,在看看它是如何处理的。里面有两个处理的方法:

	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	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
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		Map<String, Object> body = getErrorAttributes(request,
				isIncludeStackTrace(request, MediaType.ALL));
		HttpStatus status = getStatus(request);
		return new ResponseEntity<>(body, status);
	}

一个返回页面,一个返回json数据。最关键的地方在@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) 这里,produces源码说明:

/**
	 * The producible media types of the mapped request, narrowing the primary mapping.
	 * <p>The format is a single media type or a sequence of media types,
	 * with a request only mapped if the {@code Accept} matches one of these media types.
	 * Examples:
	 * <pre class="code">
	 * produces = "text/plain"
	 * produces = {"text/plain", "application/*"}
	 * produces = MediaType.APPLICATION_JSON_UTF8_VALUE
	 * </pre>
	 * <p>It affects the actual content type written, for example to produce a JSON response
	 * with UTF-8 encoding, {@link org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE} should be used.
	 * <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
	 * all requests with a {@code Accept} other than "text/plain".
	 * <p><b>Supported at the type level as well as at the method level!</b>
	 * When used at the type level, all method-level mappings override
	 * this produces restriction.
	 * @see org.springframework.http.MediaType
	 */
	String[] produces() default {};

要求请求request中的Accept符合一种MediaType才会执行,也就是当request中Accept 值为

	/**
	 * A String equivalent of {@link MediaType#TEXT_HTML}.
	 */
	public static final String TEXT_HTML_VALUE = "text/html";

才会执行errorHtml()方法,否则执行error()方法。那我们来看看两种请求的请求头:
在这里插入图片描述
在这里插入图片描述
所以浏览器访问时返回的是页面,客户端访问时返回的是json。

自定义错误页面返回

在这里插入图片描述

在error文件夹下的创建对应的http 状态码页面即可返回对应的错误页面。

为什么呢?它是怎么识别出来的呢?继续看源码:
errorHtml()方法中页面返回是调用resolveErrorView(request, response, status, model) 来获取到页面视图。该方法调用的是父类AbstractErrorController中的方法,而该方法实际的执行是ErrorViewResolver接口下resolveErrorView()方法。该接口只有一个实现
DefaultErrorViewResolver,来看看该类中的实现

	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
			Map<String, Object> model) {
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

	private ModelAndView resolve(String viewName, Map<String, Object> model) {
		String errorViewName = "error/" + viewName;
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
				.getProvider(errorViewName, this.applicationContext);
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		return resolveResource(errorViewName, model);
	}

resolve()erroViewName 该属性就是实际要返回的视图的url。前缀为error/viewName为状态码,所以自定义的404.html等页面必须要在error包下才能被正确识别出来。

了解到这些我们即可自己来实现项目中自定义全局异常处理,浏览器返回错误页面,客户端返回json格式数据。

全局异常处理
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;

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

/**
 * 全局异常处理类
 * @author peter
 * date: 2019-04-29 18:42
 **/
@Controller
@RequestMapping("/error")
@Slf4j
public class GlobalErrorHandlerController implements ErrorController {

    private ErrorAttributes errorAttributes;

    //注入ErrorAttributes
    public GlobalErrorHandlerController(ErrorAttributes errorAttributes) {
        this.errorAttributes = errorAttributes;
    }


    @Override
    public String getErrorPath() {
        return "error";
    }
	
	//浏览器的处理
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public String errorHtml(HttpServletRequest request,
                            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        log.warn("errorHtml错误信息:", getRequestErrorMsg(request), getErrorAttributes(request));
        switch (status) {
            case NOT_FOUND:
                return "404";
            case BAD_REQUEST:
                return "400";
            case INTERNAL_SERVER_ERROR:
                return "500";
            default:
                return "404";
        }
    }
	
	//rest请求的处理
    @RequestMapping
    public ResponseEntity<?> error(HttpServletRequest request) {

        HttpStatus status = getStatus(request);
        log.warn("error错误信息:", getRequestErrorMsg(request), getErrorAttributes(request));
        return ResponseEntity.ok(new GeneralMessage(status.value(), StringUtils.isEmpty(getRequestErrorMsg(request)) ? getErrorAttributes(request) : getRequestErrorMsg(request)));
    }

    private Object getRequestErrorMsg(HttpServletRequest request) {
        return request.getAttribute("javax.servlet.error.message");
    }

    /**
     * 获取request状态码
     * @param request
     * @return
     */
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        try {
            return HttpStatus.valueOf(statusCode);
        } catch (Exception ex) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
    }

    /**
     * 获取request请求中的异常信息
     * @param request
     * @return
     */
    private Throwable getErrorAttributes(HttpServletRequest request) {
        WebRequest webRequest = new ServletWebRequest(request);
        return errorAttributes.getError(webRequest);
    }
}
错误测试url

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试错误的url
 * @author peter
 * date: 2019-04-29 17:31
 **/
@RestController
public class TestErrorController {

    @GetMapping("/test400")
    public void test400(@RequestParam("name") String name) {
        System.out.println(name);
    }

    @GetMapping("/test500")
    public void test500() {
        throw new NullPointerException("test500");
    }

    @GetMapping("/test5001")
    public void test5001() {
        throw new IllegalThreadStateException("test5001");
    }
}
模拟错误页面

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 模拟错误页面
 * @author peter
 * date: 2019-04-30 08:31
 **/
@RestController
public class ErrorPageController {

    @GetMapping("/404")
    public String page_404(){
        return "404_page";
    }

    @GetMapping("/400")
    public String page_400(){
        return "400_page";
    }

    @GetMapping("/500")
    public String page_500(){
        return "500_page";
    }
}

使用浏览器和rest访问同一个url 可以看到返回的信息不同。当然也可以使用@ControllerAdvice@RestControllerAdvice注解来进行全局异常的处理,但是使用该注解处理之后不会再被/error进行处理,只能返回一种数据格式。可以使用该注解来进行自定义异常的全局处理,在搭配/error 来进行更优雅的异常处理。

/**
 * @author peter
 * date: 2019-04-30 09:22
 **/
@RestControllerAdvice
@Component
public class GlobalErrorAdvice {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> errorHandler(Exception ex){
        return ResponseEntity.ok("错误信息:"+ex.getMessage());
    }

}
以上个人的理解,肯定有不妥之处,如有发现错误或不正确之处,欢迎指正。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值