文章目录

  • 1、配置文件的加载位置
  • 2、静态资源映射规则
  • 2.1、第一种映射规则
  • 2.1.1、官方文档介绍
  • 2.1.2、源码分析
  • 2.2、第二种映射规则
  • 2.2.1、官方文档介绍
  • 2.2.2、源码分析
  • 2.3、欢迎页映射规则
  • 2.3.1、官方文档介绍
  • 2.3.2、源码分析
  • 3、templates文件夹
  • 3.1、静态页面
  • 3.2、动态页面
  • 4、错误页面处理(Error Handling)
  • 4.1官方文档介绍
  • 4.2、源码分析
  • 5、WebMvcConfigurer配置讲解
  • 5.1、Spring官方API与文档介绍
  • 5.2、addInterceptors:拦截器
  • 5.2、addViewControllers:页面跳转
  • 5.3、addResourceHandlers:静态资源
  • 5.4、configureDefaultServletHandling:默认静态资源处理器
  • 5.5、configureViewResolvers:视图解析器
  • 5.6、addCorsMappings:跨域
  • 5.7、configureMessageConverters:信息转换器



研究的SpringBoot版本:2.4.0


1、配置文件的加载位置


SpringBoot启动会扫描以下位置的applicaiton.properties或者application.yml文件作为SpringBoot的默认配置文件

  • file:./config/
  • file:./
  • classpath:/config/
  • classpath:/

以上是按照优先级从高到低的顺序,所有位置的文件都会被加载,互补配置高优先级配置内容会覆盖低优先级配置内容

Tip:可以输出以下 java.class.path 看一下当前项目的类路径

我们也可以通过配置 spring.config.location 来改变默认位置

项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的这些配置文件共同起作用形成互补配置

eg:java -jar example-0.0.1-SNAPSHOT.jar --spring.config.location=D:/application.yml

源码可以参考一下org.springframework.boot.context.config.ConfigFileApplicationListener类(2.4.0版本之前)

@Deprecated
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
   // Note the order is from least to most specific (last one wins)
   private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";

   private static final String DEFAULT_NAMES = "application";

可以通过阅读一下此类的注释了解一下该类的作用

/**
 * {@link EnvironmentPostProcessor} that configures the context environment by loading
 * properties from well known file locations. By default properties will be loaded from
 * 'application.properties' and/or 'application.yml' files in the following locations:
 * <ul>
 * <li>file:./config/</li>
 * <li>file:./config/{@literal *}/</li>
 * <li>file:./</li>
 * <li>classpath:config/</li>
 * <li>classpath:</li>
 * </ul>
 * The list is ordered by precedence (properties defined in locations higher in the list
 * override those defined in lower locations).
 * <p>
 * Alternative search locations and names can be specified using
 * {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}.
 * <p>
 * Additional files will also be loaded based on active profiles. For example if a 'web'
 * profile is active 'application-web.properties' and 'application-web.yml' will be
 * considered.
 * <p>
 * The 'spring.config.name' property can be used to specify an alternative name to load
 * and the 'spring.config.location' property can be used to specify alternative search
 * locations or specific files.
 * <p>
 *
 * @since 1.0.0
 * @deprecated since 2.4.0 in favor of {@link ConfigDataEnvironmentPostProcessor}
 */

得到以下几条信息

  1. 它通过加载来配置上下文环境来自已知文件位置的属性。
  2. 默认情况下,将从"classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/"扫描加载配置文件
  3. 列表按优先级排序(在列表中较高位置定义的属性覆盖在较低位置定义的值)
  4. 支持多环境配置,多环境配置文件格式:application-{profile}.properties/yml在主配置文件中用spring.profiles.active激活
  5. 可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置
  6. 自SpringBoot2.4.0版本起已弃用,取而代之的是 ConfigDataEnvironmentPostProcessor

可以看到与我们之前一开始总结的结论一样


2、静态资源映射规则


在SpringBoot里,SpringMVC的相关配置都在 WebMvcAutoConfiguration 里面,而这个自动配置类里有这样一段

spring boot前端模版 springboot的前端_java

根据以上代码内容,我们可以看到两种映射规则!



