SpringBoot 之 MVC

时间:2023-03-08 15:36:28

SpringBoot MVC 和静态资源

首先,我们一定要搞清楚,mvc 配置和 static 配置的联系和区别。 mvc 配置其实就是给 spring mvc 框架用的, 具体来说, 比如 @RequestMapping, 它会返回一个ModelAndView。 我们对这个ModelAndView进行渲染的时候, 需要查找 view, 具体怎么查找呢? 通过mvc 的各种配置。 当然,最终是交给了某个 viewResolver 进行解析。静态资源,其实也是经过了spring mvc,具体来说是ResourceHttpRequestHandler,但ResourceHttpRequestHandler 几乎不需要配置。spring mvc几乎没有提供静态资源的配置项(除了一个静态资源的映射),boot倒是提供了 关于位置和后缀的配置, 具体参见我的boot相关博客。

SpringBoot 中的MVC配置

boot 提供了很多 mvc 配置项,

# SPRING MVC (WebMvcProperties)
spring.mvc.async.request-timeout= # Amount of time (in milliseconds) before asynchronous request handling times out.
spring.mvc.date-format= # Date format to use. For instance `dd/MM/yyyy`.
spring.mvc.dispatch-trace-request=false # Dispatch TRACE requests to the FrameworkServlet doService method.
spring.mvc.dispatch-options-request=true # Dispatch OPTIONS requests to the FrameworkServlet doService method.
spring.mvc.favicon.enabled=true # Enable resolution of favicon.ico.
spring.mvc.formcontent.putfilter.enabled=true # Enable Spring's HttpPutFormContentFilter.
spring.mvc.ignore-default-model-on-redirect=true # If the content of the "default" model should be ignored during redirect scenarios.
spring.mvc.locale= # Locale to use. By default, this locale is overridden by the "Accept-Language" header.
spring.mvc.locale-resolver=accept-header # Define how the locale should be resolved.
spring.mvc.log-resolved-exception=false # Enable warn logging of exceptions resolved by a "HandlerExceptionResolver".
spring.mvc.media-types.*= # Maps file extensions to media types for content negotiation.
spring.mvc.message-codes-resolver-format= # Formatting strategy for message codes. For instance `PREFIX_ERROR_CODE`.
spring.mvc.servlet.load-on-startup=- # Load on startup priority of the Spring Web Services servlet.
spring.mvc.static-path-pattern=/** # Path pattern used for static resources.
spring.mvc.throw-exception-if-no-handler-found=false # If a "NoHandlerFoundException" should be thrown if no Handler was found to process a request.
spring.mvc.view.prefix= # Spring MVC view prefix.
spring.mvc.view.suffix= # Spring MVC view suffix.

最常用的大概是:

spring.mvc.view.prefix=/somePath/
spring.mvc.view.suffix=.html

这个两个都是对 viewResolver  进行配置用的。这两个值默认都是空, 为空的意思是说,@RequestMapping 返回的是什么, 那么boot 就直接查找那个view, 没有前缀和后缀 。这会产生一种奇怪的情况, 那就是, 比如映射是@RequestMapping("lk")  返回 "aa" 或者"/aa" ( 结果是一样的 ),  如果aa 文件不存在于跟目录,那么就返回 404; 如果存在,不管 aa 文件内容是什么, 由于浏览器不 识别器类型, 于是变成了下载。。

boot 在查找mvc  view 的时候, 是去静态资源目录去查找的,  也就是 spring.resources.static-locations 对应的目录。

如果我们直接反问静态资源, 不管是 spring.mvc.view.prefix 还是 spring.mvc.view.suffix , 还是 其他的 spring.mvc.view 开头的属性, 怎么配置都是没影响的。 但是@RequestMapping返回的view, 却又是在 静态目录进行查找的(非thymeleaf 等视图模板的情况, thymeleaf等是在templates下查找的, 而且 spring.mvc.view.prefix 及 spring.mvc.view.suffix 都是有效的。 这里应该要理清, 不然容易搞混。

再强调一遍, 访问静态资源, 一定要路径完全匹配, 否则就被 boot 认为是访问 动态资源, 比如 mvc 请求!

SpringBoot MVC 和JSP

如果我们希望使用 jsp 做动态资源的渲染, 那么, 我们应该这么配置:

spring.mvc.view.suffix=.jsp

但是呢, 这样做之后,还是不够的。 因为boot 内嵌的 tomcat容器默认不能解析 jsp, 关于 boot 中使用jsp,参见我的另外的博客。 一般来说,最好还是不要再boot 中使用jsp。谁用谁苦自己知道, 我当初就被坑了很久。 官方的说明是这样的:

If possible, JSPs should be avoided. There are several known limitations when using them with embedded servlet containers. —— 尽量不要使用jsp !!

为什么? 因为各种局限:

27.4.5 JSP Limitations
When running a Spring Boot application that uses an embedded servlet container (and is packaged as an executable archive), there are some limitations in the JSP support.

局限1 With Tomcat, it should work if you use war packaging. That is, an executable war works and is also deployable to a standard container (not limited to, but including Tomcat). An executable jar does not work because of a hard-coded file pattern in Tomcat.
局限2  With Jetty, it should work if you use war packaging. That is, an executable war works, and is also deployable to any standard container.
局限3 Undertow does not support JSPs.
局限4 Creating a custom error.jsp page does not override the default view for error handling. Custom error pages should be used instead.

There is a JSP sample so that you can see how to set things up

这么简单的英语我就不翻译了。其实官方也是有提供boot集成jsp示例的,jsp官方的示例是:https://github.com/spring-projects/spring-boot/tree/v2.0.0.M7/spring-boot-samples/spring-boot-sample-web-jsp。 我试过,确实可行(不废话吗,官方的能有啥毛病....) 但是尽管如此,还是很容易踩坑,一不小心就。。

SpringBoot MVC 自动配置是如何生效的?

boot 的mvc 功能大致是通过 WebMvcAutoConfiguration,以及DispatcherServletAutoConfiguration ,EmbeddedServletContainerAutoConfiguration 完成的。它做了很多工作。 多到, 很多时候, 出了问题, 我们搞不清, 只能通过源码来了解。

我们看一下大致有那些 自动配置:

配置了:

DispatcherServlet 这个当然是必须的,在传统项目中,我们可能通过web.xml 来配置,boot 是使用编程方式配置
MultipartResolver 上传解析器
ServletRegistrationBean 用来定制传统的Servlet,一般配合@Bean使用。
OrderedHttpPutFormContentFilter 对method 为put即patch 的form 请求的做一些参数处理
OrderedHiddenHttpMethodFilter 把post请求中_method 参数转换为 请求的method,
ResourceChainResourceHandlerRegistrationCustomizer 资源链,前端资嵌套引用时会用到
WelcomePage 首页,欢迎页面
ResourceResolver 静态资源
RequestMappingHandlerAdapter 
RequestMappingHandlerMapping
Validator
ConfigurableWebBindingInitializer 
ExceptionHandlerExceptionResolver
ContentNegotiationManager
InternalResourceViewResolver
BeanNameViewResolver
ContentNegotiatingViewResolver
LocaleResolver
Formatter<Date>
FaviconConfiguration
ErrorPagexxx

官方说明

它完成了很多的工作。引用 官方 27.1.1 Spring MVC Auto-configuration(https://docs.spring.io/spring-boot/docs/2.0.0.M7/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration)的描述:

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.

The auto-configuration adds the following features on top of Spring’s defaults:

Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
Support for serving static resources, including support for WebJars (covered later in this document).
Automatic registration of Converter, GenericConverter, and Formatter beans.
Support for HttpMessageConverters (see below).
Automatic registration of MessageCodesResolver (covered later in this document).
Static index.html support.
Custom Favicon support (covered later in this document).
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

如果你想使用使用spring boot mvc 的特性的同时,想增加一些mvc 的配置,比如拦截器,格式化去,视图控制器等,你可以写一个类继承WebMvcConfigurer,注解 @Configuration,但不要有@EnableWebMvc注解。如果你只是想定制化RequestMappingHandlerMapping, RequestMappingHandlerAdapter, 或者ExceptionHandlerExceptionResolver,那么你只要定义一个WebMvcRegistrationsAdapter 实例就ok了

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

如果你想自己控制整个 Spring MVC,你可以写一个自己的配置类,同时注解上@Configuration 和 @EnableWebMvc。

关于OrderedHttpPutFormContentFilter , 大致是这样的:

protected void doFilterInternal(final HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (("PUT".equals(request.getMethod()) || "PATCH".equals(request.getMethod())) && this.isFormContentType(request)) {
HttpInputMessage inputMessage = new ServletServerHttpRequest(request) {
public InputStream getBody() throws IOException {
return request.getInputStream();
}
};
MultiValueMap<String, String> formParameters = this.formConverter.read((Class)null, inputMessage);
if (!formParameters.isEmpty()) {
HttpServletRequest wrapper = new HttpPutFormContentFilter.HttpPutFormContentRequestWrapper(request, formParameters);
filterChain.doFilter(wrapper, response);
return;
}
} filterChain.doFilter(request, response);
}

也就是把put/patch请求的 body解析为请求参数,body 应该是这个格式: aa=11&bb=22; 使用 & 和= 来分隔。就跟我们的get 的querystring是一样的。然后使用 URLDecoder.decode 来解码。 不太懂这里的FormHttpMessageConverter 为什么这么设计,感觉怪怪的。