springboot异常处理解析

在之前的springmvc中,详细的研究了异常处理的源码以及不同异常处理方式的优先级。那么SpringBoot呢,同样有异常的处理通用方式,下面根据SpringMvc的类似方式入手,尝试不同的异常处理方式。

本章概要
1、模拟springmvc实现异常处理;
2、springboot更丰富的异常处理;
3、简单的源码分析;

模拟springmvc实现异常处理

我们需要在之前的welcome控制类中添加一个抛出异常的请求方法:
package com.shf.springboot.controller;

import java.util.Date;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class WelcomeController {

@Value("${application.message:Hello World}")
private String message = "Hello World";
@GetMapping("/welcome")
public String welcome(Map<String, Object> model) {
model.put("time", new Date());
model.put("message", this.message);
return "welcome";
}

@RequestMapping("/exception")
public String exception(Map<String, Object> model) {
throw new RuntimeException("异常啦");
}
}

并在

目录下新建几个不同状态的异常页面:
404.jsp:
<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
</head>
<body>
404
<br/>
${exception.message}
${message }
<br/>
</body>

</html>

500.jsp:
<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
</head>
<body>
500
<br/>
${exception.message}
${message }
<br/>
</body>

</html>

error2.jsp:
<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
</head>

<body>
error2
<%-- error: ${error} --%>
${exception.message}
<br/>
${url }
</body>

</html>

准备工作完毕,下面开始正式的异常处理实践:
1、首先尝试在控制类中直接定义异常处理方法:
1.1、跳转至异常页面,并显示异常信息:
/**
* 捕获类内所有的异常
* @param ex
* @return
*/
@ExceptionHandler
public ModelAndView exceptionHandel(Exception ex){
ModelAndView mav=new ModelAndView();
mav.setViewName("500");
mav.addObject("exception", ex);
return mav;
}
请求产生异常的请求地址:

成功跳转至500异常页面,并显示异常消息。

1.2、显示异常后的json信息,首先取消1.1中的异常处理,然后添加如下:
/**
* 异常返回JSON
*/
@ExceptionHandler
@ResponseBody
public Map<String,String> exceptionHandel(Exception ex){
Map<String,String> map=new HashMap<String,String>();
map.put("error", "错误信息:"+ex.getMessage());
return map;
}
请求验证:


1.3、在springmvc中可以通过@ResponseStatus来进行不同响应状态异常的转发,此时我们在定义的异常处理方法上加一个看看什么效果:
@ExceptionHandler
@ResponseStatus(value=HttpStatus.NOT_FOUND,code=HttpStatus.NOT_FOUND)//配置不生效,500异常仍然进入404响应
public ModelAndView exception404Handel(Exception ex){
ModelAndView mav=new ModelAndView();
mav.setViewName("404");
mav.addObject("exception", ex);
return mav;
}
此时我们仍然访问exception,此时抛出的应该是500的内部异常错误,而我们的异常处理方法注解仅处理404错误,但请求后发现

该注解并 未生效,虽然是500状态的异常,但仍然进去了异常处理方法,返回404异常页面。

2、在springmvc中可以通过注解@ControllerAdvice定义全局的异常处理类,在springboot中同样适用:
2.1、首先添加异常处理类:
package com.shf.springboot.controller;

import java.io.IOException;

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

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;

@ControllerAdvice
class GlobalExceptionHandler {

public static final String DEFAULT_ERROR_VIEW = " error2";

@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}

}
尝试请求验证:


2.2、我们是否可以通过全局的异常来捕获404错误呢,结合springmvc中的DefaultHandlerExceptionResolver默认异常处理视图解析器,常看其源码404捕获的异常类型为

下面改造上述的代码:
/**
* 404通过此异常捕获不生效
* @param ex
* @param request
* @param response
* @param handler
* @return
* @throws IOException
*/
@ExceptionHandler(value = NoHandlerFoundException.class)
public ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, HttpServletRequest request,
HttpServletResponse response, Object handler) throws IOException {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", ex);
mav.addObject("url", request.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
首先继续访问上面的异常请求地址:

发现我们的异常确实由于类型不匹配没有被捕获处理,采用了系统默认的异常处理响应,该响应来源于源代码的ErrorMvcAutoConfiguration的实现

尝试一个未定义请求地址:

同样采用上述的ErrorMvcAutoConfiguration配置的异常处理响应。
从而也证明了通过上述的全局异常捕获 NoHandlerFoundException处理无法生效。

springboot更丰富的异常处理

3、通过上述的方法能够解决类似500错误的问题,那么如果处理404错误呢,在springboot中同样有多种解决方案。
3.1、如果我们仅仅需要通过一个静态页面来处理404错误,500错误等。无需显示异常相关信息,则可以通过如下方式实现:
package com.shf.springboot.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;

import com.shf.SpringBoot1.ServerConfig;


/**
* 验证加载不同profile环境下的bean配置类
* @author song
*
*/
@Configuration
public class WebConfiguration {
@Autowired
ServerConfig serverConfig;
@Bean
@Profile(value="dev")
public EmbeddedServletContainerCustomizer devEmbeddedServletContainerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setPort(Integer.valueOf(serverConfig.getPort()).intValue());
//如果是静态页面的异常处理,如404错误,此方式不能传递异常具体信息
//错误页面需要放在Spring Boot web应用的static内容目录下,它的默认位置是:src/main/resources/static,
container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/404.html"));
}
};
}
@Bean
@Profile(value="production")
public EmbeddedServletContainerCustomizer proEmbeddedServletContainerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
// container.setPort(Integer.valueOf(serverConfig.getPort()).intValue());
}
};
}
}
通过 EmbeddedServletContainerCustomizer 来实现错误页面的定义,此时我们需要注意,静态的错误页面默认路径 src/main/resources/static

