SpringBoot——web开发之错误处理机制

一、SpringBoot提供的默认错误处理

1、在浏览器端访问时,出现错误时响应一个错误页面:

SpringBoot——web开发之错误处理机制_第1张图片

2、在其他客户端访问时,响应json数据:

SpringBoot——web开发之错误处理机制_第2张图片

3、错误处理机制的原理,参照错误自动配置类——ErrorMvcAutoConfiguration,在错误自动配置类中,配置了以下组件:

①ErrorPageCustomizer:定制错误的响应规则

@Value("${error.path:/error}")
private String path = "/error";

从主配置文件中获取error.path的值,如果没有则默认"/error",即一旦系统出现4xx或5xx之类的错误时就会发送/error请求,或者我们自定义的error.path的值的请求,该请求会有BasicErrorController类处理

②BasicErrorController:处理默认的/error请求,但是该类提供了两种/error请求的处理方式,一种产生"text/html"响应(浏览器端请求),一种产生JSON格式数据

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
	...
	@RequestMapping(produces = "text/html")//处理浏览器端发送的请求
	public ModelAndView errorHtml(HttpServletRequest request,
			HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map 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> error(HttpServletRequest request) {
		Map body = getErrorAttributes(request,
				isIncludeStackTrace(request, MediaType.ALL));
		HttpStatus status = getStatus(request);
		return new ResponseEntity<>(body, status);
	}
	...
}

那SpringBoot怎么区分请求是来自浏览器还是其他客户端呢?是根据请求头来判断的,来自浏览器的请求的请求头Accept携带有浏览器请求的信息:

SpringBoot——web开发之错误处理机制_第3张图片

其他客户端中Accept请求头的信息:没有要求优先接收html数据

SpringBoot——web开发之错误处理机制_第4张图片

这两个方法会根据错误的状态码判断去哪个错误页面或者响应什么JSON数据,响应页面的解析:遍历所有的错误视图解析器(ErrorViewResolver),如果得到异常视图则返回,否则返回null,这里的ErrorViewResolver就是下面注册的组件DefaultErrorViewResolver,也就是说去往哪个错误页面是由DefaultErrorViewResolver解析得到的

protected ModelAndView resolveErrorView(HttpServletRequest request,HttpServletResponse response, HttpStatus status, Map model) {
	for (ErrorViewResolver resolver : this.errorViewResolvers) {
		ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
		if (modelAndView != null) {
			return modelAndView;
		}
	}
	return null;
}

③DefaultErrorViewResolver:4xx为客户端错误,5xx为服务端错误

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
	...
	static {
		Map views = new EnumMap<>(Series.class);
		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 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 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方法,将状态码拼接到error/后面作为视图名称返回,也就是说SpringBoot默认会在error文件夹下找状态码对应的错误页面,同时如果模板引擎可用则返回到模板引擎指定的文件夹下的errorViewName视图,否则会执行下面这段代码:从所有的静态资源文件夹下找对应的视图(.html),找到则返回,找不到返回null

private ModelAndView resolveResource(String viewName, Map 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:帮我们在页面共享信息,视图中会有哪些数据是在该类中封装的

public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
	Map errorAttributes = new LinkedHashMap();
	errorAttributes.put("timestamp", new Date());
	this.addStatus(errorAttributes, webRequest);
	this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
	this.addPath(errorAttributes, webRequest);
	return errorAttributes;
}

出错时,SpringBoot为我们放入的数据有:这些数据我们都可以在页面中获取到

timestamp(时间戳)、status(状态码)、error(错误信息)

status:[[${status}]]

timestamp:[[${timestamp}]]

整体步骤:系统出现4xx或者5xx之类的错误时,ErrorPageCustomizer就会生效从而定制错误的响应规则,就会来到/error
请求,该请求会被BasicErrorController处理,处理的结果由DefaultErrorViewResolver返回

二、如何定制错误处理

1、如何定制错误的页面

①有模板引擎时:在模板引擎静态资源文件夹(templates)下新建一个error文件夹,并提供与错误码对应的错误页面即可(error/状态码.html)

SpringBoot——web开发之错误处理机制_第5张图片

也可以提供一个4xx.html来响应以4开头的状态码页面,此时若有精确匹配的则优先使用精确匹配的:

SpringBoot——web开发之错误处理机制_第6张图片

②没有模板引擎:可以放在任何一个静态资源文件夹下,因为视图解析时会一个个的解析,解析到则返回,解析不到则返回null,注意必须是静态资源文件夹下的error/状态码.html,若所有的静态资源文件夹下都没有error/状态码.html则会响应SpringBoot为我们提供的默认错误页面,也即是下面的代码

@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

	private final SpelView defaultErrorView = new SpelView(
			"

Whitelabel Error Page

" + "

This application has no explicit mapping for /error, so you are seeing this as a fallback.

" + "
${timestamp}
" + "
There was an unexpected error (type=${error}, status=${status}).
" + "
${message}
"); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } // If the user adds @EnableWebMvc then the bean name view resolver from // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment. @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } }

2、如何定制错误的JSON数据

①编写一个自定义异常

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

②编写一个异常处理器

a、异常处理方法使用@ResponseBody

@ControllerAdvice
public class MyExceptionHandler {

    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)//处理的异常
    public Map handlerException(Exception e){
        Map map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        //map.put("exception",e);
        return map;
    }
}

异常的处理方法中通过map放置异常信息,这个map中的key和value就是我们能够在出现异常时定制的信息

结果:通过postMan请求和通过浏览器请求都会是如下结果,并没有自适应效果(浏览器访问应该响应页面),这是因为在异常拦截器中使用了@ResponseBody

{
    "code":"user.notExist",
    "message":"用户不存在!"
}

b、不使用@ResponseBody,而将请求转发至/error,达到自适应效果

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(UserNotExistException.class)//处理的异常
    public String handlerException(Exception e){
        Map map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        //map.put("exception",e);
        return "forward:/error";
    }
}