2.1、第一种映射规则


2.1.1、官方文档介绍

By default, Spring Boot serves static content from a directory called /static (or /public or /resources or /META-INF/resources) in the classpath or from the root of the ServletContext. It uses the ResourceHttpRequestHandler from Spring MVC so that you can modify that behavior by adding your own WebMvcConfigurer and overriding the addResourceHandlers method.

默认情况下,Spring Boot从类路径中的 /static 目录(或 /public 或 /resources或 /META-INF / resources)或ServletContext的根目录中提供静态内容

它使用Spring MVC中的ResourceHttpRequestHandler,以便您可以通过添加自己的WebMvcConfigurer并重写addResourceHandlers方法来修改该行为

By default, resources are mapped on /**, but you can tune that with the spring.mvc.static-path-pattern property. For instance, relocating all resources to /resources/** can be achieved as follows:spring.mvc.static-path-pattern=/resources/**

默认情况下,资源映射在 /** 上,但是您可以使用 spring.mvc.static-path-pattern属性对其进行调整

例如,将所有资源重定位到 /resources/ ,可以如下实现:spring.mvc.static-path-pattern=/resources/

2.1.2、源码分析

String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
   customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
         .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
         .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)
         .setUseLastModified(this.resourceProperties.getCache().isUseLastModified()));
}

访问当前项目配置的staticPathPattern路径下的所有资源

都去getResourceLocations(this.resourceProperties.getStaticLocations)

那 staticPathPattern 与 getResourceLocations(this.resourceProperties.getStaticLocations) 指的又是什么呢?

spring boot前端模版 springboot的前端_java_02

spring boot前端模版 springboot的前端_spring_03

spring boot前端模版 springboot的前端_spring boot_04

所以:/**访问当前项目下的任何资源,都去以下四个路径下寻找资源,优先级由高到低

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/
  • /:当前项目的根路径

这几个文件夹我们就称为静态资源文件夹,你把你的静态资源只要保存在这几个文件夹里面,人家访问的时候呢,只要没有人处理,都会在这几个文件夹里面找资源

例如:访问 localhost:8080/a.css,会默认去找这几个文件夹中的a.css返回


2.2、第二种映射规则



2.2.1、官方文档介绍


In addition to the “standard” static resource locations mentioned earlier, a special case is made for Webjars content. Any resources with a path in /webjars/** are served from jar files if they are packaged in the Webjars format.



除了前面提到的“标准”静态资源位置,Webjar内容也有特殊情况

如果 jar 文件以 Webjars 格式打包,则从jar文件提供带有 /webjars/** 路径的所有资源

Tip:如果您的应用程序打包为jar,则不要使用 src / main / webapp 目录
       尽管此目录是一个通用标准,但它仅与 war 打包一起使用,并且如果生成jar,大多数构建工具都将其忽略


2.2.2、源码分析

if (!registry.hasMappingForPattern("/webjars/**")) {
				customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
						.addResourceLocations("classpath:/META-INF/resources/webjars/")
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)
						.setUseLastModified(this.resourceProperties.getCache().isUseLastModified()));
			}

访问/webjars/**下面的所有资源都去classpath:/META-INF/resources/webjars/找资源

webjars:以jar包的方式引入静态资源 https://www.webjars.org/

里面将我们常用的前端框架例如JQuery,Bootstrap等,以Maven依赖的方式交给我们

spring boot前端模版 springboot的前端_java_05

我们只需要将它的Maven坐标引入项目中即可使用

spring boot前端模版 springboot的前端_spring boot_06

根据上述的映射规则,如果我们发送localhost:8080/webjars/bootstrap/4.5.3/js/bootstrap.js

则这个资源会去哪找呢?

就会去找到这个jar包的类路径下/META-INF/resources/webjars//bootstrap/4.5.3/js/bootstrap.js资源!


2.3、欢迎页映射规则



2.3.1、官方文档介绍

Spring Boot supports both static and templated welcome pages. It first looks for an index.html file in the configured static content locations. If one is not found, it then looks for an index template. If either is found, it is automatically used as the welcome page of the application.

Spring Boot同时支持静态和模板化欢迎页面。它首先在配置的静态内容位置中查找index.html文件。如果没有找到索引模板,则查找索引模板。如果找到任何一个,它将自动用作应用程序的欢迎页面


2.3.2、源码分析

spring boot前端模版 springboot的前端_java_07

HandlerMapping是SpringMvc的底层注解,它来保存每一个请求对应谁来处理

private Optional<Resource> getWelcomePage() {
			String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
			return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
		}

private Resource getIndexHtml(String location) {
			return this.resourceLoader.getResource(location + "index.html");
		}

getWelcomePage()的返回对应由 this.mvcProperties.getStaticPathPattern()来处理

即所有静态资源文件夹下的index.html,都被/**映射,即localhost:8080 -> 找index页面



3、templates文件夹


SpringBoot里面没有我们之前常规web开发的 WebContent(WebApp),它只有src目录(正常的maven结构)

在src/main/resources下面有两个文件夹,static和templates。springboot默认static中放静态页面,而templates中放动态页面

spring boot前端模版 springboot的前端_spring boot前端模版_08



3.1、静态页面

这里我们直接在static(或者讲过的任何一个静态资源文件夹)放一个example.html,然后直接输入http://localhost:8080/example.html便能成功访问

或者也可以通过controller控制器跳转:

@Controller
public class ExampleController {
    @GetMapping("/example")
    public String sayHello() {
        return "example.html";
    }
}

然后输入http://localhost:8080/example就可以成功访问!

这两种请求的方式当我们的 DispatcherServlet 在进行处理的时候,差别为根据九大组件之一的 HandlerMapping 返回的请求处理的执行链 HandlerExecutionChain 中的处理器 Handler 不同

前者直接输入URL请求静态资源的方式

spring boot前端模版 springboot的前端_spring boot_09

spring boot前端模版 springboot的前端_web_10

它最后会在真正调用处理方法中调用 HttpMessageConverterWrite() 方法将客户端浏览器请求的内容以 Byte[] 写回,并返回一个空的 ModelAndView,具体实现类为ResourceRegionHttpMessageConverter

后者通过控制器controller访问

spring boot前端模版 springboot的前端_web_11

这种方式在真正调用处理方法后返回的 ModelAndView 不为空,其内包含了视图与数据,在最后在 doDispatch() 方法中调用 processDispatchResult() 处理调度结果时候,调用 ModelAndView 中的视图 Viewrender() 方法根据逻辑视图地址去往真正的页面,具体为执行请求转发,转发给另外一个 DispatcherServlet 的实例处理转发的请求,请求路径为 /example.html,随后处理的逻辑就与前者直接输入URL请求静态资源的方式一样了


3.2、动态页面

动态页面需要先请求服务器,访问后台应用程序,然后再转向到页面,比如访问JSP。spring boot建议不要使用JSP,默认使用Thymeleaf模板引擎来做动态页面

在pom文件中添加Thymeleaf的场景启动器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

我们先在tempates文件夹中也新建一个example.html但内容不同,然后先试一下直接访问该页面

输入http://localhost:8080/example.html,结果显然访问的是静态资源文件夹里面的那个example.html

然后我们现在再试一下用controller跳转

spring boot前端模版 springboot的前端_spring boot前端模版_12

无法访问到example.html了,直接报错

因为:静态页面的return默认是跳转到/static/index.html,当在pom.xml中引入了thymeleaf组件,动态跳转会覆盖默认的静态跳转,默认就会跳转到/templates/index.html,注意看两者return代码也有区别,动态没有html后缀。

去ThymeleafProperties里看一下Thymeleaf的默认规则

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

   private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;

   public static final String DEFAULT_PREFIX = "classpath:/templates/";

   public static final String DEFAULT_SUFFIX = ".html";

有一个默认的前缀 classpath:/templates/ 与一个默认的后缀.html,即只要我们把 html 页面放到我们的类路径下的templates里面,就能帮我们自动渲染了

按照规则修改一下我们的代码

@Controller
public class ExampleController {
    @GetMapping("/example")
    public String sayHello() {
        return "example";
    }
}

我们以前写返回example,springmvc给我们拼串得jsp页面,现在我们引入thymeleaf之后,现在我们发送example请求,然后我们要去example页面,这个页面在哪里呢?照thymeleaf拼串规则,会去classpath:/templates/文件夹里面找到example,再给你拼一个html后缀,返回

如果在使用动态页面时还想跳转到/static/index.html,可以使用重定向return “redirect:/example.html”

return "redirect:/example.html";



4、错误页面处理(Error Handling)


4.1官方文档介绍


If you want to display a custom HTML error page for a given status code, you can add a file to an /error directory. Error pages can either be static HTML (that is, added under any of the static resource directories) or be built by using templates. The name of the file should be the exact status code or a series mask.



如果要显示给定状态代码的自定义HTML错误页面,可以将文件添加到 /error 目录

错误页面可以是静态HTML(即添加到任何静态资源目录下),也可以使用模板来构建

文件名应为确切的状态代码或系列掩码

例如,要将404映射到静态HTML文件,您的目录结构应该如下所示:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

要使用Thymeleaf模板映射所有5xx错误,您的目录结构应该如下所示:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.html
             +- <other templates>

对于更复杂的映射,您还可以添加实现ErrorViewResolver接口的bean,如下面的示例所示

public class MyErrorViewResolver implements ErrorViewResolver {
    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request,
            HttpStatus status, Map<String, Object> model) {
        // 根据请求或者状态码选择性的返回一个ModelAndView
        return ...
    }
}


4.2、源码分析

原理都可以参照SpringBoot的自动配置,在Web的自动配置模块中有一个叫ErrorMvcAutoConfiguration的自动配置类

与Web模块的错误页面怎么处理的自动配置规则全部封装在这个自动配置类里面

主要给容器中添加了以下几个重要组件

  • DefaultErrorAttributes
  • BasicErrorController
  • ErrorPageCustomizer
  • DefaultErrorViewResolver

一旦系统出现4XX或者5XX的错误,ErrorPageCustomizer 就会生效(定制错误的相应规则)

@Bean
	public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
		return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
	}

ErrorPageCustomizer 的唯一方法(作用)就是来注册错误页面的响应规则的

@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
   ErrorPage errorPage = new ErrorPage(
         this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
   errorPageRegistry.addErrorPages(errorPage);
}

主要有一个 getPath(),点进去发现就是从配置文件中取出的 error.path:/error 的值,默认为/error

/**
	 * Path of the error controller.
	 */
	@Value("${error.path:/error}")
	private String path = "/error";

