SpringBoot框架错误处理机制:默认错误处理机制和原理,定制错误响应页面和json数据

错误处理机制

springboot的默认错误处理机制

在浏览器上默认返回的是一个错误页面,下面所示

在这里插入图片描述

因为在浏览器中请求优先接收text/html数据

在这里插入图片描述

在其他的客户端默认返回的是json字符串
因为在其他的客户端的接收参数是: accept:"*/*"

springboot错误处理机制原理

具体的我们可以参考源码ErrorMvcAutoConfiguration,
在这个自动配置类里面主要给给容器添加了下面重要的组件:
	DefaultErrorAttributes
	BasicErrorController
	ErrorPageCustomizer
	DefaultErrorViewResolver
BasicErrorController
//可以看出也是从配置文件里面获取错误的路径,然后进行处理对应的错误请求
//如果获取不到就默认是/error
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
	//产生html类型的数据,浏览器发送的请求就来到这个方法进行处理
	@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 ? new ModelAndView("error", model) : modelAndView);
	}
	
	//产生json数据,其他的客户端的请求来这个方法进行处理
	@RequestMapping
	@ResponseBody
	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<Map<String, Object>>(body, status);
	}
点进resolveErrorView进行查看,可以看出这个方法是得到所有的ErrorViewResolver
进行解析,如果解析成功就返回回去,否则就返回null
	protected ModelAndView resolveErrorView(HttpServletRequest request,
			HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}
ErrorPageCustomizer
里面就是在系统出现错误之后来到error请求
里面的getPath()就是从配置文件里面获取配置的值
如果取不出,默认是/error
	@Value("${error.path:/error}")
	private String path = "/error";
	private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

		private final ServerProperties properties;

		protected ErrorPageCustomizer(ServerProperties properties) {
			this.properties = properties;
		}

		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
			ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
					+ this.properties.getError().getPath());
			errorPageRegistry.addErrorPages(errorPage);
		}

		@Override
		public int getOrder() {
			return 0;
		}

	}
DefaultErrorViewResolver
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new HashMap<Series, String>();
		views.put(Series.CLIENT_ERROR, "4xx");
		views.put(Series.SERVER_ERROR, "5xx");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}
	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
			Map<String, Object> model) {
		ModelAndView modelAndView = resolve(String.valueOf(status), 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) {
		//默认springboot去找一个error/错误状态码的页面,比如error/404
		String errorViewName = "error/" + viewName;
		//如果模板引擎可以解析就使用模板引擎解析,
		//就是返回errorViewName指定的页面
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
				.getProvider(errorViewName, this.applicationContext);
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		//模板引擎解析不了,就去静态资源文件夹下面去找errorViewName对应的页面
		//比如error/404.html
		return resolveResource(errorViewName, model);
	}
	
	private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
		for (String location : this.resourceProperties.getStaticLocations()) {
			try {
				Resource resource = this.applicationContext.getResource(location);
				resource = resource.createRelative(viewName + ".html");
				if (resource.exists()) {
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			}
			catch (Exception ex) {
			}
		}
		return null;
	}
DefaultErrorAttributes
这个是进行页面共享信息的
页面能获取的信息;
	timestamp:时间戳
	status:状态码
	error:错误提示
	exception:异常对象
	message:异常消息
	errors:JSR303数据校验的错误
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes
		implements ErrorAttributes, HandlerExceptionResolver, Ordered {
	@Override
	public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
			boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, requestAttributes);
		addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
		addPath(errorAttributes, requestAttributes);
		return errorAttributes;
	}

	private void addStatus(Map<String, Object> errorAttributes,
			RequestAttributes requestAttributes) {
		Integer status = getAttribute(requestAttributes,
				"javax.servlet.error.status_code");
		if (status == null) {
			errorAttributes.put("status", 999);
			errorAttributes.put("error", "None");
			return;
		}
		errorAttributes.put("status", status);
		try {
			errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
		}
		catch (Exception ex) {
			// Unable to obtain a reason
			errorAttributes.put("error", "Http Status " + status);
		}
	}

	private void addErrorDetails(Map<String, Object> errorAttributes,
			RequestAttributes requestAttributes, boolean includeStackTrace) {
		Throwable error = getError(requestAttributes);
		if (error != null) {
			while (error instanceof ServletException && error.getCause() != null) {
				error = ((ServletException) error).getCause();
			}
			errorAttributes.put("exception", error.getClass().getName());
			addErrorMessage(errorAttributes, error);
			if (includeStackTrace) {
				addStackTrace(errorAttributes, error);
			}
		}
		Object message = getAttribute(requestAttributes, "javax.servlet.error.message");
		if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
				&& !(error instanceof BindingResult)) {
			errorAttributes.put("message",
					StringUtils.isEmpty(message) ? "No message available" : message);
		}
	}

	private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
		BindingResult result = extractBindingResult(error);
		if (result == null) {
			errorAttributes.put("message", error.getMessage());
			return;
		}
		if (result.getErrorCount() > 0) {
			errorAttributes.put("errors", result.getAllErrors());
			errorAttributes.put("message",
					"Validation failed for object='" + result.getObjectName()
							+ "'. Error count: " + result.getErrorCount());
		}
		else {
			errorAttributes.put("message", "No errors");
		}
	}
