在SpringBoot中,通过诸多的自动配置简化开发者的复杂的配置流程。
2020/12/12,通过尚硅谷的视频记录一下在SpringBoot2.4.0中对错误页面的定制。
在本地服务器上运行服务器。

1.SpringBoot默认的错误处理机制

模拟环境:
①首先编写一个自定义的运行期异常:

public class UserNotExistException extends RuntimeException {

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

②编写一个Controller处理"/hello"请求,如果入参user为字符串aaa,则抛出异常

@Controller
public class HelloController {

	@ResponseBody
    @RequestMapping("/hello")
    public String hello(@RequestParam("user") String user) {
        if (user.equals("aaa")) {
            throw new UserNotExistException();
        }
        return "Hello World";
    }
}

SpringBoot对默认错误异常请求进行处理时:

1.浏览器上会返回一个默认的错误页面

springboot 错误信息配置_springboot 错误信息配置


其中页面的响应头中携带"text/html",底层通过BasicErrorController处理请求,调用如下方法

@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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		// 返回modelAndView对象进行页面跳转
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

页面通过ErrorPageCustomizer定制,在自动配置类中存在一个private static class StaticView implements View的内部类,通过以下方法自动生成页面

@Override
		public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
				throws Exception {
			if (response.isCommitted()) {
				String message = getMessage(model);
				logger.error(message);
				return;
			}
			response.setContentType(TEXT_HTML_UTF8.toString());
			StringBuilder builder = new StringBuilder();
			Object timestamp = model.get("timestamp");
			Object message = model.get("message");
			Object trace = model.get("trace");
			if (response.getContentType() == null) {
				response.setContentType(getContentType());
			}
			builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
					"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
					.append("<div id='created'>").append(timestamp).append("</div>")
					.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
					.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
			if (message != null) {
				builder.append("<div>").append(htmlEscape(message)).append("</div>");
			}
			if (trace != null) {
				builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
			}
			builder.append("</body></html>");
			response.getWriter().append(builder.toString());
		}

2.如果是其他客户端访问则返回json对象

我通过Postman发送get请求,"/hello?user=aaa",返回响应json数据

springboot 错误信息配置_状态码_02


其中的响应头信息

springboot 错误信息配置_html_03


底层通过BasicErrorController处理请求,调用如下方法

@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

2.参照ErrorMvcAutoConfiguration错误处理的自动配置

ErrorMvcAutoConfiguration中,给容器中添加了以下组件

1.DefaultErrorAttributes
作用 : 帮我们在页面共享信息

/**
	通过这个方法获取错误信息
	其中入参对象 WebRequest 继承了 RequestAttributes
*/
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
        ...
        return errorAttributes;
    }


/** @deprecated */
/**
	这个方法被弃用了,但是在上面这个方法中被调用
	通过返回一个Map集合,将错误信息添加并返回
*/
    @Deprecated
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        this.addStatus(errorAttributes, webRequest);
        this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        this.addPath(errorAttributes, webRequest);
        return errorAttributes;
    }

2.BasicErrorController
作用 : 处理默认"/error"请求,如之前所说

@Controller
@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); // 获取状态码
		//通过getErrorAttributes方法获取错误信息并封装到model对象
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		// 通过返回ModelAndView转发页面并携带错误内容
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}
	
	//返回json数据,其他客户端请求来到这个方法处理
	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

3.ErrorPageCustomizer
作用 : 错误页面定制器

/**
	 * {@link WebServerFactoryCustomizer} that configures the server's error pages.
	 */
	static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
			ErrorPage errorPage = new ErrorPage(
					this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
			errorPageRegistry.addErrorPages(errorPage);
		}
	}

4.DefaultErrorViewResolver
作用 : 默认的错误视图解析器

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
	@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) {
		// 默认SpringBoot可以去找到一个页面? error/404
		String errorViewName = "error/" + viewName;
		//模板引擎可以解析这个页面地址就用模板引擎解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
		//模板引擎可用的情况下返回到errorViewName指定的视图地址
			return new ModelAndView(errorViewName, model);
		}
		//模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
		return resolveResource(errorViewName, model);
	}
}

实现流程:
首先,一旦页面出现4xx或者5xx之类错误,ErrorPageCustomizer就会生效(定制错误的响应规则),就会来到/error 请求,就会被BasicErrorController处理;

