启动一个springboot 项目后,访问一个不存在的页面,浏览器则产生一个错误的白页,而使用客户端工具返回的是一个json格式的数据。
常见的400/500错误也是如此。
为什么同一个地址,不同的客户端访问会产生不同的响应呢?
The BasicErrorController can be used as a base class for a custom ErrorController.
官方文档中有这么一句话。在SpringBoot 中是BasicErrorController
是一个自定义ErrorController
的基类来是实现错误的。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
该类是一个控制器,系统中出现的所有异常全部进到/error
这个url下,及该控制器中,在看看它是如何处理的。里面有两个处理的方法:
@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, 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
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);
}
一个返回页面,一个返回json数据。最关键的地方在@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
这里,produces源码说明:
/**
* The producible media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Accept} matches one of these media types.
* Examples:
* <pre class="code">
* produces = "text/plain"
* produces = {"text/plain", "application/*"}
* produces = MediaType.APPLICATION_JSON_UTF8_VALUE
* </pre>
* <p>It affects the actual content type written, for example to produce a JSON response
* with UTF-8 encoding, {@link org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE} should be used.
* <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Accept} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this produces restriction.
* @see org.springframework.http.MediaType
*/
String[] produces() default {};
要求请求request中的Accept符合一种MediaType才会执行,也就是当request中Accept 值为
/**
* A String equivalent of {@link MediaType#TEXT_HTML}.
*/
public static final String TEXT_HTML_VALUE = "text/html";
才会执行errorHtml()
方法,否则执行error()
方法。那我们来看看两种请求的请求头:
所以浏览器访问时返回的是页面,客户端访问时返回的是json。
自定义错误页面返回
在error文件夹下的创建对应的http 状态码页面即可返回对应的错误页面。
为什么呢?它是怎么识别出来的呢?继续看源码:
在errorHtml()
方法中页面返回是调用resolveErrorView(request, response, status, model)
来获取到页面视图。该方法调用的是父类AbstractErrorController
中的方法,而该方法实际的执行是ErrorViewResolver
接口下resolveErrorView()
方法。该接口只有一个实现
DefaultErrorViewResolver
,来看看该类中的实现
@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) {
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()
中erroViewName
该属性就是实际要返回的视图的url。前缀为error/
而viewName
为状态码,所以自定义的404.html等页面必须要在error包下才能被正确识别出来。
了解到这些我们即可自己来实现项目中自定义全局异常处理,浏览器返回错误页面,客户端返回json格式数据。
全局异常处理
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 全局异常处理类
* @author peter
* date: 2019-04-29 18:42
**/
@Controller
@RequestMapping("/error")
@Slf4j
public class GlobalErrorHandlerController implements ErrorController {
private ErrorAttributes errorAttributes;
//注入ErrorAttributes
public GlobalErrorHandlerController(ErrorAttributes errorAttributes) {
this.errorAttributes = errorAttributes;
}
@Override
public String getErrorPath() {
return "error";
}
//浏览器的处理
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public String errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
log.warn("errorHtml错误信息:", getRequestErrorMsg(request), getErrorAttributes(request));
switch (status) {
case NOT_FOUND:
return "404";
case BAD_REQUEST:
return "400";
case INTERNAL_SERVER_ERROR:
return "500";
default:
return "404";
}
}
//rest请求的处理
@RequestMapping
public ResponseEntity<?> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
log.warn("error错误信息:", getRequestErrorMsg(request), getErrorAttributes(request));
return ResponseEntity.ok(new GeneralMessage(status.value(), StringUtils.isEmpty(getRequestErrorMsg(request)) ? getErrorAttributes(request) : getRequestErrorMsg(request)));
}
private Object getRequestErrorMsg(HttpServletRequest request) {
return request.getAttribute("javax.servlet.error.message");
}
/**
* 获取request状态码
* @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;
}
}
/**
* 获取request请求中的异常信息
* @param request
* @return
*/
private Throwable getErrorAttributes(HttpServletRequest request) {
WebRequest webRequest = new ServletWebRequest(request);
return errorAttributes.getError(webRequest);
}
}
错误测试url
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试错误的url
* @author peter
* date: 2019-04-29 17:31
**/
@RestController
public class TestErrorController {
@GetMapping("/test400")
public void test400(@RequestParam("name") String name) {
System.out.println(name);
}
@GetMapping("/test500")
public void test500() {
throw new NullPointerException("test500");
}
@GetMapping("/test5001")
public void test5001() {
throw new IllegalThreadStateException("test5001");
}
}
模拟错误页面
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 模拟错误页面
* @author peter
* date: 2019-04-30 08:31
**/
@RestController
public class ErrorPageController {
@GetMapping("/404")
public String page_404(){
return "404_page";
}
@GetMapping("/400")
public String page_400(){
return "400_page";
}
@GetMapping("/500")
public String page_500(){
return "500_page";
}
}
使用浏览器和rest访问同一个url 可以看到返回的信息不同。当然也可以使用@ControllerAdvice
或@RestControllerAdvice
注解来进行全局异常的处理,但是使用该注解处理之后不会再被/error
进行处理,只能返回一种数据格式。可以使用该注解来进行自定义异常的全局处理,在搭配/error
来进行更优雅的异常处理。
/**
* @author peter
* date: 2019-04-30 09:22
**/
@RestControllerAdvice
@Component
public class GlobalErrorAdvice {
@ExceptionHandler(Exception.class)
public ResponseEntity<?> errorHandler(Exception ex){
return ResponseEntity.ok("错误信息:"+ex.getMessage());
}
}