Spring Boot学习笔记(八)Web开发之错误处理机制以及自定义错误页面和错误数据

一、Spring Boot错误处理原理

Spring Boot的错误处理自动配置类:org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration(在spring-boot-autoconfigure.jar下),这个类注册了很多关于错误处理的组件。主要的有四个DefaultErrorAttributes、BasicErrorController、ErrorPageCustomizer、DefaultErrorViewResolver。

public class ErrorMvcAutoConfiguration {
    ...
    //注入DefaultErrorAttributes组件
    @Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes(
				this.serverProperties.getError().isIncludeException());
	}

    //注入BasicErrorController 组件
	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	//@Bean放在方法上,如果参数类型所对应的实例在spring容器中只有一个,则默认选择这个实例。如果有多个,则需要根据参数名
	//称来选择(参数名称就相当于是spring的配置文件中的bean的id)
	//errorAttributes从容器中注入的其实就是DefaultErrorAttributes(实现ErrorAttributes )
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				this.errorViewResolvers);
	}
    //注入ErrorPageCustomizer 组件
	@Bean
	public ErrorPageCustomizer errorPageCustomizer() {
		return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
	}
   @Configuration
	static class DefaultErrorViewResolverConfiguration {

		private final ApplicationContext applicationContext;

		private final ResourceProperties resourceProperties;

		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
				ResourceProperties resourceProperties) {
			this.applicationContext = applicationContext;
			this.resourceProperties = resourceProperties;
		}
        //注入DefaultErrorViewResolver 组件
		@Bean
		@ConditionalOnBean(DispatcherServlet.class)
		@ConditionalOnMissingBean
		public DefaultErrorViewResolver conventionErrorViewResolver() {
			return new DefaultErrorViewResolver(this.applicationContext,
					this.resourceProperties);
		}

	}
    ...
}

Spring boot错误处理步骤:
1、当程序发生异常或者错误,ErrorPageCustomizer组件就会生效,它的作用是定制错误的响应规则。

private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
   ...
		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
		//this.properties.getError().getPath())的值是"/error"
			ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
					.getRelativePath(this.properties.getError().getPath()));
			errorPageRegistry.addErrorPages(errorPage);
		}
    ...
	}

这段代码说明当系统出现错误后就去到/error请求进行处理。
2、/error请求就会被BasicErrorController处理,处理完后返回响应页面。

@Controller
//如果server.error.path获取不到就用${error.path:/error},error.path获取不到就用/error,说明这个controller的地址是/error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
   ...
    //MediaType.TEXT_HTML_VALUE="text/html"
    //如果是浏览器的请求就走这个方法
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request,
			HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		//构建错误数据,保存到model中
		Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
				request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		//构建ModelAndView即处理完返回的视图,也就是响应页面
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		//如果找不到返回error视图
		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);
	}
	...
}

3、响应页面的规则,实际上是通过DefaultErrorViewResolver组件来处理的。上面代码中的resolveErrorView方法就是返回响应页面的。它是BasicErrorController的父类AbstractErrorController中的方法。

public abstract class AbstractErrorController implements ErrorController {
   private final ErrorAttributes errorAttributes;
   private final List<ErrorViewResolver> errorViewResolvers;
   ...
   protected ModelAndView resolveErrorView(HttpServletRequest request,
			HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
		//获取所有的ErrorViewResolver,包括DefaultErrorViewResolver,所以我们可以自己定制自己的ErrorViewResolver组件
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}
}

DefaultErrorViewResolver类源码:

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
		views.put(Series.CLIENT_ERROR, "4xx");
		views.put(Series.SERVER_ERROR, "5xx");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}

   //构建响应页面规则,HttpStatus 是错误的状态码:404、500等。
    @Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
			Map<String, Object> model) {
	   //调用resolve方法创建错误视图
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
		//如果没有这个视图并且静态资源中也没有错误页面(具体状态码.html例如:404.html),而且状态码是4开头或者5开头。
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
		   //创建error/4xx或者error/5xx视图。
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

	private ModelAndView resolve(String viewName, Map<String, Object> model) {
	   //错误视图例:error/404
		String errorViewName = "error/" + viewName;
		//使用模板引擎找到这个视图
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
				.getProvider(errorViewName, this.applicationContext);
		//如果能找到视图就创建视图对象
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		//没有找到则调用此方法,这个方法是去默认的静态资源文件夹下找。
		return resolveResource(errorViewName, model);
	}

	private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
	   // this.resourceProperties.getStaticLocations()获取的是默认的静态资源文件夹:public、static这些。
		for (String location : this.resourceProperties.getStaticLocations()) {
			try {
				Resource resource = this.applicationContext.getResource(location);
				//错误页面,例:error/400.html
				resource = resource.createRelative(viewName + ".html");
				if (resource.exists()) {
				   //如果存在这个html页面就用这个,否则返回null
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			}
			catch (Exception ex) {
			}
		}
		return null;
	}
}