SpringBoot提供了处理/error请求的Controller,自适应效果会在该Controller中实现,但此时响应错误页面并不是我们自定义的错误页面,而是SpringBoot提供的错误页面,且状态码为200:

SpringBoot——web开发之错误处理机制_第7张图片

这是因为我们在异常处理方法中没有设置状态码,修改一下处理方法:

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(UserNotExistException.class)//处理的异常
    public String handlerException(Exception e, HttpServletRequest request){
        request.setAttribute("javax.servlet.error.status_code",400);
        Map map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        return "forward:/error";
    }
}

错误状态码的key值是BasicErrorController在解析错误时需要从请求中获取的:

@RequestMapping
@ResponseBody
public ResponseEntity> error(HttpServletRequest request) {
	Map body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
	HttpStatus status = this.getStatus(request);
	return new ResponseEntity(body, status);
}

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

此时再在浏览器中访问时:

SpringBoot——web开发之错误处理机制_第8张图片

但是在postMan中请求时会发现并没有将我们自定义的定制信息(code、message等)带回到响应中来,

③如果我们想使用自适应同时又想定制错误信息,有一下两种方式:出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法):

a、在容器中添加ErrorController的实现类或者继承ErrorController的子类AbstractErrorController

b、页面上能获取到的数据,或者是json返回的数据都是通过errorAttributes.getErrorAttributes()得到的,SpringBoot容器中通过DefaultErrorAttributes.getErrorAttributes()进行数据处理,因此我们可以自定义ErrorAttributes,重写该类的getErrorAttributes方法,在从父类获取的map中塞入定制的信息即可:

@Component//给容器中加入自定义的错误属性类ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map map = super.getErrorAttributes(webRequest,includeStackTrace);
        map.put("company","bdm");
        return map;
    }
}

但此时已然只能放置一些公共的定制信息,不能放入一些具体的信息,比如在出现某个异常时的提示信息,此时需要我们改动一下异常处理器和上面的ErrorAttributes,将异常处理器中的map放入请求对象中携带到ErrorAttributes中:

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(UserNotExistException.class)//处理的异常
    public String handlerException(Exception e, HttpServletRequest request){
        request.setAttribute("javax.servlet.error.status_code",400);
        Map map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        request.setAttribute("ext",map);
        return "forward:/error";
    }
}
@Component//给容器中加入自定义的错误属性类ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {


    @Override
    public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map map = super.getErrorAttributes(webRequest,includeStackTrace);
        map.put("company","bdm");
        Map ext = (Map)webRequest.getAttribute("ext", 0);
        map.put("ext",ext);
        return map;
    }
}

这样我们就可以在页面和响应的json中获取到定制的错误信息了:

SpringBoot——web开发之错误处理机制_第9张图片

你可能感兴趣的:(SpringBoot)