即系统出现错误后就来到/error请求进行处理,此时第二个重要组件 BasicErrorController 就要干活了,注册代码如下:

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
      ObjectProvider<ErrorViewResolver> errorViewResolvers) {
   return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
         errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

它就是一个contoller,它来从配置文件中取出 server.error.path:${error.path:/error} 的值(:的意思为前面取不出用后面)

所以它就是默认来处理 /error 请求的controller,它是怎么处理的呢?类中有下面两种请求的处理方式

@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, getErrorAttributeOptions(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) {
   HttpStatus status = getStatus(request);
   if (status == HttpStatus.NO_CONTENT) {
      return new ResponseEntity<>(status);
   }
   Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
   return new ResponseEntity<>(body, status);
}

第一种处理方式会产生 TEXT_HTML_VALUE = "text/html" 类型的数据,第二种返回 ResponseEntity 类型数据(与 @ResponseBody 相比,不仅可以返回json结果,还可以定义返回的 HttpHeadersHttpStatus

浏览器发送请求的时候,我们在控制台可以看到它优先接收 text/html 类型的数据

spring boot前端模版 springboot的前端_spring boot_13

而其它客户端(例如Postman)发送请求的时候,它没有说它要优先接收 text/html 类型的数据

spring boot前端模版 springboot的前端_java_14

所以,发生错误时,浏览器发送的请求会使用第一个方法处理,其它客户端会使用第二个方法处理

以第一个方法为例,浏览器发生错误去那个页面呢?代码会通过 resolveErrorView() 方法解析生成一个 ModelAndView 对象并返回,我们都知道 ModelAndView 包含了页面地址与页面内容(模型数据)

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
      Map<String, Object> model) {
   for (ErrorViewResolver resolver : this.errorViewResolvers) {
      ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
      if (modelAndView != null) {
         return modelAndView;
      }
   }
   return null;
}

它拿到所有的异常视图解析器 ErrorViewResolver 挨个尝试得到 ModelAndView,主要是使用 Springboot 缺省实现的一个错误视图解析器 ErrorViewResolverDefaultErrorViewResolver 来解析的,这也是 ErrorMvcAutoConfiguration 注入的4个重要组件中第三个

注入代码:

@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
   return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}