是如何调用到这个类的呢?
	在BasicErrorController控制器里面,用来返回的数据都在model里面
	model的获取是调用getErrorAttributes()
	Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
				request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
				
	进入到getErrorAttributes()
	protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
			boolean includeStackTrace) {
		RequestAttributes requestAttributes = new ServletRequestAttributes(request);
		return this.errorAttributes.getErrorAttributes(requestAttributes,
				includeStackTrace);
	}

	然后是errorAttributes调用getErrorAttributes()
	errorAttributes就是
	private final ErrorAttributes errorAttributes;
	它的实现类就是DefaultErrorAttributes
	所以返回去的数据是通过DefaultErrorAttributes获取的
总的步骤是:系统一出现4xx或者5xx之类的错误,
ErrorPageCustomizer就会起作用,进行定制错误的响应规则,
然后就会来到/error请求,然后会被BasicErrorController
进行处理

springboot定制错误响应

定制错误页面
如果是有模板引擎的情况下:
	就将错误页面命名为错误状态码.html放在
	模板引擎文件夹里面的error文件夹下面就行,如果发生对应的状态码就会
	去到对应状态码的页面里面去

	但是这样的话,那么多的错误状态码,我们不可能为每个错误都写一个页面
	所以我们还可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误
	当然如果有精确的匹配,肯定找精确匹配的状态码
没有模板引擎的情况下(模板引擎找不到这个错误页面):
	那么就会到静态资源文件夹下面去找
上面两个都找不到的情况:
	那么就是默认返回springboot的空白的错误页面显示
	
	原理:
	在BasicErrorController里面的errorHtml()里面有
	return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
	现在找不到就返回error视图,这个视图是在容器里面的
	
	在ErrorMvcAutoConfiguration里面的WhitelabelErrorViewConfiguration里面
	它给容器加入了一个error的视图,视图的内容是defaultErrorView;
	
	@Configuration
	@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
	@Conditional(ErrorTemplateMissingCondition.class)
	protected static class WhitelabelErrorViewConfiguration {

		private final SpelView defaultErrorView = new SpelView(
				"<html><body><h1>Whitelabel Error Page</h1>"
						+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
						+ "<div id='created'>${timestamp}</div>"
						+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
						+ "<div>${message}</div></body></html>");

		@Bean(name = "error")
		@ConditionalOnMissingBean(name = "error")
		public View defaultErrorView() {
			return this.defaultErrorView;
		}
定制错误json数据
1.
	我们可以定制自己的异常处理器来接收某种异常,然后返回定制的json数据
	但是这样没有自适应的效果,就是不能浏览器返回的是页面,
	其他客户端返回的是json数据,
	这种处理方式返回的都是json数据了
package jane.test.myexception;

/**
 * @author jane
 * @create 2021-01-14 15:07
 */
public class UserNotExistException extends RuntimeException
{
    public UserNotExistException()
    {
        super("异常:用户不存在异常");
    }
}

package jane.test.controller;

import jane.test.myexception.UserNotExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

/**
 * @author jane
 * @create 2021-01-14 15:25
 */
@ControllerAdvice
public class MyExceptionHandler
{
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String,Object> handlerException(Exception e)
    {
        Map<String,Object> map = new HashMap<>();
        map.put("handlerCode","处理器说接收到用户不存在异常");
        map.put("handlerMessage",e.getMessage());
        return map;
    }
}

2.
	转发到/error进行自适应响应效果处理
 @ExceptionHandler(UserNotExistException.class)
 public String handlerException(Exception e, HttpServletRequest request)
 {
     //传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
     /**
      * Integer statusCode = (Integer) request
      .getAttribute("javax.servlet.error.status_code");
      */
     request.setAttribute("javax.servlet.error.status_code",500);
     Map<String,Object> map = new HashMap<>();
     map.put("handlerCode","处理器说接收到用户不存在异常");
     map.put("handlerMessage",e.getMessage());
     //转发到/error
     return "forward:/error";
 }
3.
	上面是进行了自适应了,但是我们自己定制的数据携带不出去,
	现在我们想将自己定制的数据携带出去
错误出现以后,就会来到/error请求,会被BasicErrorController进行处理
响应出去可以获取的数据是由getErrorAttributes得到的
这个方法是由AbstractErrorController抽象类定义的
AbstractErrorController implements ErrorController
而BasicErrorController就是实现了AbstractErrorController
所以
1.我们要携带自己定制的数据,就可以完全编写一个ErrorController的实现类
	或者是编写AbstractErrorController的子类,放在容器中;
2.
	我们也可以发现,页面上能用的数据,或者是json返回能用的数据都是通过
	errorAttributes.getErrorAttributes得到;
	默认是调用容器中DefaultErrorAttributes.getErrorAttributes()进行数据处理的;
	所以我们可以自定义ErrorAttributes
package jane.test.component;

import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import java.util.Map;

/**
 * @author jane
 * @create 2021-01-14 16:06
 */
@Component
public class MyErrorAttributes extends DefaultErrorAttributes
{
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace)
    {
        Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
        map.put("名字","jane");

        //我们的异常处理器携带的数据
        Map<String,Object> ext = (Map<String, Object>) requestAttributes.getAttribute("ext", 0);
        map.put("ext",ext);
        return map;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ReflectMirroring

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

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

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

打赏作者

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

抵扣说明:

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

余额充值