一般情况下,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数据:
到此结束!