zoukankan      html  css  js  c++  java
  • springboot情操陶冶-web配置(二)

    承接前文springboot情操陶冶-web配置(一),在分析mvc的配置之前先了解下其默认的错误界面是如何显示的

    404界面

    springboot有个比较有趣的配置server.error.whitelabel.enabled,可用来管理404界面的显示方式,是简单的显示还是详细的显示。
    指定为false的时候,则会简简单单的显示视图找不到的错误信息,如下
    404_noHandler
    指定为true的时候(默认配置),则会显示前文样例中的错误信息,如下
    404_page

    源码层分析

    springboot安排了ErrorMvcAutoConfiguration自动配置类来处理错误页面的相关信息,笔者分几个步骤来进行分析


    No.1 脑壳上的注解看一发

    @Configuration
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
    // Load before the main WebMvcAutoConfiguration so that the error View is available
    @AutoConfigureBefore(WebMvcAutoConfiguration.class)
    @EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class })
    public class ErrorMvcAutoConfiguration {
    }
    

    可以看出其是排在WebMvcAutoConfiguration配置类之前的,那么为什么需要排在前面呢?看注释是说这样才可以使error视图有效,那怎么实现的呢?笔者带着问题继续往下探索


    No.2 DefaultErrorViewResolverConfiguration内部类-错误视图解析器注册

    	@Configuration
    	static class DefaultErrorViewResolverConfiguration {
    
    		private final ApplicationContext applicationContext;
    
    		private final ResourceProperties resourceProperties;
    
    		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
    				ResourceProperties resourceProperties) {
    			this.applicationContext = applicationContext;
    			this.resourceProperties = resourceProperties;
    		}
    
    		// 注册了DefaultErrorViewResolver解析器
    		@Bean
    		@ConditionalOnBean(DispatcherServlet.class)
    		@ConditionalOnMissingBean
    		public DefaultErrorViewResolver conventionErrorViewResolver() {
    			return new DefaultErrorViewResolver(this.applicationContext,
    					this.resourceProperties);
    		}
    
    	}
    

    DefaultErrorViewResolver这个默认的错误视图解析器很有意思,里面包含了一些默认的处理,也分几个小步骤来吧,这样会显得清晰

    • 静态方法了解
    	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);
    	}
    

    应该是对HTTP状态码的映射处理,以4开头的是客户端错误,5开头的为服务端错误

    • 构造函数了解
    	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);
    	}
    

    上述的模板加载器主要是读取所有spring.factories中的org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider对应的属性值,本质也就是模板的渲染器,比如我们常用的freemarker、velocity、jsp等等

    • 视图对象获取了解
    	@Override
    	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
    			Map<String, Object> model) {
    		// 优先根据状态码来查找view静态资源,比如404则会查找error/404视图
    		ModelAndView modelAndView = resolve(String.valueOf(status), model);
    		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    			// 上述不存在则再查找error/4xx或者error/5xx视图
    			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);
    	}
    	
    	// 默认查找staticLocation指定路径的资源,比如classpath:/static/error/404.html
    	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()) {
    					// view类型为HtmlResourceView,直接将html资源输出到response对象中
    					return new ModelAndView(new HtmlResourceView(resource), model);
    				}
    			}
    			catch (Exception ex) {
    			}
    		}
    		return null;
    	}
    

    通过上述的代码注释,基本可以得知错误视图的查找规则,所以用户可以简单的在static目录下配置对应状态码的页面比如error/404.html或者error/500.html;当然也可以配置统一的页面error/4xx.html或者error/5xx.html

    那如果我们啥也不指定,那上述的错误提示信息是如何展示的呢?


    No.3 WhitelabelErrorViewConfiguration-白板错误视图配置

    	// server.error.whitelabel.enabled开关,默认是打开的
    	@Configuration
    	@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
    	@Conditional(ErrorTemplateMissingCondition.class)
    	protected static class WhitelabelErrorViewConfiguration {
    		
    		// 熟悉的打印信息
    		private final SpelView defaultErrorView = new SpelView(
    				"<html><body><h1>Whitelabel Error Page</h1>"
    						+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
    						+ "<div id='created'>${timestamp}</div>"
    						+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
    						+ "<div>${message}</div></body></html>");
    
    		// 创建了名为error的视图对象
    		@Bean(name = "error")
    		@ConditionalOnMissingBean(name = "error")
    		public View defaultErrorView() {
    			return this.defaultErrorView;
    		}
    
    		// 与上面的View对象搭配使用
    		@Bean
    		@ConditionalOnMissingBean
    		public BeanNameViewResolver beanNameViewResolver() {
    			BeanNameViewResolver resolver = new BeanNameViewResolver();
    			resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
    			return resolver;
    		}
    	}
    

    上述就是我们开头可见的错误信息的处理处,详细的用户可自行查阅代码


    No.4 构造函数了解

    	public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
    			ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
    		this.serverProperties = serverProperties;
    		this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
    	}
    

    上述的errorViewResolverProvider便会加载第二步骤的DefaultViewResolver,当然用户也可以自定义去实现ErrorViewResolver接口。这些错误的视图解析器将会在下一步骤的controller层被调用


    No.5 error控制器注册

    	@Bean
    	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    	public DefaultErrorAttributes errorAttributes() {
    		return new DefaultErrorAttributes(
    				this.serverProperties.getError().isIncludeException());
    	}
    
    	// 创建BasicErrorController控制器用于响应server.error.path指定的路径,默认为/error
    	@Bean
    	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
    	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
    		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
    				this.errorViewResolvers);
    	}
    

    此处的BasicErrorController对象则会默认响应/error的请求,其内部写了一个返回html页面的响应方法

    	@RequestMapping(produces = "text/html")
    	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());
    		// 调用errorViewResolvers集合去获取对应的错误视图
    		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    		// 如果没指定相应的视图,则会采用默认的名为error的视图
    		return (modelAndView != null ? modelAndView : new ModelAndView("error", model));
    	}
    

    对上述代码的注释作下简单的解释,帮助读者们理顺下思路

    1. 首先其会调用所有实现了ErrorViewResolver接口的视图解析器去找寻相应的错误视图,并支持通过Order接口进行排序。所以此处默认情况下会调用DefaultErrorViewResolver来获取view,具体的如果获取可见上文的讲解
    2. 如果上述找到了,那么也就么事了,但是如果还没找到,则会默认指定名为error的视图。
    3. 那么如何去解析默认名为error的视图呢?答案在DispatcherServlet在最终确定渲染视图的时候,会统一调用所有实现了ViewResolver接口的视图解析器去获取视图对象,那么第三步骤中的BeanNameViewResolver对象便会找寻到对应的SpelView视图,由其来进行相应的渲染

    在此处笔者回答下开头的问题,为什么ErrorMvcAutoConfiguration需要放在DispatcherServletAutoConfiguration之前,其实最主要的是后者并没有去注册BeanViewResolver,此处上了一份保险,好让能正确的找到SpelView对象

    error请求问题

    经过上文的分析,我们知道了BasicErrorController用来处理访问方式为GET [/error]的请求并处理得到相应的错误视图,那么最重要的问题来了,到底怎么在出现资源找不到的时候去路由至此路径上呢?笔者继续带着这个问题去探索


    No.1 ErrorPageCustomizer-错误页面配置

    	@Bean
    	public ErrorPageCustomizer errorPageCustomizer() {
    		return new ErrorPageCustomizer(this.serverProperties);
    	}
    
    	private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
    
    		private final ServerProperties properties;
    
    		protected ErrorPageCustomizer(ServerProperties properties) {
    			this.properties = properties;
    		}
    
    		@Override
    		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
    			// 默认路径为/error
    			ErrorPage errorPage = new ErrorPage(
    					this.properties.getServlet().getServletPrefix()
    							+ this.properties.getError().getPath());
    			// 注册
    			errorPageRegistry.addErrorPages(errorPage);
    		}
    
    		@Override
    		public int getOrder() {
    			return 0;
    		}
    
    	}
    

    上述的errorPage貌似展示了一点信息,可能是会去访问/error的源头,那么ErrorPageCustomizer#registerErrorPages()是如何被调用的呢?继续往下


    No.2 ServletWebAutoConfiguration引入的时候还注册了一个BeanPostProcessor

    		@Override
    		public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
    				BeanDefinitionRegistry registry) {
    			if (this.beanFactory == null) {
    				return;
    			}
    			registerSyntheticBeanIfMissing(registry,
    					"webServerFactoryCustomizerBeanPostProcessor",
    					WebServerFactoryCustomizerBeanPostProcessor.class);
    			// 就是这个
    			registerSyntheticBeanIfMissing(registry,
    					"errorPageRegistrarBeanPostProcessor",
    					ErrorPageRegistrarBeanPostProcessor.class);
    		}
    

    我们直接去关注其主要的方法

    	// 注册了相应的错误界面
    	private void postProcessBeforeInitialization(ErrorPageRegistry registry) {
    		for (ErrorPageRegistrar registrar : getRegistrars()) {
    			registrar.registerErrorPages(registry);
    		}
    	}
    
    	private Collection<ErrorPageRegistrar> getRegistrars() {
    		if (this.registrars == null) {
    			// Look up does not include the parent context
    			this.registrars = new ArrayList<>(this.beanFactory
    					.getBeansOfType(ErrorPageRegistrar.class, false, false).values());
    			this.registrars.sort(AnnotationAwareOrderComparator.INSTANCE);
    			this.registrars = Collections.unmodifiableList(this.registrars);
    		}
    		return this.registrars;
    	}
    

    至于为什么在该类中去注册这个processor去执行注册错误页面,看来这个路径的转发应该与web容器有关。其实追踪源头其实将错误界面注册到了相应的web容器中(Tomcat),具体的读者可自行去分析。


    No.4 web容器加载(插曲,顺带提一下)
    我们都知道springboot对环境为Servlet所采用的ApplicationContextAnnotationConfigServletWebServerApplicationContext,其父类在刷新上下文过程中的onRefresh()方法便去启动了web容器

    	@Override
    	protected void onRefresh() {
    		super.onRefresh();
    		try {
    			// 创建web服务器
    			createWebServer();
    		}
    		catch (Throwable ex) {
    			throw new ApplicationContextException("Unable to start web server", ex);
    		}
    	}
    
    	private void createWebServer() {
    		WebServer webServer = this.webServer;
    		ServletContext servletContext = getServletContext();
    		if (webServer == null && servletContext == null) {
    			// 默认为TomcatServletWebServerFactory
    			ServletWebServerFactory factory = getWebServerFactory();
    			// 初始化servlet/filter等
    			this.webServer = factory.getWebServer(getSelfInitializer());
    		}
    		else if (servletContext != null) {
    			try {
    				getSelfInitializer().onStartup(servletContext);
    			}
    			catch (ServletException ex) {
    				throw new ApplicationContextException("Cannot initialize servlet context",
    						ex);
    			}
    		}
    		initPropertySources();
    	}
    

    上述的代码主要会在ServletContext上注册Filters和Servlets集合并且注册ErrorPages,限于代码过长,读者可自行分析。而具体的去启动web容器则是在finishRefresh()方法中

    	@Override
    	protected void finishRefresh() {
    		super.finishRefresh();
    		// 启动
    		WebServer webServer = startWebServer();
    		if (webServer != null) {
    			publishEvent(new ServletWebServerInitializedEvent(webServer, this));
    		}
    	}
    

    No.5 StandardHostValve-错误界面应用

        private void status(Request request, Response response) {
    
            int statusCode = response.getStatus();
    
            ....
    		// 优先查找404对应的ErrorPage
            ErrorPage errorPage = context.findErrorPage(statusCode);
            if (errorPage == null) {
                // 0-默认的ErrorPage,此处便是上文注册的
                errorPage = context.findErrorPage(0);
            }
            if (errorPage != null && response.isErrorReportRequired()) {
                ....
                // 在custom方法中会调用RequestDispatcher对象进行后端路由重置到/error请求
                if (custom(request, response, errorPage)) {
                    response.setErrorReported();
                    try {
                        response.finishResponse();
                    } catch (ClientAbortException e) {
                        // Ignore
                    } catch (IOException e) {
                        container.getLogger().warn("Exception Processing " + errorPage, e);
                    }
                }
            }
        }
    

    此源码来源于tomcat,这让笔者想起了针对状态码的page配置

        <!--404 error page specified based on Tomcat-->
        <error-page>
            <error-code>404</error-code>
            <location>/404.html</location>
        </error-page>
    

    小结

    本文的内容较多,需要耐心阅读,读者只需要了解View视图的解析加载便可通读全文,如果想要自定义状态码视图则直接在classpath:/static/error目录下新建相应的状态码HTML文件即可,具体可参照本文的讲述。

  • 相关阅读:
    pip升级报错AttributeError: 'NoneType' object has no attribute 'bytes'
    在Windows中安装MySQL
    nginx配置成功,浏览器无法访问
    mysql 安装完以后没有mysql服务
    对字符串的切片操作
    linux下anaconda的安装和使用
    python学习笔记
    python学习笔记2018-9-18
    python学习笔记2018-9-17
    电脑必须用U盘引导盘才能进系统解决办法
  • 原文地址:https://www.cnblogs.com/question-sky/p/9596743.html
Copyright © 2011-2022 走看看