DefaultErrorViewResolver 主要代码与解释:

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

   private static final Map<Series, String> SERIES_VIEWS;

   static {		//客户端错误用4xx来表示,服务端错误用5xx来表示
      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);
   }
    
    	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model){
        //调用esolve方法通过具体状态码的值(例如404)得到modelAndView
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        //若上一步得不到,就通过状态码的系列来获取(例如404为4xx系列错误)
		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) {
        //默认SpringBoot可以去到一个页面:error/状态码
		String errorViewName = "error/" + viewName;		//viewName即传入的SERIES_VIEWS.get(status.series())状态码
		
        //如果模板引擎可以解析这个页面地址,就用模板引擎来解析
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
            //模板引擎解析返回上述的:error/状态码
			return new ModelAndView(errorViewName, model);
		}
        //如果模板引擎不可用,调用下述方法返回ModelAndView
		return resolveResource(errorViewName, model);
	}

	private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        //遍历静态资源文件夹
		for (String location : this.resources.getStaticLocations()) {
			try {
				Resource resource = this.applicationContext.getResource(location);
                //在静态资源文件夹中找  error/状态码.html
				resource = resource.createRelative(viewName + ".html");
                //如果找到了就返回
                if (resource.exists()) 
					return new ModelAndView(new HtmlResourceView(resource), model);			
			}
			catch (Exception ex) {
			}
		}
        //找不到返回null
		return null;
	}
}