下的404.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Page Not Found</title>
<style>
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}

::selection {
background: #b3d4fc;
text-shadow: none;
}

html {
padding: 30px 10px;
font-size: 20px;
line-height: 1.4;
color: #737373;
background: #f0f0f0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}

body {
max-width: 550px;
_width: 550px;
padding: 30px 20px 50px;
border: 1px solid #b3b3b3;
border-radius: 4px;
margin: 0 auto;
box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
background: #fcfcfc;
}

h1 {
margin: 0 10px;
font-size: 50px;
text-align: center;
}

h1 span {
color: #bbb;
}

h3 {
margin: 1.5em 0 0.5em;
}

p {
margin: 1em 0;
}

ul {
padding: 0 0 0 40px;
margin: 1em 0;
}

.container {
max-width: 500px;
_width: 500px;
margin: 0 auto;
}

</style>
</head>
<body>
<div class="container">
<h1>没找到<span>:(</span></h1>
<p>Sorry, but the page you were trying to view does not exist.</p>
<p>It looks like this was the result of either:</p>
<ul>
<li>a mistyped address</li>
<li>an out-of-date link</li>
</ul>
</div>
</body>
</html>
此时请求一个未定义地址:

验证结果确实是我们需要的,此时我们可以发现主要的异常页面选择是通过HttpStatus来控制的,那么对于500、401异常状态我们同样添加如下代码即可(先取消2中定义的异常处理)。
container.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/401.html"));
container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html"));
请求500异常后跳转

此时如果我们 打开2.1中的全局异常处理,再次请求:


能够说明优先级上为1.1>2.1>3.1。

3.2、在3.1中定义的是静态的页面实现404错误处理,那么如果我们要定义一个全局的404、500异常处理,且可动态传入异常信息等如何实现呢,首先还是了解系统默认的异常处理控制类BasicErrorController,可以找到默认的异常页面路径以及相应的处理。其bean实例的创建也是在ErrorMvcAutoConfiguration注册。(默认的异常页面路径跟踪:ErrorMvcAutoConfiguration--> ServerProperties--> ErrorProperties--> @Value("${error.path:/error}")private String path;) 。页面中显示的异常信息来源于DefaultErrorAttributes中的getErrorAttributes方法将error信息保存至errorAttributes,从而通过request能够在error对应的页面中显示。
下面通过实现ErrorController,重写BasicErrorController的功能实现:
package com.shf.springboot.controller;

import java.util.Map;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping(value = "error")
@EnableConfigurationProperties({ServerProperties.class})
public class ExceptionController implements ErrorController {

private ErrorAttributes errorAttributes;

@Autowired
private ServerProperties serverProperties;


/**
* 初始化ExceptionController
* @param errorAttributes
*/
@Autowired
public ExceptionController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}


/**
* 定义404的ModelAndView
* @param request
* @param response
* @return
*/
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml404(HttpServletRequest request,
HttpServletResponse response) {
response.setStatus(getStatus(request).value());
Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
if(HttpStatus.INTERNAL_SERVER_ERROR.value()==getStatus(request).value()){
return errorHtml500(request,response);
}
return new ModelAndView("404", model);
}

/**
* 定义404的JSON数据
* @param request
* @return
*/
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error404(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}

/**
* 定义500的ModelAndView
* @param request
* @param response
* @return
*/
public ModelAndView errorHtml500(HttpServletRequest request,
HttpServletResponse response) {
response.setStatus(getStatus(request).value());
Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
return new ModelAndView("500", model);
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request,
MediaType produces) {
ErrorProperties.IncludeStacktrace include = this.serverProperties.getError().getIncludeStacktrace();
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
return true;
}
if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}

/**
* 获取错误的信息
* @param request
* @param includeStackTrace
* @return
*/
private Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}

/**
* 是否包含trace
* @param request
* @return
*/
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}

/**
* 获取错误编码
* @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;
}
}

/**
* 实现错误路径,暂时无用
* @see ExceptionMvcAutoConfiguration#containerCustomizer()
* @return
*/
@Override
public String getErrorPath() {
return "";
}

}

此时的异常页面采用的是文章头定义的几个jsp异常页面。
首先取消上述1、2中的异常处理功能, 请求一个未定义的地址:

请求最初定义的异常请求地址,发现不是我们定义的500页面

控制台提示:


可以发现是message属性问题导致,在14行,注释第14行,则再次请求显示正常

那么我们的异常信息究竟有哪些呢,

具体的查看源码即可。我们可以自己实现ErrorAttributes接口,实现自己的Error Attributes方案, 只要配置一个类型为ErrorAttributes的bean,默认的DefaultErrorAttributes就不会被配置。

系统自动配置的bean均需要满足@ConditionalOnMissingBean条件,故我们的bean一旦被注册,则直接覆盖系统默认配置。

参考资料

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值