• Springboot 默认的异常处理


     springboot 对异常处理默认的自动配置都封装在 ErrorMvcAutoConfiguration 这个类中,在项目启动的过程中,会往容器中注入一些默认的组件、如果容器中已经存在了这些组件,那么就不会再注入这些默认的组件到 IOC 容器中.

    项目启动的时候,会往 IOC 容器中注入下面这些默认的组件(我们这里挑比较重要的来说)

    @Configuration
    @ConditionalOnWebApplication
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
    @AutoConfigureBefore(WebMvcAutoConfiguration.class)
    @EnableConfigurationProperties(ResourceProperties.class)
    public class ErrorMvcAutoConfiguration {
    	// 将 application.properties 中标签前缀为 server 的配置与 ServerProperties 属性绑定
    	private final ServerProperties serverProperties;
    	// 错误视图解析器
    	private final List<ErrorViewResolver> errorViewResolvers;
    
    	public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
    			ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
    			
    		this.serverProperties = serverProperties;
    		this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
    	}
    	
    	@Configuration
    	static class DefaultErrorViewResolverConfiguration {
    		private final ApplicationContext applicationContext;
    		private final ResourceProperties resourceProperties;
    
    		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,ResourceProperties resourceProperties) {
    			this.applicationContext = applicationContext;
    			this.resourceProperties = resourceProperties;
    		}
    		
    		@Bean
    		@ConditionalOnBean(DispatcherServlet.class)
    		// 如果容器中不存在 DefaultErrorViewResolver ,就将该 bean 注入容器中
    		@ConditionalOnMissingBean
    		public DefaultErrorViewResolver conventionErrorViewResolver() {
    			return new DefaultErrorViewResolver(this.applicationContext,this.resourceProperties);
    		}
    	}
    	
    	@Bean
    	public ErrorPageCustomizer errorPageCustomizer() {
    		return new ErrorPageCustomizer(this.serverProperties);
    	}
    	
    	@Bean
    	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    	// 如果容器中不存在 ErrorAttributes 的实现类对象,则注入 DefaultErrorAttributes 这个 bean
    	public DefaultErrorAttributes errorAttributes() {
    		return new DefaultErrorAttributes();
    	}
    
    	@Bean
    	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
    	// 如果容器中不存在 BasicErrorController 的实现类对象,则注入 BasicErrorController 这个 bean
    	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
    		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
    				this.errorViewResolvers);
    	}
    	
    	@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>");
    
    		@Bean(name = "error")
    		// 如果容器中不存在 beanName 为 error 的 View,那么注入 bean 
    		@ConditionalOnMissingBean(name = "error")
    		public View defaultErrorView() {
    			return this.defaultErrorView;
    		}
    
    	.......
    }
    

    首先是 ErrorMvcAutoConfiguration 构造方法,这里面有两个赋值的动作,分别将 serverProperties、errorViewResolversProvider.getIfAvailable() 赋值给了 ErrorMvcAutoConfiguration 的成员变量,我们分别看一下这两个值到底是什么?

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

    serverProperties 这个变量的类型是 ServerProperties,点开 ServerProperties 这个类

    // 将 spring 的配置文件 application.properties 中 server 开头的配置和 ServerProperties 类的属性绑定起来
    // ignoreUnknownFields=true 的意思是,如果 ServerProperties 中有属性不能匹配到配置文件中的值时,不会抛出异常
    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
    public class ServerProperties implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered {
    
    	private Integer port;
    
    	private InetAddress address;
    
    	private String contextPath;
    	
    	...
    }
    

    我们可以发现,原来 serverProperties 里面封装的是我们在 application.properties 中配置的以 server 为前缀的标签,当然如果不配置,它们会有默认值

    接着看一下 errorViewResolversProvider.getIfAvailable() ,它的类型是 List<ErrorViewResolver> ,点开 ErrorViewResolver ,我们可以发现它是一个接口,并且只有一个实现类(DefaultErrorViewResolver),我们可以看一下是如何获取到 ErrorViewResolver 的 

    在 ErrorMvcAutoConfiguration 这个类中有一个静态内部类

    @Configuration
    static class DefaultErrorViewResolverConfiguration {
    	// 容器对象
    	private final ApplicationContext applicationContext;
    	// 静态资源路径对象
    	private final ResourceProperties resourceProperties;
    
    	DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,ResourceProperties resourceProperties) {
    		// 容器对象
    		this.applicationContext = applicationContext;
    		// 获取静态资源路径
    		this.resourceProperties = resourceProperties;
    	}
    	
    	// 将 DefaultErrorViewResolver 注入到容器中 id 为 conventionErrorViewResolver
    	@Bean
    	@ConditionalOnBean(DispatcherServlet.class)
    	// 如果容器中不存在 DefaultErrorViewResolver,那么我们就往容器中注入该 bean
    	@ConditionalOnMissingBean
    	public DefaultErrorViewResolver conventionErrorViewResolver() {
    		// 返回一个 DefaultErrorViewResolver
    		return new DefaultErrorViewResolver(this.applicationContext,this.resourceProperties);
    	}
    }
    

    我们看一下这个静态内部类的构造方法,这里面有一个 resourceProperties ,它就是 springboot 默认的静态资源信息,可以看出 springboot 默认的静态资源文件夹信息也封装在里面

    接着看一下怎么返回 DefaultErrorViewResolver 对象的

    public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
    	// 默认的错误视图,如果是客户端错误对应 4xx ,如果是服务器端的错误对应 5xx
    	private static final Map<Series, String> SERIES_VIEWS;
    	// 静态代码块,随着类的加载而加载
    	static {
    		Map<Series, String> views = new HashMap<Series, String>();
    		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;
    	// 默认的错误视图解析器优先级最低,如果我们自己定义了 ErrorViewResolver ,如果要生效,需要设置优先级高于 DefaultErrorViewResolver
    	// 数值越小,优先级越高,负数的优先级高于正数
    	private int order = Ordered.LOWEST_PRECEDENCE;	
    	
    	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;
    		// 获取模板对象,并将它赋值给 DefaultErrorViewResolver 类的成员变量
    		this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
    	}
    		
    	...
    }
    

    返回了 DefaultErrorViewResolver 对象之后,就将该对象注入到了 IOC 容器中

    接着来到错误页面定制器中

    @Bean
    public ErrorPageCustomizer errorPageCustomizer() {
    	// 调用 ErrorPageCustomizer 的构造方法,参数是 ErrorMvcAutoConfiguration 的成员变量 serverProperties
    	return new ErrorPageCustomizer(this.serverProperties);
    }
    
    // ErrorMvcAutoConfiguration 的静态内部类
    private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
    	// 封装的是 application.properties 配置文件中带 server 前缀的属性
    	private final ServerProperties properties;
    
    	protected ErrorPageCustomizer(ServerProperties properties) {
    		this.properties = properties;
    	}
    
    	@Override
    	public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
    		// 返回一个错误页面
    		ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()+ this.properties.getError().getPath());
    		// 注册错误页面
    		errorPageRegistry.addErrorPages(errorPage);
    	}
    	// 优先级
    	@Override
    	public int getOrder() {
    		return 0;
    	}
    }
    

    我们看一下如何返回错误页面的

    ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()+ this.properties.getError().getPath());
    

    这里有一个参数,是由两个表达式通过字符串拼接而成的,这个两个表达式是什么意思,作用是什么

    this.properties.getServletPrefix(): 对应的是 application.properties 配置文件中的 server.servlet-path 配置(如果 application.properties 中没有配置,则默认值为 /)

    public String getServletPrefix() {
    	// 获取 springboot 配置文件 application.properties 中 server.servlet-path 配置项的值
    	// 如果没有配置该配置项,默认值是 ""
    	String result = this.servletPath;
    	if (result.contains("*")) {
    		result = result.substring(0, result.indexOf("*"));
    	}
    	// 如果配置的是 server.servlet-path=/ 
    	if (result.endsWith("/")) {
    		// 截取掉 / ,最终保留的是 "" ,那么这样的话配置为 / 和不配置是同样的效果
    		result = result.substring(0, result.length() - 1);
    	}
    	return result;
    }
    

    通过上面的代码可以得出

    如果配置了 server.servlet-path = /xiaomao ,那么访问路径就是 http://ip:port/xiaomao/

    如果不配置或 server.servlet-path = / ,那么访问路径就是http://ip:port/

    接着看一下另外一个表达式 this.properties.getError().getPath()

    public class ErrorProperties {
    	// 如果 application.properties 配置了 error.path ,那么 path 就使用 error.path 的值
    	// 如果没有配置 error.path ,那么 path 就使用 /error 
    	@Value("${error.path:/error}")
    	private String path = "/error";
    
    	private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER;
    
    	public String getPath() {
    		return this.path;
    	}
    	...
    }
    

    由于我们没有在 application.properties 中配置 server.servert-path 和 error.path 的值,所以它们都是使用默认值,一个是空字符串,一个是 /error ,最终拼接的就是 "/error"

    调用 ErrorPage 的构造方法生产 ErrorPage 对象,这里的 path 就是 /error

    public ErrorPage(String path) {
    	// 设置状态码
    	this.status = null;
    	// 设置异常信息
    	this.exception = null;
    	// 设置错误页面路径
    	this.path = path;
    }

    然后就是保存错误页面到 AbstractConfigurableEmbeddedServletContainer 类中的一个 属性中

    // 所有的错误页面都会保存到这个集合中
    private Set<ErrorPage> errorPages = new LinkedHashSet<ErrorPage>();
    

    再然后就是注册错误页面,这里注册的这个错误页面是干什么的,有什么用,具体的还不是很明白,例如我配置了 server.servert-path ,然后 error.path 的值不配置,最后的 ErrorPage 就是

    /xiaomaomao/error 这个东西到底是谁的路径,还没有搞明白.

    errorPageRegistry.addErrorPages(errorPage);
    

    接着注册默认的视图 defaultErrorView ,默认的视图名称是 error

    @Configuration
    // application.properties 配置文件中不管是配置了、还是没有配置 server.error.whitelabel.enabled 节点,
    // 判断条件都成立
    @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
    // ErrorTemplateMissingCondition 的 matches(...) 方法,返回值为 true , 则判断条件成立
    @Conditional(ErrorTemplateMissingCondition.class)
    protected static class WhitelabelErrorViewConfiguration {
    	// 默认的错误视图页面,也就是我们在浏览器看到的 Whitelabel 页面
    	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>");
    					
    	// 往 IOC 容器中注入一个 beanName 为 error 的 bean 
    	@Bean(name = "error")
    	// 如果 IOC 容器中没有 error 这个 bean ,那么判断条件成立
    	@ConditionalOnMissingBean(name = "error")
    	public View defaultErrorView() {
    		return this.defaultErrorView;
    	}
    
    	// If the user adds @EnableWebMvc then the bean name view resolver from
    	// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
    	@Bean
    	@ConditionalOnMissingBean(BeanNameViewResolver.class)
    	public BeanNameViewResolver beanNameViewResolver() {
    		BeanNameViewResolver resolver = new BeanNameViewResolver();
    		resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
    		return resolver;
    	}
    }

    再接着是注册 DefaultErrorAttributes ,它里面主要是包括 时间戳、状态码、错误信息等内容

    注册完了之后就是往容器中注入 BasicErrorController 了

    @Bean
    // 如果容器中不存在 ErrorController ,判断条件成立
    @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
    	return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
    			this.errorViewResolvers);
    }

    上面说完了启动项目的时候注册的一些默认组件,现在就说一下,如果出现了错误之后, Springboot 默认是怎么处理的.

    如果浏览器或者其它客户端访问资源的时候出现了错误,就会来到 /error 请求

    // 浏览器发起请求时,如果出现错误时,对应的 Controller 处理逻辑
    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,HttpServletResponse response) {
    	// 获取状态码
    	HttpStatus status = getStatus(request);
    	// 获取模型数据 model
    	Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
    			request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    	// 响应数据中设置状态码
    	response.setStatus(status.value());
    	// 解析视图得到 ModelAndView 对象
    	ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    	// 如果 ModelAndView 不为空,直接返回 ModelAndView 对象,
    	// 如果为空,那么视图的名称为 error
    	return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
    }

    我们先看一下是如何获取 model (模型数据)的

    我们可以看出 Collections.unmodifiableMap(...) 方法的参数是通过 getErrorAttributes(...) 方法来获取的

    下面我们就仔细看一下 getErrorAttributes(...) 方法到底做了什么

    protected Map<String, Object> getErrorAttributes(HttpServletRequest request,boolean includeStackTrace) {
    	// 创建一个 ServletRequestAttributes 对象,该对象主要是对 request、response、session 等进行了封装
    	RequestAttributes requestAttributes = new ServletRequestAttributes(request);
    	// includeStackTrace 的值为 false
    	return this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace);
    }
    

    继续点开 this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace) ,这里的 this.errorAttributes 代表的是 ErrorAttributes 对象,点开 ErrorAttributes ,发现它是个接口

    public interface ErrorAttributes {
    	Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
    			boolean includeStackTrace);
    	...
    }

    所以 this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace) 调用的方法实际上是它的实现类的 getErrorAttributes(...) ,而该接口只有一个实现类 DefaultErrorAttributes ,点进去可以看到如下的内容

    // DefaultErrorAttributes 类中的方法
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
    			boolean includeStackTrace) {
    	// 新建一个 Map 集合 errorAttributes , 用来存放 model 数据
    	Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
    	// 存放时间戳
    	errorAttributes.put("timestamp", new Date());
    	// 存放状态码,这里会详细的说一下
    	// 它会去默认去 Request 域中寻找 javax.servlet.error.status_code 属性对应的值
    	// 如果找不到就去 Session 域中寻找, Request 域和 Session 域中如果都找不到,返回 null
    	// 如果返回值为 null ,则往 errorAttributes 这个 Map 集合中添加 status =999 ,error = None
    	// 如果返回值不为 null ,则往 errorAttributes 中添加 status 
    	// 以及在 HttpStatus 中定义好的 status 对应的 error reason
    	addStatus(errorAttributes, requestAttributes);
    	// 存放错误的详细信息 message
    	addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
    	// 存放访问路径 localhost:8080/abcde  ====> /abcde
    	addPath(errorAttributes, requestAttributes);
    	return errorAttributes;
    }
    

    接着就是 resolveErrorView(...) 这个方法了

    protected ModelAndView resolveErrorView(HttpServletRequest request,
    			HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
    	// 遍历循环所有的 ErrorViewResolver
    	for (ErrorViewResolver resolver : this.errorViewResolvers) {
    		// 解析错误视图
    		ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
    		if (modelAndView != null) {
    			return modelAndView;
    		}
    	}
    	// 如果 ModelAndView 对象为空,返回 null
    	return null;
    }

    解析错误视图

    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
    			Map<String, Object> model) {
    	// 这里的参数是将状态码转成了 String 类型的字符串,另外一个参数是 model
    	// 1、存在 thymeleaf 模板引擎的情况下,就优先去去找 templates/error/status的值.html 这个视图
    	// 2、不存在 thymeleaf 模板引擎的情况下就去所有的静态资源文件下寻找 /error/status的值.html 视图
    	// 如果上面两种情况都找不到,那么 modelAndView 的值为 null
    	ModelAndView modelAndView = resolve(String.valueOf(status), model);
    	// modelAndView 的值为 null ,并且如果是 CLIENT_ERROR 或者是 SERVER_ERROR
    	if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    		// 假设这里是客户端错误,那么就按照 4xx 重复上面的步骤重新解析一次
    		// 如果是服务端错误,那么按照 5xx 重复上面的步骤重新解析一次
    		modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    	}
    	return modelAndView;
    }
    

    解析错误视图,返回 ModelAndView 对象

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
    	// error/ 拼接 状态码的字符串就是错误的视图名称
    	String errorViewName = "error/" + viewName;
    	// 根据 errorViewName 获取可用的模板引擎 
    	// 例如 thymeleaf 引擎,那么判断当前项目的 templates 下有没有 /error/viewName.html 这个视图
    	// 如果有 provider 就不为空,如果没有则为 null
    	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
    			.getProvider(errorViewName, this.applicationContext);
    	// 如果有可用的模板引擎
    	if (provider != null) {
    		return new ModelAndView(errorViewName, model);
    	}
    	// 如果没有模板引擎,那么去静态资源文件夹下面找 error/status的值.html
    	return resolveResource(errorViewName, model);
    }
    

    如果有 thymeleaf 模板引擎的情况下

    // 如果存在模板引擎的情况下
    public ModelAndView(String viewName, Map<String, ?> model) {
    	// ModelAndView.setView("viewName")
    	this.view = viewName;
    	// 将 model 中的数据作为 ModelAndView 对象的属性
    	if (model != null) {
    		getModelMap().addAllAttributes(model);
    	}
    }
    

    如果没有模板引擎的情况下

    // 如果不存在模板引擎的情况下
    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
    	// 挨个遍历所有的静态文件夹 
    	// /META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/、/ 
    	for (String location : this.resourceProperties.getStaticLocations()) {
    		try {
    			Resource resource = this.applicationContext.getResource(location);
    			// 查看上面所有的 5 个静态资源文件夹中是否存在 /error/status的值.html 这个视图
    			resource = resource.createRelative(viewName + ".html");
    			if (resource.exists()) {
    				return new ModelAndView(new HtmlResourceView(resource), model);
    			}
    		}
    		catch (Exception ex) {
    		}
    	}
    	// 所有的静态资源文件夹下都不存在 /error/status的值.html 则返回 null
    	return null;
    }
    

    假设状态码为 404、如果在 templates/error/404.html、静态文件夹下 /error/404.html、templates/error/404.html、静态文件夹下 /error/404.html 这些目录都找不到视图,那么 ModelAndView 对象就为 null,接着就会继续去找templates/error/4xx.html、静态文件夹下 /error/4xx4.html、templates/error/4xx.html、静态文件夹下 /error/4xx.html 如果还找不到的情况下,就会创建一个新的视图,视图名称为 error ,那么这个 error 视图是什么呢?

    @Configuration
    // 如果 application.properties 中没有配置 server.error.whitelabel.enabled 标签,那么判断条件成立
    // 这里要注意一下,如果配置了 server.error.whitelabel.enabled=false,那么判断条件是不成立的
    // 要搞清楚 matchIfMissing=true 到底是什么意思(没有配置标签,判断条件才成立)
    @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")
    	// 如果容器中不存在一个名称为 error 的视图对象,条件成立
    	@ConditionalOnMissingBean(name = "error")
    	public View defaultErrorView() {
    		return this.defaultErrorView;
    	}
    ...
    }	
    

    看到没 @Bean 注解注册一个 View 对象,它的名称就是 error,看一下这个视图不就是我们的 Whitelabel 吗.

    总结一下上面的源码,这里假设状态码为 404 (客户端错误)

    1、项目中引入了 thymeleaf 模板引擎,那么就去寻找 templates/error/404.html 视图,找不到执行步骤 2

    2、去静态资源文件(5个静态资源文件夹)下找 /error/404.html 视图,找不到执行步骤 3

    3、去找 templates/error/4xx.html 视图(如果是服务端错误,就找 /error/5xx.html),找不到执行步骤 4

    4、去找找静态资源文件下的 /error/4xx.html 视图(如果是服务端错误,就是 /error/5xx.html)

    5、如果上面的情况都不符合,那么就是找默认的视图 Whitelabel 了

     

  • 相关阅读:
    苏教版国标本小学语文第一册汉字笔画
    C++ preprocessor __VA_ARGS__ number of arguments
    The Aggregate Magic Algorithms
    USB Mass Storage大容量存储的基本知识
    【转】驱动和应用层的三种通信方式
    Do You Actually Know What *P++ Does In C?
    Repeat Macro Cloak
    A SCSI command code quick reference
    USB device layout, descriptors, interface alternate setting
    RTOS Semaphore Services
  • 原文地址:https://www.cnblogs.com/xiaomaomao/p/14013290.html
Copyright © 2020-2023  润新知