所以根据 DefaultErrorViewResolver 解析流程,结论就是:

  1. 有模板引擎的情况下,将错误页面放在 templates/error/ 即可
  2. 没有模板引擎的情况下,将错误页面放在 静态资源文件夹/error/ 即可
  3. 我们可以使用4xx,5xx作为页面的文件名来匹配这种类型所有的错误,精确优先(因为代码执行顺序优先)

那页面能获得的信息都有哪些呢?我们回过头来看看我们的处理方法 errorHtml()

@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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
        //解析错误页面时候传入的模型中的数据model是在上面调用的getErrorAttributes()方法得到的
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

可以清楚的看到解析错误页面时候传入的模型中的数据model是在上面调用的getErrorAttributes()方法得到的,而getErrorAttributes()方法就是调用本类的 ErrorAttributes 引用属性对象的 getErrorAttributes() 方法

private final ErrorAttributes errorAttributes;

protected Map<String, Object> getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) {
   WebRequest webRequest = new ServletWebRequest(request);
   return this.errorAttributes.getErrorAttributes(webRequest, options);
}

ErrorAttributes为一个接口,只有一个默认的实现即为:DefaultErrorAttributes,它的作用就是帮我们在页面共享信息

这也是ErrorMvcAutoConfiguration 注入的4个重要组件中最后一个

DefaultErrorAttributes 中的 getErrorAttributes() 方法:

@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
   Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
   if (Boolean.TRUE.equals(this.includeException)) {
      options = options.including(Include.EXCEPTION);
   }
   if (!options.isIncluded(Include.EXCEPTION)) {
      errorAttributes.remove("exception");
   }
   if (!options.isIncluded(Include.STACK_TRACE)) {
      errorAttributes.remove("trace");
   }
   if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
      errorAttributes.put("message", "");
   }
   if (!options.isIncluded(Include.BINDING_ERRORS)) {
      errorAttributes.remove("errors");
   }
   return errorAttributes;
}

前前后后主要会链式调用下面一些方法将指定内容暴露在请求域中,供我们在错误页面共享!

