一般情况下,springboot在我们访问一个不存在的页面时候,会默认给我们返回一个404没找到的页面,但是这只是浏览器的访问返回的结果,但是我们访问后台接口的不仅仅浏览器,可能还会有安卓,iOS客户端APP,其他形式的访问,所以我们应该对错误页面进行一个定制。对于我们的移动端APP,我们就要对json数据进行一个定制。
一。定制错误页面。
1.默认情况下,浏览器会返回一个404没找到的页面,如下图:
2.
springmvc中的定制错误页面,我们需要配置web.xml,具体请点击这里。
这里主要是看springboot是怎么处理的。
我们需要对该页面进行一个定制的话,我们可以通过在下面的路径下建立error文件夹,并创建相应的页面:
举例:
这里的4xx.html就是我们事先写好的定制的错误页面。当我们通过浏览器访问一个404的时候,便会看到我们定制化的页面。
3.原理解释。
在配置错误信息的时候,springboot用到的几个组件:
1.ErrorPageCustomizer(错误页定制器):主要作用是在发生错误是去哪个路径寻找目标。
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties,
DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
点击查看getPath()方法,可以看到,springboot会去error路径下查找资源。
2.BasicErrorController(基本错误信息处理器):也就是上面的组件发的错误请求由他来处理。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
*/
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties) {
this(errorAttributes, errorProperties, Collections.emptyList());
}
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
* @param errorViewResolvers error view resolvers
*/
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
@Override
public String getErrorPath() {
return this.errorProperties.getPath();
}
@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);
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request,
MediaType produces) {
IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
if (include == IncludeStacktrace.ALWAYS) {
return true;
}
if (include == IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}
/**
* Provide access to the error properties.
* @return the error properties
*/
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
}
我们看到这个类里面有两个方法:
很显然,第一个方法看方法名就知道是给浏览器返回一个界面,下面的是给其他非浏览器形式的返回json数据。
那后台是怎么区分请求者是浏览器还是其他形式的终端呢?
其实在我们打开控制台管理工具可以看到请求头中存在accept项:
而在其他形式的连接请求头中并不是这个text/html。
3.DefaultErrorViewResolver(默认错误页面解析器):就是在BasicErrorController中调用该类的对象来解析错误页面视图ModelAndView。
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);
}
private ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
private int order = Ordered.LOWEST_PRECEDENCE;
/**
* Create a new {@link DefaultErrorViewResolver} instance.
* @param applicationContext the source application context
* @param resourceProperties resource properties
*/
public DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(
applicationContext);
}
DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties,
TemplateAvailabilityProviders templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
@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);
}
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;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
/**
* {@link View} backed by an HTML resource.
*/
private static class HtmlResourceView implements View {
private Resource resource;
HtmlResourceView(Resource resource) {
this.resource = resource;
}
@Override
public String getContentType() {
return MediaType.TEXT_HTML_VALUE;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
FileCopyUtils.copy(this.resource.getInputStream(),
response.getOutputStream());
}
}
}
我们可以看到该类中有两个方法:
@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) {
//得到视图名,就是加上error/+视图名
String errorViewName = "error/" + viewName;
//首先通过模板引擎解析,看能不能解析成功,若不能解析成功,则通过resolveResource()方法进行
解析。
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
//调用resolveResource进行解析,,去静态资源文件夹下去找错误页面。
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;
}
通过上述两个方法我们就得到了ModelAndView。
在该类的静态代码块中可以看到:
所以,要是给每一个状态码都对应一个错误页面,有事未免有些麻烦,所以我们可以用4xx 或 5xx 的形式给错误页面命名,这样所有以4或5开头的状态码都会使用4xx 或 5xx 页面显示。
4.DefaultErrorAttributes(默认错误属性):共享页面信息。
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;
}
可以看到在map中放了时间戳,状态码,错误信息,访问路径等等 的信息。
以上就是如何定制错误页面已经springboot是如何帮我们实现这种机制的,下面就看下如何定制错误的json数据。
二。定制错误的json数据
我们使用spring的建言来实现定制发生异常时的json数据。
1.首先实现一个异常类。
2.再编写一个全局建言,使用@ControllerAdvice注解,
@ExceptionHandler注解可以具体到发生哪个异常时,进行处理。这里当发生用户不存在异常时,进行处理,将想要定制的数据写入map中,并配合@ResponseBody注解,将该map对象通过转为json数据发给前端。从而达到定制json数据的目的。
但是上面的这种方法是可以达到自己定制错误的json数据,但是无论什么以什么方式访问,返回给访问者的都是json数据,假如是用户通过浏览器访问的话,那么返回给浏览器的就是一串json数据,但是用户并不想看到是一串json数据,而是一个提示性良好的网页,这种就不具有良好的自适应性,要解决上面出现的问题,我们看下面这种方式。
3.基于上述的方法进行修改。
我们去掉@ResponseBody注解,我们通过给request设置状态码使后台返回给前端浏览器一个错误页面,同时也可以是一个json数据放回给移动端。
当我们通过非浏览器方式进行数据访问时,返回给访问者是一串json数据,就不是网页了,通过这种方式,我们可以达到自适应的效果。原理就是通过BasicErrorController实现的,它内部有两个方法,可以通过区分请求头的accept信息,来判断是进入那个方法。
但是上面的方式,又出现了另外的缺点:我返回给浏览器的确实是一个网页了,但是返回给非浏览器的json中,并没有携带我们之间设置的字段,那这该怎么解决呢?看下面。
3.解决无法携带json数据的问题。
首先我们先思考下,我们给前端返回的json数据是通过谁得到。通过上面对定制错误页面的四个组件的讲解。我们看到在BasicErrorController中的方法
可以知道,解析视图modelandview的resolveErrorView方法中的入参model中是封装了默认的json数据,那我们想自己定制json数据就可以通过这个model下手。那看下是如何获取这个model的呢?看getErrorAttributes方法:
可以看到是通过errrorAttributes.getErrorAttributes方法获得的,看一下getErrorAttributes()方法:
由此,我们可以想到,通过继承该类并重写这个方法,就可以添加我们自己想要定制的json数据了。
实现步骤:
1.将UserExceptionHandler中的方法做如下修改:
将map放入request中。
2.编写继承自DefaultErrorAttributes类的UserErrorAttriubtes类,并重写getErrorAttributes方法,首先获得父类的默认map,并通过得到requset中的我们自己定制的json数据。并加入到之前默认的map中,一起返回。
通过非浏览器端访问我们可以看到,得到了定制的json数据:
到此结束!