DefaultErrorViewResolver先去模板引擎(以thymeleaf为例)默认文件夹下(templates)找”error/状态码(404、500等).html"文件,没有,则去Spring Boot默认静态文件夹下找,没有,再去找”error/4xx(5xx).html"文件,找到了则创建对应的视图,没找到返回null。而在BasicErrorController的resolveErrorView方法中有一句:

return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);

表示如果找不到视图,则创建默认视图"error"。而在ErrorMvcAutoConfiguration.class中引入了error视图:

        private final StaticView defaultErrorView = new StaticView();
        @Bean(name = "error")
		@ConditionalOnMissingBean(name = "error")
		public View defaultErrorView() {
			return this.defaultErrorView;
		}

error视图的类型是StaticView,这个类的作用就是生成Spring Boot默认的错误页面。
4、错误数据
从前面的代码中我们知道,通过@Bean注解,我们把DefaultErrorAttributes注入到了BasicErrorController中,而在BasicErrorController的errorHtml方法中,调用getErrorAttributes方法,此方法再调用DefaultErrorAttributes的getErrorAttributes方法获取了封装好的错误数据据。

@Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest,
			boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, webRequest);
		addErrorDetails(errorAttributes, webRequest, includeStackTrace);
		addPath(errorAttributes, webRequest);`在这里插入代码片`
		return errorAttributes;
	}

自定义错误页面:
综上所述,可以知道当有模板引擎时,将错误页面命名为"错误状态码.html"放在模板引擎文件夹里面的error文件夹下,发生此状态码的错误就会来到对应的页面,找不到会去Spring Boot默认的静态资源文件夹下找,也可以使用4xx.html/5xx.html作为错误页面的文件名来匹配4类型和5类型的所有错误,但是精确优先(优先寻找精确的状态码.html)。没有模板引擎时直接去Spring Boot默认的静态资源文件夹下找。如果都没有就生成默认错误页面。

二、自定义异常和错误数据

上面我们说过,Spring Boot的错误数据处理是通过DefaultErrorAttributes类来实现的,我们可以通过继承DefaultErrorAttributes来扩展自己的错误数据处理。这时DefaultErrorAttributes会失效(@ConditionalOnMissingBean)。

@Component//将MyErrorAttributes加到容器中
public class MyErrorAttributes extends DefaultErrorAttributes{

	@Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
		// 在DefaultErrorAttributes的基础上做扩展。
		Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
		map.put("company", "comDMF");//自己的错误记录数据thymeleaf使用${company}取出。
		return map;
	}

}

自定义异常:
1、定义异常类

public class UserNotExistException extends RuntimeException{

	public UserNotExistException(){
		super("用户不存在!");
	}
}

2、创建异常处理类

@ControllerAdvice
public class MyExceptionHandler {
	@ResponseBody
	@ExceptionHandler(UserNotExistException.class)//处理UserNotExistException异常
	public Map<String, Object> handleException(Exception e){
       	Map<String, Object> map = new HashMap<>();
		map.put("code", "userNotExist");
		map.put("message", e.getMessage());
		return map;
	}
}

3、创建测试的action方法

    //当user参数为aaa时抛出UserNotExistException异常
    @ResponseBody
	@RequestMapping("/hello")
	public String  hello(@RequestParam("user") String user){
		if("aaa".equals(user)){
			throw new UserNotExistException();
		}
		return "hello world!";
	}

当程序发生UserNotExistException异常时,就会调用MyExceptionHandler的handleException来处理,返回json数据,而不是错误页面,如果想要让自定义异常经过默认的错误处理流程,返回错误页面,并且携带自己定制的错误数据可以这样做。

1、转发到"/error"请求,因为"/error"请求默认由BasicErrorController处理,这样就可以经过默认的错误处理流程。
2、自定义状态码,如果不设置状态码默认是200,就会跳到Spring Boot的默认错误页面。
3、将自己的错误数据存到request中,然后在自己的ErrorAttributes类里取出存到model里。
实例:

@ControllerAdvice
public class MyExceptionHandler {
    //处理UserNotExistException异常
	@ExceptionHandler(UserNotExistException.class)
	public String handleException(Exception e,HttpServletRequest request){
	   //设置状态码
		request.setAttribute("javax.servlet.error.status_code", 500);
		//设置自己的错误数据,存到request中
		Map<String, Object> map = new HashMap<>();
		map.put("code", "userNotExist");
		map.put("message", e.getMessage());
		request.setAttribute("ext", map);
		//转发到/error请求
		return "forward:/error";
	}
}

//必须要将自己的组件加到容器中,才会生效
@Component
public class MyErrorAttributes extends DefaultErrorAttributes{

	@Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        //先执行默认的错误数据处理
		Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
		map.put("company", "comDMF");
		//取出异常处理器携带的数据,存到map中
		Map<String, Object> ext = (Map<String, Object>)webRequest.getAttribute("ext", 0);
		map.put("ext", ext);
		return map;
	}

}

状态码这么设置是因为在BasicErrorController的errorHtml方法中通过getStatus方法取状态码:

HttpStatus status = getStatus(request);

getStatus是BasicErrorController的父类AbstractErrorController中的方法:

protected 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;
		}

所以设置状态码时设置名为"javax.servlet.error.status_code"。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值