**响应页面 : 去哪个页面是由DefaultErrorViewResolver解析得到的。

private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
		//默认配置跳到/error/4xx.html或者/error/5xx.html
		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.value()), model);
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

3.如何定制错误响应

1)、如何定制错误的页面;

1.1)、有模板引擎的情况下;error/状态码; 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error文件夹下】, 发生此状态码的错误就会来到 对应的页面;
我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html);
页面能获取的信息;
timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303数据校验的错误都在这里
1.2)、没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找;
1.3)、以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面;

2)、如何定制错误的json数据;

2.1)、自定义异常处理&返回定制json数据;
通过自己编写一个Handler处理异常请求

@ControllerAdvice
public class MyExceptionHandler {
/**
     * 浏览器客户端返回的都是json
     * @param e
     * @return
     */
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String, Object> handleException(Exception e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", "user.notExist");
        map.put("message", e.getMessage());
        return map;
    }
}

但是我们发现在客户端中成功定制了json数据,但在页面却也响应了json,与实际效果不符。

springboot 错误信息配置_springboot 错误信息配置_04


springboot 错误信息配置_html_05


2.2)、转发到/error进行自适应响应效果处理

所以我们可以return “forward:/error”,转发到/error进行自适应响应效果处理

@ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", "user.notExist");
        map.put("message", e.getMessage());
        //转发到/error
        return "forward:/error";
    }

虽然实现了自适应效果,但是页面返回200状态码,客户端也没有携带自定义的字段数据

springboot 错误信息配置_状态码_06


springboot 错误信息配置_html_07


分析:

出现200状态码,是因为它没有解析到错误视图,它要从request中获取状态码,而request通过getAttribute获取,所以综上所述,我们需要传入自己的状态码,才能跳转到错误页面。

springboot 错误信息配置_java_08


springboot 错误信息配置_http_09


springboot 错误信息配置_java_10


再修改一遍我们的代码,可以成功跳转到400错误页面,此处不展示效果图了。

@ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<>();
        //传入我们自己的错误状态码 4xx 5xx
        /**
         Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
         String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
         */
        request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 400);
        map.put("code", "user.notExist");
        map.put("message", e.getMessage());
        //转发到/error
        return "forward:/error";
    }

2.3)、将我们的定制数据携带出去

出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由 getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法);

springboot 错误信息配置_html_11


所以我们要想实现自定义数据,第一种办法就是完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;但是这个太复杂,不推荐

第二种方法,因为页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到。errorAttributes的实现类是DefaultErrorAttributes,内部通过容器中DefaultErrorAttributes.getErrorAttributes()方法默认进行数据处理的;所以我们可以自定义编写ErrorAttributes并重写getErrorAttributes()方法,将组件加入容器中。

自定义errorAttributes

//给容器中加入我们自己定义的ErrorAttributes
//@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    //返回值的map就是页面和json能获取的所有字段
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
    	// 通过super.getErrorAttributes获取原来默认的错误信息
        Map<String, Object> map = super.getErrorAttributes(webRequest, options);
        //添加我们自定义的信息进去
        map.put("actor", "XiaoJiang");
        // 通过request获取我们在controller中保存的 异常处理器携带的数据
        Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
        // 并加入到map集合中
        map.put("ext", ext);
        return map;
    }
}

在我们自定义Controller中

@ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<>();
        //传入我们自己的错误状态码 4xx 5xx
        /**
         *     Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
         *     String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
         */
        request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 400);
        map.put("code", "user.notExist");
        map.put("message", e.getMessage());

        request.setAttribute("ext", map); // 将自定义信息加入到map
        //转发到/error
        return "forward:/error";
    }

最终实现效果: 响应是自适应的,可以通过定制ErrorAttributes改变需要返回的内容,喜大普奔

springboot 错误信息配置_java_12


springboot 错误信息配置_状态码_13

补充说明 : 在springboot中使用thymeleaf展示数据时,遇到message和exception具体内容无法在页面中展示,需要在application.properties中设置 :

springboot 错误信息配置_java_14

server.error.include-exception=true
server.error.include-message=always

最后,参考视频了解底层实现,如果有什么错误的地方,请大佬们指正!