springboot初学(六)错误处理机制

            一般情况下,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数据:

到此结束!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值