@Override
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
   Map<String, Object> errorAttributes = new LinkedHashMap<>();
   errorAttributes.put("timestamp", new Date());
   addStatus(errorAttributes, webRequest);
   addErrorDetails(errorAttributes, webRequest, includeStackTrace);
   addPath(errorAttributes, webRequest);
   return errorAttributes;
}

private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
   Integer status = getAttribute(requestAttributes, RequestDispatcher.ERROR_STATUS_CODE);
   if (status == null) {
      errorAttributes.put("status", 999);
      errorAttributes.put("error", "None");
      return;
   }
   errorAttributes.put("status", status);
   try {
      errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
   }
   catch (Exception ex) {
      // Unable to obtain a reason
      errorAttributes.put("error", "Http Status " + status);
   }
}
private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest,
                             boolean includeStackTrace) {
    Throwable error = getError(webRequest);
    if (error != null) {
        while (error instanceof ServletException && error.getCause() != null) {
            error = error.getCause();
        }
        errorAttributes.put("exception", error.getClass().getName());
        if (includeStackTrace) {
            addStackTrace(errorAttributes, error);
        }
    }
    addErrorMessage(errorAttributes, webRequest, error);
}

private void addBindingResultErrorMessage(Map<String, Object> errorAttributes, BindingResult result) {
		errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. "
				+ "Error count: " + result.getErrorCount());
		errorAttributes.put("errors", result.getAllErrors());
	}

从上述代码可以看到我们页面能获取的信息(有模板引擎情况下):

  • timestamp:时间戳
  • status:状态码
  • error:错误提示
  • exception:异常对象
  • message:异常消息
  • errors:JSR303数据校验的错误

若我们没有配置错误页面(即在静态资源路径与templates文件夹下都没有页面),errorHtml()方法返回的ModelAndView为空,即下述代码最后一段逻辑,它就会默认返回一个 error 视图

@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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
   response.setStatus(status.value());
   ModelAndView modelAndView = resolveErrorView(request, response, status, model);
   return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

error视图又是什么呢?在ErrorMvcAutoConfiguration里面有配置,注入了一个名字为error的视图组件

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

   private final StaticView defaultErrorView = new StaticView();

   @Bean(name = "error")
   @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
   public BeanNameViewResolver beanNameViewResolver() {
      BeanNameViewResolver resolver = new BeanNameViewResolver();
      resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
      return resolver;
   }
}

它返回了this.defaultErrorView,即 new StaticView()

private static class StaticView implements View {

   private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);

   private static final Log logger = LogFactory.getLog(StaticView.class);

   @Override
   public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
         throws Exception {
      if (response.isCommitted()) {
         String message = getMessage(model);
         logger.error(message);
         return;
      }
      response.setContentType(TEXT_HTML_UTF8.toString());
      StringBuilder builder = new StringBuilder();
      Object timestamp = model.get("timestamp");
      Object message = model.get("message");
      Object trace = model.get("trace");
      if (response.getContentType() == null) {
         response.setContentType(getContentType());
      }
      builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
            "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
            .append("<div id='created'>").append(timestamp).append("</div>")
            .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
            .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
      if (message != null) {
         builder.append("<div>").append(htmlEscape(message)).append("</div>");
      }
      if (trace != null) {
         builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
      }
      builder.append("</body></html>");
      response.getWriter().append(builder.toString());
   }

其内的 render() 方法在被调用的时候,通过 StringBuilderresponse 的状态定制出了默认的相应错误页面的信息!


5、WebMvcConfigurer配置讲解



5.1、Spring官方API与文档介绍

spring boot前端模版 springboot的前端_spring_15

可以看出:WebMvcConfigurer 配置类其实是Spring内部的一种配置方式,采用 JavaBean 的形式来代替传统的 xml 配置文件形式进行针对框架个性化定制


文档介绍


If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without@EnableWebMvc.

如果您想保留那些 Spring Boot MVC 自定义,并进行更多的 MVC 自定义(拦截器、格式化器、视图控制器和其他特性),您可以添加自己的类型为WebMvcConfigurer的@Configuration类,但不添加@EnableWebMvc


If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

如果您想完全控制Spring MVC,可以添加您自己的带有@EnableWebMvc注释的@Configuration,或者添加您自己的带有@Configuration注释的DelegatingWebMvcConfiguration,如@EnableWebMvc的Javadoc中所描述的那样.



接口方法一览图



spring boot前端模版 springboot的前端_web_16



常用方法

/* 拦截器配置 */
default void addInterceptors(InterceptorRegistry registry) {}

/*静态资源处理*/
default void addResourceHandlers(ResourceHandlerRegistry registry) {}

/** 解决跨域问题 **/
default void addCorsMappings(CorsRegistry registry) {}

/* 视图跳转控制器 */
default void addViewControllers(ViewControllerRegistry registry) {}

/*这里配置视图解析器*/
default void configureViewResolvers(ViewResolverRegistry registry) {}

/* 配置内容裁决的一些选项*/
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {}

/* 默认静态资源处理器 */
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {}


5.2、addInterceptors:拦截器

  • addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例
public InterceptorRegistration addInterceptor(HandlerInterceptor interceptor) {
		InterceptorRegistration registration = new InterceptorRegistration(interceptor);
		this.registrations.add(registration);
		return registration;
	}
  • addPathPatterns:设置拦截器的过滤规则
public InterceptorRegistration addPathPatterns(String... patterns) {
   return addPathPatterns(Arrays.asList(patterns));
}
  • excludePathPatterns:用于设置不需要拦截的过滤规则
public InterceptorRegistration excludePathPatterns(String... patterns) {
   return excludePathPatterns(Arrays.asList(patterns));
}
  • 拦截器主要用途:进行用户登录的拦截,访问日志(记录访客的ip,来源),在线统计人数,字符集转换,身份验证等
@Override
public void addInterceptors(InterceptorRegistry registry) {

    registry.addInterceptor(new LoginHandlerInterceptor())
            .addPathPatterns("/**")
            .excludePathPatterns("/toAdminLoginPage","/","/toUserLoginPage")
            .excludePathPatterns("/adminLogin","/userLogin")
            .excludePathPatterns("/backstage/**","/commons/**","/forestage/**","/index/**","/login/**","/mine/**")
            .excludePathPatterns("/insertFeedback","/insertSubscribe");
}


5.2、addViewControllers:页面跳转

以前写SpringMVC的时候,如果需要访问一个页面,必须要写Controller类,然后再写一个方法跳转到页面,其实重写WebMvcConfigurer中的 addViewControllers() 方法即可达到效果了

@Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/toLogin").setViewName("login");
        registry.addViewController("/index.html").setViewName("index");
    }


5.3、addResourceHandlers:静态资源

比如,我们想自定义静态资源映射目录的话,只需重写 addResourceHandlers() 方法即可

  • addResoureHandler:对外暴露的访问路径
  • addResourceLocations:内部文件放置的路径
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/my/**").addResourceLocations("classpath:/my/");
    }
}


5.4、configureDefaultServletHandling:默认静态资源处理器

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
}

看一下configurer.enable()方法的源码:

spring boot前端模版 springboot的前端_web_17



此时会注册一个默认的Handler:DefaultServletHttpRequestHandler,该类的作用如下图

spring boot前端模版 springboot的前端_spring boot_18


根据Spring官方的解释,该类用来处理静态文件的 ”default“ Servlet,它会尝试映射 /*。当DispatcherServelt映射 / 时,从而覆盖Servlet容器对静态资源的默认处理,到该处理程序的映射通常应作为链中的最后一个进行排序,以便它只在没有其他更具体的映射(即到控制器)匹配的情况下执行

请求通过 RequestDispatcher 转发来处理通过 setDefaultServletName 指定的名称。

在大多数情况下,defaultServletName 不需要显式设置,如处理程序在初始化时检查是否存在已知的默认Servlet容器,如Tomcat、Jetty、Resin、WebLogic和WebSphere。但是,当在一个容器中运行时,如果默认Servlet的名称未知,或者已经通过服务器配置对其进行了自定义,则需要显式地设置 defaultServletName

其内处理请求的方法代码如下

@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

   Assert.state(this.servletContext != null, "No ServletContext set");
   RequestDispatcher rd = this.servletContext.getNamedDispatcher(this.defaultServletName);
   if (rd == null) {
      throw new IllegalStateException("A RequestDispatcher could not be located for the default servlet '" +
            this.defaultServletName + "'");
   }
   rd.forward(request, response);
}

即根据 this.defaultServletName 尝试来获取转发器,若能获取到就进行请求的转发,获取不到就报错

this.defaultServletName 即为当前应用上下文:this.servletContext 或者我们传入的 defaultServletName

public void enable() {
   enable(null);
}
/**
 * Enable forwarding to the "default" Servlet identified by the given name.
 * <p>This is useful when the default Servlet cannot be autodetected,
 * for example when it has been manually configured.
 * @see DefaultServletHttpRequestHandler
 */
public void enable(@Nullable String defaultServletName) {
   this.handler = new DefaultServletHttpRequestHandler();
   if (defaultServletName != null) {
      this.handler.setDefaultServletName(defaultServletName);
   }
   this.handler.setServletContext(this.servletContext);
}



5.5、configureViewResolvers:视图解析器

用来配置视图解析器的,该方法的参数 ViewResolverRegistry 是一个注册器,用来注册你想自定义的视图解析器等

@Bean
public InternalResourceViewResolver getMyViewResolver()
{
	InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver();
	//请求视图文件的前缀地址
	internalResourceViewResolver.setPrefix("/WEB-INF/jsp/");
	//请求视图文件的后缀
	internalResourceViewResolver.setSuffix(".jsp");
	return internalResourceViewResolver;
}
 
//视图配置
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
	super.configureViewResolvers(registry);
	registry.viewResolver(getMyViewResolver());
}


5.6、addCorsMappings:跨域



官方文档

Cross-origin resource sharing](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) (CORS) is a W3C specification implemented by most browsers that lets you specify in a flexible way what kind of cross-domain requests are authorized., instead of using some less secure and less powerful approaches such as IFRAME or JSONP.

跨源资源共享(CORS)是由大多数浏览器实现的W3C规范,它允许您以灵活的方式指定授权哪种类型的跨域请求。,而不是使用IFRAME或JSONP等安全性较低、功能较弱的方法

As of version 4.2, Spring MVC supports CORS. Using controller method CORS configuration with @CrossOrigin annotations in your Spring Boot application does not require any specific configuration. Global CORS configuration can be defined by registering a WebMvcConfigurer bean with a customized addCorsMappings(CorsRegistry) method, as shown in the following example:

从4.2版本开始,Spring MVC支持CORS

在Spring Boot应用程序中使用带有@CrossOrigin批注的控制器方法CORS配置不需要任何特定的配置

可以通过使用自定义的addCorsMappings(CorsRegistry)方法注册WebMvcConfigurer bean来定义全局CORS配置,如以下示例所示:

@Configuration(proxyBeanMethods = false)
public class MyConfiguration {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**");
            }
        };
    }
}



自己的例子:

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")// 允许跨域访问的路径
            .allowedOrigins("*")// 允许跨域访问的源
            .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")// 允许请求方法
            .maxAge(168000)// 预检间隔时间
            .allowedHeaders("*")// 允许头部设置
            .allowCredentials(true);  // 是否发送cookie
}

但是interceptor和addCorsMappings一起的话addCorsMappings机会失效,应该是顺序问题,interceptor覆盖了


5.7、configureMessageConverters:信息转换器

// 消息内容转换配置   配置fastJson返回json转换
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    //创建fastJson消息转换器
    FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
    //创建配置类
    FastJsonConfig fastJsonConfig = new FastJsonConfig();
    //修改配置返回内容的过滤
    fastJsonConfig.setSerializerFeatures(
            SerializerFeature.DisableCircularReferenceDetect,
            SerializerFeature.WriteMapNullValue,
            SerializerFeature.WriteNullStringAsEmpty
    );
    fastConverter.setFastJsonConfig(fastJsonConfig);
    //将fastjson添加到视图消息转换器列表内
    converters.add(fastConverter);
}