Spring MVC里的DispatchServlet
- 前言
- 1.Spring Web MVC
- 1.1 DispatcherServlet(中央调度器)
- 1.1.1 Context Hierarchy(上下文层次结构)
- 1.1.2 Special Bean Types(特定的bean类型)
- 1.1.3 Web MVC Config
- 1.1.4 Servlet Config
- 1.1.5. Processing
- 1.1.6 Path Matching(路径匹配)
- 1.1.7 Interception(拦截器)
- 1.1.8 Exceptions(异常)
- Chain of Resolvers(解析链)
- Container Error Page(容器错误页)
- 1.1.9 View Resolution(视图解析)
- Handling(管理ViewResolver)
- Redirecting(重定向)
- Forwarding(转发)
- Content Negotiation(内容协商)
- 1.1.10 Local(地区)
- Time Zone(时区)
- Header Resolver(头解析器)
- Cookie Resolver(Cookie解析器)
- Session Resolver(Session解析器)
- Locale Interceptor(地区拦截器)
- 1.1.11 Themes(主题)
- Defining a theme(定义一个主题)
- Resolving Themes(解析主题)
- 1.1.12 Multipart Resolver(多部分解析器)
- Servlet Multipart Parsing(Servlet 多部分解析)
- 1.1.13 Logging(日志记录)
- Sensitive Data (敏感数据)
- 写在最后
前言
在进入这一节之前我建议各位对Java Web有比较熟悉的了解以后再进行,否则阅读起来会比较困难且难以理解。诸如Servlet生命周期、ServletContext 、ApplicationContext等等。如果不太熟悉Java Web,可以参考以下文章: JavaWeb——Servlet(全网最详细教程包括Servlet源码分析) 以及
ServletContext与ApplicationContext的区别
请注意本文附带有其中翻译以及作者的的一些理解和解释,对于原文中的外链接并未有跳转。还有就是作者不是英语专业出身,翻译只是为了锻炼自己的英语阅读水平以及增加常用词汇量,很多时候都借助了翻译软件。
其中PS及黄字标注部分是作者阅读时的补充。
原文官网地址:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet-context-hierarchy
版本为:6.0.5
下面正式发车
本部分文档介绍了对基于Servlet API构建并部署到Servlet容器的Servlet堆栈web应用程序的支持。各个章节包括 Spring MVC, View Technologies, CORS Support, 以及 WebSocket Support。有关反应式堆栈web应用程序,请参阅Web on Reactive Stack。
1.Spring Web MVC
Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就包含在Spring框架中。正式名称“Spring Web MVC”来自其源模块(spring-webmvc)的名称,但更常见的是“Spring MVC”。
与Spring Web MVC相提并论的是Spring 框架5.0中包含的一个名为“Spring WebFlux”的反应式堆栈网页框架,这个名字同样是源于其模块(名为spring-webflux)。这部分仅包含Spring Web MVC。下一部分包含Spring WebFlux。
Spring WebFlux是Spring框架中用于构建响应式Web应用程序的模块。
它是Spring 5引入的一项重要功能,旨在支持基于异步和非阻塞的编程模型,以应对高并发和高吞吐量的场景。
Spring WebFlux构建在Reactive Streams规范之上,提供了两种编程模型:基于响应式流的WebFlux和基于反应式处理器的WebFlux。
1. 基于响应式流的WebFlux:这种模型使用Reactive Streams的Publisher和Subscriber接口,允许您以异步和非阻塞的方式处理请求和响应。
它适用于处理大量并发请求和响应,并能够实现高吞吐量的应用程序。
2. 基于反应式处理器的WebFlux:这种模型使用类似于Java 8的CompletableFuture的Mono和Flux接口,
提供了一种更直观的编程模型,用于处理单个结果或多个结果的异步操作。
与Spring MVC的区别:
1、Spring MVC是传统的基于Servlet的MVC框架,使用同步阻塞的方式处理请求和响应。
它适用于传统的Web应用程序,但在高并发和高吞吐量的情况下可能面临性能瓶颈。
2、Spring WebFlux是基于响应式编程的框架,使用异步非阻塞的方式处理请求和响应。
它适用于响应式和异步编程场景,特别适合构建高性能、高并发的Web应用程序。
3、Spring WebFlux支持Reactive Streams规范,提供了一种响应式的编程模型,
可以更好地处理高并发和高吞吐量的请求。
有关标准信息、Servlet容器和Jakarta EE版本范围的兼容性,请参阅Spring Framework Wiki。
1.1 DispatcherServlet(中央调度器)
与许多其他web框架一样,Spring MVC是围绕前端控制器模式(Front Controller Pattern)设计的,其中央servlet–DispatcherServlet ,为请求处理提供了共享算法,而实际工作则由可配置的委托组件执行。该模型非常灵活,支持多种工作流。
PS: 所谓前端控制器模式(Front Controller Pattern):用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理,该处理程序可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。
DispatcherServlet和其他的Servlet一样,需要根据Servlet使用规范,通过Java配置或者在web.xml里进行声明和映射。反而言之,DispatcherServlet通过Spring配置来发现其所需的代理组件,譬如请求映射、视图解析、异常处理等等。(request mapping, view resolution, exception handling)
下面是Java语言注册并初始化了DispatcherServlet的配置信息示例,它由Servlet容器自动检测(点击查看Servlet Config)。
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
除了直接使用ServletContext API之外,还可以扩展AbstractAnnotationConfigDispatcherServletInitializer并重写特定方法(请参阅Context Hierarchy下的示例)。
对于编程用例,GenericWebApplicationContext可以用作AnnotationConfigWebApplicationContext的替代。有关详细信息,请参阅GenericWebApplicationContext javadoc。
GenericWebApplicationContext(通用web程序上下文)是Spring框架在Web环境中使用的通用应用程序上下文,
可以用于Web应用程序中的Bean管理、依赖注入和其他Spring相关功能。
下面是一个web.xml中DispatcherServlet的注册及初始化的配置示例:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
Spring Boot遵循以下不同的初始化顺序。相较于上面的与Servlet容器的生命周期挂钩,Spring Boot使用Spring配置信息去启动并且使用嵌入式Servlet容器。获取更多的细节,请查看Spring Boot文档。
1.1.1 Context Hierarchy(上下文层次结构)
DispatcherServlet需要一个由它自己的配置的WebApplicationContext(一个ApplicationContext的简单扩展)。WebApplicationContext具有指向ServletContext及其关联的Servlet的链接。并且与ServletContext绑定,以便应用程序在需要访问WebApplicationContext时可以使用RequestContextUtils上的静态方法来查找WebApplicationContext。
ApplicationContext是Spring框架的核心容器。
它是一个接口,它提供了一个高级的配置机制来管理应用程序中的Bean(对象)以及它们之间的依赖关系。
ApplicationContext负责初始化Bean,将它们连接在一起,并在需要时将它们提供给应用程序的其他部分。
ServletContext是JavaEE规范中的一个接口,它代表了一个Web应用程序的运行环境。
使用ServletContext,Spring能够在Web应用程序中进行更深入的集成,
可以轻松地获取Web应用程序的上下文信息,并与其他Servlet、过滤器、监听器等Web组件进行交互。
对于许多的应用来说,单个的WebApplicationContext是简单且足够的。但是它同样可以拥有一个context hierarchy,其中一个root WebApplicationContext 在多DispatcherServlet实例(或者其他的Servlet)中共享,每个实例都有自己的child WebApplicationContext。查看 Additional Capabilities of the ApplicationContext以获取更多的context hierarchy特性。
上面的 Root WebApplicationContext通常包含一些基础结构的bean,例如需要在多个Servlet实例之间共享的数据存储库和业务服务。事实上,这些bean是被继承的,并且可以在特定Servlet的 child WebApplicationContext中被重写(即重新声明),该child WebApplicationContext一般包含给定的Servlet的本地bean。下面的图片展示了它们之间的关系:

下面是一个WebApplicationContext层次结构配置的示例:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
如果一个应用不需要上下文层次结构,那么应用会返回通过getRootConfigClasses()返回所有的配置,并且从getServletConfigClasses()返回null。
下面展示了与上面相同的web.xml里的配置:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
如果不需要应用程序上下文层次结构,则应用程序可以仅配置“root”上下文,并将contextConfigLocation Servlet参数留空。
1.1.2 Special Bean Types(特定的bean类型)
DispatcherServlet委托特定的bean处理请求并呈现适当的响应。我们说的“特定的bean”是指由Spring管理、实现了框架契约的对象实例。它们通常带有内部契约,但是你可以自定义它们的属性并且拓展或者替换它们。
PS:这里的框架契约以及内部契约(framework contracts 、built-in contracts),没找到相关资料。听起来像一种创建bean的规则?
下面表格列出DispatcherServlet会检索的特定的bean:
| 特定的Bean | 说明 |
|---|---|
HandlerMapping | 将请求与用于预处理和后处理的拦截器列表一起映射到处理程序。该映射基于一些标准,其细节因HandlerMapping实现而各有不同。HandlerMapper有两个主要的实现,一个是RequestMappingHandlerMapping(支持@RequestMapping 注解方法)另一个是SimpleUrlHandlerMapping(维持了URI路径模式到处理程序的显式注册) |
HandlerAdapter | 帮助DispatcherServlet去调用请求映射到的处理程序,不管处理程序是如何被调用的。举个例子来说,调用一个注解的控制器需要去解析注解。HandlerAdapter的主要作用就是对DispatcherServlet遮蔽那样的细节。 |
| HandlerExceptionResolver | 解析异常的策略,可能映射异常到处理程序,HTML的错误页,或者其他的处理。查看Exception。 |
ViewResolver | 解析符合逻辑的基本字符串视图名称,从处理程序返回一个呈现给响应的实际视图。查看View Resolution 以及 View Technologies。 |
| LocaleResolver, LocaleContextResolver | 解析客户端正在使用的区域设置以及可能的时区,以便能够提供国际化视图。查看Local。 |
| ThemeResolver | 解析web应用程序可以使用的主题 ,例如,提供个性化布局。查看Themes。 |
MultipartResolver | 借助某些多部分解析库处理多部分请求(例如,浏览器表单文件上传)的抽象类。查看Multipart Resolver。 |
| FlashMapManager | 存储和检索可用于将属性从一个请求传递到另一个请求的“输入”和“输出”FlashMap,通常通过重定向。 |
以下是一个请求处理的的简单描述过程,可以清楚地明白上述部分Bean的作用:
HTTP请求到达DispatcherServlet。
DispatcherServlet通过HandlerMapping找到适合的Handler(Handler一般可以简单认为是控制器)。
DispatcherServlet使用HandlerAdapter调用Handler的处理方法。
Handler处理请求,执行业务逻辑,并生成响应结果。
HandlerAdapter封装响应结果为ModelAndView对象。
DispatcherServlet将ModelAndView中的视图展示给用户(一般是JSON数据)。
1.1.3 Web MVC Config
应用程序可以声明的、处理请求所需的基础对象已经在 Special Bean Types中列出来了。DispatcherServlet在WebApplicationContext中检查每个special bean。如果没有匹配的bean类型,则返回DispatcherServlet.properties中列出的默认类型。
在大部分情况下,MVC Config是最佳启动点。因为它在Java或者XML中声明了需要的bean,并且提供了一种更高级的配置回调API来自定义。
值得注意的是,Spring Boot依赖MVC Java配置信息去配置Spring MVC,并提供了许多额外便利的选项。
1.1.4 Servlet Config
在Servlet环境中,您可以选择以编程方式配置Servlet容器作为替代方案或与web.xml文件结合使用。下面展示了一个DispatcherServlet注册例子:
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
WebApplicationInitializer是SpringMVC提供的一个接口,可确保检测到您的实现并自动用于初始化任何Servlet 3容器。WebApplicationInitializer是抽象基类:AbstractDispatcherServletInitializer的实现,通过重写方法来指定servlet映射和DispatcherServlet配置的位置,从而更容易注册DispatcherServlet。
Servlet 3 container(Servlet 3容器):
通常是指支持 Servlet 3.0 规范的 Java Web 容器
对于使用基于Java的Spring配置的应用程序,建议这样做,如下例所示:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
如果使用基于XML的Spring配置,则应直接从AbstractDispatcherServletInitializer进行扩展,如下例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
AbstractDispatcherServletInitializer还提供了一种方便的方法来添加Filter实例,并将它们自动映射到DispatcherServlet,如下例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
每个过滤器都会根据其具体类型添加一个默认名称,并自动映射到DispatcherServlet。
AbstractDispatcherServletInitializer的受保护方法isAsyncSupported提供了一个可以在DispatcherServlet和映射到它的所有筛选器上启用异步支持的地方。默认情况下,此标志设置为true。
这里的异步支持是指:
允许Servlet在接收到请求后不立即处理,而是将请求委托给另一个线程去处理,
然后在后台继续处理其他请求。
这样可以提高Web应用程序的并发性和性能,特别是对于涉及到耗时操作的请求。
最后,如果需要进一步自定义DispatcherServlet,可以重写createDispatcherServlet方法。
1.1.5. Processing
DispatcherServlet处理请求如下:
WebApplicationContext在请求中作为控制器和流程中其他元素中可使用的属性进行搜索和绑定。默认情况下,它绑定在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE键下。
这里的"其他元素"可能包括
拦截器(Interceptors)、
过滤器(Filters)、
监听器(Listeners)、
视图解析器(View Resolvers)、
HandlerExceptionResolver(异常处理器)、
资源处理器(Resource Handlers)等。
地区解析器(locale resolver)绑定到请求,以让流程中的元素处理请求时使用地区解析(呈现视图、准备数据等)。如果不需要地区解析,则不需要地区解析器。主题解析器(theme resolver)绑定到请求,以让视图等元素确定要使用的主题。如果不使用主题,可以忽略它。- 如果指定
多部分文件解析器(multipart file resolver),将检查请求里的多部分。如果找到,则将请求包装在MultipartHttpServletRequest中,以供流程中的其他元素进一步处理。有关多部分处理的更多信息,请参阅Multipart Resolver。 - 搜索适当的
处理程序(handler)。如果找到了处理程序,则运行与处理程序(预处理器、后处理器和控制器)关联的执行链(execution chain),以准备渲染模型。可替代方案是,对于带注释的控制器,可以(在HandlerAdapter中)呈现响应,而不是返回视图。 - 如果有
返回模型,则会渲染视图。如果没有返回模型(可能是由于预处理器或后处理器拦截了请求,可能是出于安全原因),则不会呈现视图,因为请求可能已经完成。
WebApplicationContext中声明的HandlerExceptionResolver bean(后文会详细讲)用于解决请求处理过程中抛出的异常。这些异常解析器允许定制逻辑以解决异常。查看Exception更多信息。
对于支持HTTP缓存的程序(HTTP caching),处理程序(handler)可以使用WebRequest的checkNotModified方法以及其他进一步的选项来注释控制器,如“控制器的HTTP缓存”(HTTP Caching for Controllers)中所描述的那样。
HTTP Caching for Controllers(控制器的HTTP缓存)是指在Spring MVC框架中,
对"Controller处理的结果"进行HTTP缓存的一种机制。
通过将Servlet初始化参数(init param元素)添加到web.xml文件中的Servlet声明中,可以自定义各个DispatcherServlet实例。下表列出了支持的参数:
表1 DispatcherServlet初始化参数
| 特定的Bean | 说明 |
|---|---|
contextClass | 实现ConfigurableWebApplicationContext的类,该类将由此Servlet实例化并在本地配置。默认情况下,使用XmlWebApplicationContext。 |
contextConfigLocation | 传递给上下文实例(由contextClass指定)的字符串,以指示可以在哪里找到上下文。该字符串可能由多个字符串(使用逗号作为分隔符)组成,以支持多个上下文。如果多个上下文位置包含定义了两次的bean,则最新位置优先。 |
namespace | WebApplicationContext的命名空间。默认为[servlet-name]-servlet。 |
throwExceptionIfNoHandlerFound | 当未找到请求的处理程序时(handler),会抛出NoHandlerFoundException。该异常会被HandlerExceptionResolver(例如,使用@ExceptionHandler注解的控制器方法)捕获,并将其作为其他异常处理。默认情况下,这设置为false,在这种情况下,DispatcherServlet将响应状态设置为404(NOT_FOUND),而不会引发异常。请注意,如果还配置了默认servlet处理,则无法解析的请求总是被转发到默认servlet,并且不会引发404。 |
1.1.6 Path Matching(路径匹配)
Servlet API将完整的请求路径公开为requestURI,并将其进一步细分为contextPath、servletPath和pathInfo,它们的值取决于Servlet的映射方式。从这些输入中,SpringMVC需要确定用于映射处理程序的查找路径,如果可以的话,应排除对应的contextPath以及所有servletMapping的前缀。
在Spring MVC框架中,Servlet API(javax.servlet)提供了许多与HTTP请求相关的接口和方法,其中包括HttpServletRequest对象。
HttpServletRequest对象公开了完整的请求路径,即'requestURI',以及进一步细分的路径信息:contextPath、servletPath和pathInfo。
1. requestURI:完整的请求路径,包括ContextPath、ServletPath和PathInfo。
例如,如果请求的URL是"http://example.com/myapp/servlet/myservlet/foo",
那么requestURI就是"/myapp/servlet/myservlet/foo"。
2. contextPath:Web应用程序的上下文路径,表示请求被映射到的应用程序的根路径。
例如,如果Web应用程序被部署到路径"/myapp",那么contextPath就是"/myapp"。
3. servletPath:Servlet的路径,表示请求被映射到的具体Servlet的路径。
例如,如果一个Servlet映射到路径"/servlet/*",那么servletPath就是"/servlet"。
4. pathInfo:附加的路径信息,表示Servlet映射之后的额外路径信息。
例如,如果一个Servlet映射到路径"/servlet/*",并且请求是"/servlet/myservlet/foo",那么pathInfo就是"myservlet/foo"。
SpringMVC框架在处理请求时需要"确定用于映射处理程序(Controller)的查找路径"。为了得到正确的查找路径,SpringMVC会从这些输入中获取必要的信息,并排除不需要的部分。具体来说:
SpringMVC需要获取用于映射处理程序的查找路径。"通常这个查找路径是从servletPath开始的",
因为请求会被映射到具体的Servlet。
为了得到准确的查找路径,SpringMVC"需要排除contextPath以及所有servletMapping的前缀"。
因为在确定查找路径时,已经知道请求被映射到了具体的Servlet,不需要将这些前缀计算在内。
得到的servletPath和pathInfo被解码,这使得它们不可能直接与完整的requestURI进行比较得到lookupPath,并且使得requestURI也必须解码。而这种做法本身就存在一些问题,因为路径可能包含编码的保留字符,如“/”或“;”,这些字符在解码后可能会改变路径的结构,这也可能导致安全问题。此外,Servlet容器可能会在不同程度上规范化Servlet路径,这使得无法根据requestURI执行startsWith比较。
这里的规范化是指"将请求的URL路径处理成标准格式",以便在Servlet容器中正确地定位和调用对应的Servlet。
规范化的过程包括"去除多余的斜杠、解码URL编码字符"等操作。
"startsWith"是Java中String类的一个方法,"用于判断一个字符串是否以指定的前缀开头"。
这就是为什么最好避免依赖基于前缀的servlet路径映射类型附带的servlet路径。如果DispatcherServlet被映射为带有“/”或没有前缀“/*”的默认Servlet,并且Servlet容器是4.0+,那么Spring MVC能够检测Servlet映射类型,并完全避免使用Servlet路径和路径信息。在3.1 Servlet容器上,假设Servlet映射类型相同,则可以通过MVC配置中的路径匹配提供alwaysUseFullPath=true的UrlPathHelper来实现等效映射。
幸运的是,默认的Servlet映射“/”是一个不错的选择。然而,仍然存在一个问题,即需要对requestURI进行解码,以便能够与控制器映射进行比较。这也是不太好的一种做法,因为有可能解码改变路径结构的保留字符。如果不需要这样的字符,那么您可以拒绝它们(如Spring Security HTTP防火墙),或者您可以将UrlPathHelper配置为urlDecode=false,但控制器映射需要与编码路径匹配,这可能并不总是很好地工作。此外,有时DispatcherServlet需要与另一个Servlet共享URL空间,并且可能需要通过前缀进行映射。
Spring MVC使用UrlPathHelper来解析URL路径,默认情况下,它会对URL路径进行解码(URL decode),
这意味着将URL编码的字符还原为原始字符。
例如,将"%20"还原为空格。但在某些情况下,如果不希望进行解码,可以将UrlPathHelper配置为urlDecode=false。
当使用 PathPatternParser 和解析后的模式作为 String 路径匹配的替代方案时,可以解决上述问题,从而避免使用 AntPathMatcher 进行 String 路径匹配。PathPatternParser从Spring MVC 5.3版本开始可用,并且从Spring MVC 6.0版本开始默认启用。与需要查找路径解码或控制器映射编码的AntPathMatcher不同,解析后的PathPattern与称为RequestPath的路径的解析表示一一匹配。这允许逐个解码和清理路径段的值,而不会改变路径的结构。解析后的PathPattern还支持使用Servlet路径前缀映射,而这种情况下同样可以使用Servlet路径映射,并且前缀简单,即没有编码字符。有关模式语法细节和比较,请参见“Pattern Comparison”。
为什么避免使用AntPathMatcher:
1、不支持完整的 RESTful 风格路径:AntPathMatcher 是基于 Ant 风格的路径表达式进行匹配的,它的语法较为简单,不支持完整的 RESTful 风格的路径。
如果应用程序需要更复杂的路径匹配规则,AntPathMatcher 可能无法满足需求。
2、无法精确匹配路径参数:在一些场景下,可能需要精确匹配路径中的某些参数,而 AntPathMatcher 并不直接支持这种精确匹配。
3、不支持多路径分隔符:AntPathMatcher 默认使用斜杠 ("/") 作为路径分隔符,不支持多个不同的路径分隔符。
4、性能问题:AntPathMatcher 的性能在处理大量路径匹配时可能不如其他更专门的路径匹配器,例如 PathPatternParser。
1.1.7 Interception(拦截器)
所有HandlerMapping实现都支持拦截器(interception),拦截器在您希望对特定请求执行特定功能时非常有用,例如检查主体(principal)。拦截器必须实现org.springframework.web.servlet包中的HandlerInterceptor接口,并包含三个方法,这些方法提供足够的灵活性来进行各种预处理和后处理:
preHandle(…):在实际处理程序运行之前
postHandle(…):在处理程序运行之后
afterCompletion(…):在请求完成后
preHandle(…)方法返回一个布尔值。您可以使用该方法来中断或继续执行链的处理。当此方法返回true时,处理程序执行链继续。当它返回false时,DispatcherServlet假定拦截器本身已经处理了请求(例如,渲染了适当的视图),并且不会继续执行其他拦截器和执行链中的实际处理程序。
有关如何配置拦截器的示例,请参见MVC配置部分中的"interception"一节。您也可以通过在各个HandlerMapping实现上使用setter方法直接注册拦截器。
postHandle方法对于使用@ResponseBody和ResponseEntity方法的情况可能没啥用,因为在postHandle之前,HandlerAdapter中写入并提交了响应。这意味着现在对响应进行任何更改(例如添加额外的头信息)都为时已晚。对于这种情况,您可以实现ResponseBodyAdvice,并将其声明为控制器建议(Controller Advice)bean或直接在RequestMappingHandlerAdapter上进行配置。
1.1.8 Exceptions(异常)
如果在请求映射(Handler Mapping)期间发生异常,或者从请求处理过程中(如加了 @Controller注解的控制器)中抛出异常,请求对应的DispatcherServlet 将委托一系列的 HandlerExceptionResolver(处理程序异常解析器) 对象来解析异常并提供备选处理方式,一般是返回一个错误响应。
下表列出了可用的HandlerExceptionResolver实现:
| HandlerExceptionResolver | 描述 |
|---|---|
SimpleMappingExceptionResolver (简单映射异常解析器) | 将异常类名与错误视图名称进行映射。用于在浏览器应用程序中用于渲染错误页面。 |
DefaultHandlerExceptionResolver (默认处理程序异常解析器) | 解决Spring MVC引发的异常,并将它们映射到HTTP状态码。还可以查看备用方案的ResponseEntityExceptionHandler和错误响应。 |
ResponseStatusExceptionResolver(响应状态异常解析器) | 以@ResponseStatus注释解析异常,并根据注释中的值将其映射到HTTP状态码。 |
ExceptionHandlerExceptionResolver(异常处理程序异常解析器) | 通过调用在@Controller中的@ExceptionHandler方法或@ControllerAdvice类来解决异常。参见@ExceptionHandler方法。 |
ExceptionHandlerExceptionResolver
会查找与抛出的异常类型匹配的"@ExceptionHandler"注解标记的方法,
并执行该方法来处理异常。
这样,开发人员可以"将不同类型的异常处理逻辑分离到不同的方法中",使代码更加清晰和易于维护。
Chain of Resolvers(解析链)
在Spring配置中,您可以通过声明多个HandlerExceptionResolver(异常处理器)bean并根据需要设置它们的order属性来形成一个异常解析器链。order属性值越高的处理器,在异常解析链中的位置越靠后,也就是优先级越低。
以下规定了异常处理器(HandlerExceptionResolver)可以返回以下几种类型:
- 返回指向错误视图的
ModelAndView。 - 返回一个
空的 ModelAndView,表示异常已在该异常处理器内部得到处理。 - 返回
null,表示异常仍未得到解决,接下来由后续的异常处理器尝试处理。如果在所有的异常处理器中都无法解决该异常,它允许异常继续传递给 Servlet 容器处理。
这里的"Servlet 容器处理"一般是指:
根据配置或默认的错误页面处理机制,找到对应的错误页面,然后将请求转发到该错误页面
下面的 Container Error Page小节会讲到
MVC Config自动为默认的Spring MVC异常、@ResponseStatus注释异常以及对@ExceptionHandler方法的支持声明内置解析器。您可以自定义或替换该列表。
Container Error Page(容器错误页)
如果所有的HandlerExceptionResolver都无法解析异常,让其传播,或者如果响应状态设置为错误状态(即4xx、5xx),Servlet容器可以呈现默认的HTML错误页面。如果要自定义容器的默认错误页面,可以在web.xml中声明错误页面映射。以下示例显示了如何执行此操作:
<error-page>
<location>/error</location>
</error-page>
上面所给的示例代码中,当出现异常或响应具有错误状态时,Servlet容器在其中向配置的URL(例如/error)发出error调度。然后由DispatcherServlet处理,可能将其映射到@Controller,该控制器可以实现为返回一个带有模型的错误视图名称或呈现JSON响应,如下例所示:
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}
- 小提示
Servlet API没有提供在Java中创建错误页面映射的方法。但是,您可以同时使用WebApplicationInitializer和web.xml来实现。
1.1.9 View Resolution(视图解析)
Spring MVC定义了ViewResolver和View接口,使您能够在浏览器中呈现模型,而无需绑定特定的视图技术。ViewResolver提供了视图名称与实际视图之间的映射。View负责在交给特定视图技术之前准备数据。
"视图名称"是一个抽象的逻辑标识(比如/article/detial,指向文章详情页),
而"实际视图"是实际用于呈现内容的技术组件(也就是渲染详情页呈现给用户,URL可能不是/article/detial),视图解析器在二者之间建立映射,
以便将模型数据渲染成具体的用户可见页面。
"特定视图技术"指的是用于呈现和展示用户界面的具体技术或框架。比如:JSP、Thymeleaf、FreeMarker、React、Angular、Vue等等
以下表格提供关于ViewResolver层次结构的更多的详细信息:
| ViewResolver (视图解析器) | 描述 |
|---|---|
AbstractCachingViewResolver(抽象缓存视图解析器) | AbstractCachingViewResolver的子类会缓存它们解析的视图实例。缓存可以提高某些视图技术的性能。您可以通过将cache属性设置为false来关闭缓存。此外,如果你必须在运行时刷新特定视图(例如,当FreeMarker模板被修改时),您可以使用removeFromCache(String viewName, Locale loc)方法(从字面上来看是把某个视图从缓存中移除出去)。 |
UrlBasedViewResolver (基于URL的视图解析器) | 这是ViewResolver接口的简单实现,可以直接将逻辑视图名称解析为URL,无需显式的映射定义。如果您的逻辑名称与视图资源的名称直接匹配,而不需要任意的映射,那么这是适当的做法。 。 。 (比如你的逻辑名称为/news,资源文件就是news.html,那么就可以用这个) |
InternalResourceViewResolver (内部资源视图解析器) | 这是UrlBasedViewResolver比较方便的一个子类,支持InternalResourceView(实际上是Servlet和JSP),以及诸如JstlView的子类。您可以通过使用setViewClass(…)为该解析器生成的所有视图指定视图类。有关详细信息,请参阅UrlBasedViewResolver的Javadoc。 |
FreeMarkerViewResolver (FreeMarker视图解析器 FreeMarker是一个模板引擎) | 这是 UrlBasedViewResolver 的比较方便的一个子类,支持 FreeMarkerView 和它们的自定义子类。 |
ContentNegotiatingViewResolver (内容协商视图解析器) | 这是 ViewResolver 接口的实现,根据请求的文件名或 Accept 标头来解析视图。请参阅 Content Negotiation。(这个表格后面会给出详细的解释) |
BeanNameViewResolver (Bean名称视图解析器) | 这是 ViewResolver 接口的实现,将视图名称解释为当前应用程序上下文中的 bean 名称。这是一种非常灵活的变体,允许根据不同的视图名称混合和匹配不同的视图类型。每个这样的视图都可以作为一个 bean 定义,例如在 XML 中或配置类中。 |
ContentNegotiatingViewResolver描述中提到的"根据请求的 文件名或 Accept 标头 来解析视图"是指:
根据客户端请求中的信息,决定返回哪种格式或类型的视图内容。
这种内容协商机制可以根据客户端的需求,选择适当的视图来呈现,以满足不同的需求。
举个例子来说明:
假设您正在开发一个 RESTful Web 服务,该服务提供有关用户信息的数据。
客户端可以通过浏览器或 API 调用来请求这些数据,而且客户端可能希望以不同的格式获取数据,
比如 HTML 或 JSON。
根据文件名解析视图:
如果客户端请求的 URL 是 /user.html,那么服务器可以根据文件名 "user.html" 来解析视图,
选择适合 HTML 格式的视图进行渲染,然后将用户信息以 HTML 形式呈现在浏览器中。
如果客户端请求的 URL 是 /user.json,那么服务器会根据文件名 "user.json" 来解析视图,
选择适合 JSON 格式的视图进行渲染,然后将用户信息以 JSON 形式返回给客户端,以便客户端可以用于 API 调用。
根据 Accept 标头解析视图:
如果客户端发送的请求头中包含 Accept: text/html,表示客户端愿意接受 HTML 格式的响应,那么服务器会根据 "Accept" 标头来解析视图,
选择适合 HTML 格式的视图进行渲染,然后将用户信息以 HTML 形式呈现在浏览器中。
如果客户端发送的请求头中包含 Accept: application/json,表示客户端愿意接受 JSON 格式的响应,那么服务器会根据 "Accept" 标头来解析视图,
选择适合 JSON 格式的视图进行渲染,然后将用户信息以 JSON 形式返回给客户端,用于 API 调用。
Handling(管理ViewResolver)
您可以通过声明多个解析器 bean 来链接视图解析器,如果需要,还可以通过设置 order 属性来指定顺序。请记住,order 属性值越高,视图解析器在链中的位置就越靠后。
ViewResolver 的约定规定,它可以返回 null 来指示找不到视图。然而,在 JSP 和 InternalResourceViewResolver 的情况下,判断 JSP 是否存在的唯一方法是通过 RequestDispatcher 执行分派。因此,您必须始终将 InternalResourceViewResolver 配置为在视图解析器的总体顺序中排在最后。
配置视图解析就像将视图解析器的bean添加到您的Spring 配置中一样简单。Spring MVC Config 提供了特定的配置API来配置视图解析器,以及添加无逻辑的视图控制器,用于在不使用控制器逻辑的情况下进行 HTML 模板渲染。
Redirecting(重定向)
在视图名称中的特殊 "redirect:" 前缀使您能够执行重定向操作。UrlBasedViewResolver(以及其子类)将其识别为需要执行重定向的指令。视图名称的其余部分是重定向的 URL。
其效果与控制器返回 RedirectView 相同,但现在控制器本身可以使用逻辑视图名称进行操作。逻辑视图名称(例如 redirect:/myapp/some/resource)相对于当前的 Servlet 上下文进行重定向,而名称如 redirect:https://myhost.com/some/arbitrary/path 会重定向到绝对 URL。
Forwarding(转发)
对于最终由UrlBasedViewResolver和其子类解析的视图名称,也可以使用特殊的forward:前缀。这会创建一个InternalResourceView,它会执行RequestDispatcher.forward()转发方法。因此,此上面的说的前缀对InternalResourceViewResolver和InternalResourceView(对于JSP)没有用处,但如果您使用另一种视图技术,但仍希望强制转发由Servlet/JSP引擎处理的资源,则它可能会有所帮助。请注意,您也可以通过链接多个视图解析器来替代上述的转发。
比如以下这样的特殊的"前缀"
@Controller
@RequestMapping("/myController")
public class MyController {
@GetMapping("/forwardExample")
public String forwardExample() {
return "forward:/myOtherController/someHandler";
}
}
Content Negotiation(内容协商)
ContentNegotiatingViewResolver不会自己解析视图,而是委托给其他视图解析器,并选择与客户端请求的表示形式相似的视图。表示形式可以从Accept标头或查询参数(例如"/path?format=pdf")中确定。
ContentNegotiatingViewResolver通过比较请求的媒体类型(media types)以及与其每个ViewResolvers相关联的View支持的媒体类型(也称为Content-Type)来选择适当的视图来处理请求。列表中的第一个具有兼容Content-Type的View会作为向客户端返回的结果。如果ViewResolver链无法提供兼容的视图,则会查询通过DefaultViews属性指定的视图列表。后一种选项适用于单例视图,无论逻辑视图名称如何,都可以呈现当前资源的适当表示形式。Accept标头可以包含通配符(例如text/*),在这种情况下,Content-Type值为text/xml的视图将是兼容的匹配项。
1.1.10 Local(地区)
Spring架构的大部分都支持国际化,就像Spring Web MVC框架一样。DispatcherServlet通过使用客户端的区域设置,可以自动解析消息。这是通过LocaleResolver对象来实现的。
当请求到达时,DispatcherServlet会寻找地区解析器,如果找到了,它会尝试使用它来设置区域设置。通过使用RequestContext.getLocale()方法,您可以随时检索由地区解析器解析出的地区信息。
除了自动地区解析之外,您还可以将拦截器附加到处理程序映射(有关处理程序映射拦截器的更多信息,请参见拦截- Interception)。以根据请求中的参数等特定情况更改地区设置。
地区解析器和拦截器位于org.springframework.web.servlet.i18n包中,并且可以像正常情况下一样在应用程序上下文中进行配置。以下地区解析器的选择包含在Spring中。
时区(Time Zone)
标头解析器(Header Resolver)
Cookie解析器(Cookie Resolver)
会话解析器(Session Resolver)
地区拦截器(Locale Interceptor)
Time Zone(时区)
除了获取客户端的区域设置之外,了解其时区通常也是有用的。LocaleContextResolver接口提供了对LocaleResolver的扩展,使解析器可以提供更丰富的LocaleContext,其中可能包括时区信息。
当其可用时,可以使用RequestContext.getTimeZone()方法获取用户的时区。时区信息将自动由任何已在Spring的ConversionService中注册的日期/时间转换器和格式化器(Date/Time Converter and Formatter)对象使用。
Header Resolver(头解析器)
这个地区解析器检查客户端(例如,Web浏览器)发送的请求中的accept-language头。通常,这个头字段包含客户端操作系统的地区信息。请注意,此解析器不支持时区信息。
Cookie Resolver(Cookie解析器)
这个地区解析器检查客户端上可能存在的Cookie,以查看是否指定了地区或时区。如果指定了,它会使用指定的详细信息。通过使用此地区解析器的属性,您可以指定Cookie的名称以及最大的有效期。以下示例定义了一个CookieLocaleResolver:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<!-- 以秒为单位。如果设置为-1,则cookie不会持久化(在浏览器关闭时删除) -->
<property name="cookieMaxAge" value="100000"/>
</bean>
下表描述了CookieLocaleResolver的属性:
| 属性 | 默认值 | 描述 |
|---|---|---|
| cookieName | 类名 + 地区 | cookie名称 |
| cookieMaxAge | Servlet容器默认值 | cookie在客户端上持续存在的最长时间。如果指定了-1,则cookie将不会持久化。它只有在客户端关闭浏览器之前才可用。 |
cookiePath | / | 将cookie的可见性限制在网站的某个部分。当指定了cookiePath时,cookie仅对该路径及其下的路径可见。 |
Session Resolver(Session解析器)
SessionLocaleResolver 允许你从可能与用户请求相关联的会话中检索区域设置和时区。与 CookieLocaleResolver 相比,此策略将本地选择的地区存储在Servlet容器的HttpSession中。因此,这些设置对于每个会话都是临时的,因此在每个会话结束时会丢失。
请注意,与外部会话管理机制(例如 Spring Session 项目)之间没有直接关系。SessionLocaleResolver 通过当前的 HttpServletRequest 评估和修改相应的 HttpSession 属性。
Locale Interceptor(地区拦截器)
您可以通过将 LocaleChangeInterceptor 添加到 HandlerMapping 定义中来启用更改区域设置的功能。它会检测请求中的参数并相应地更改地区设置,使用调度程序的应用上下文里面的 LocaleResolver的 setLocale 方法。下面的示例显示,对所有包含名为 siteLanguage 的参数的 *.view 资源的调用都会更改地区设置。因此,例如,对 URL www.sf.net/home.view?siteLanguage=nl 的请求将更改站点语言为荷兰语。以下示例显示了如何拦截区域设置:
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
1.1.11 Themes(主题)
你可以应用Spring Web MVC框架主题来设置应用程序的整体外观(look-and-feel),从而增强用户体验。主题是影响应用程序视觉样式的静态资源的集合,通常是样式表和图像。
请注意:从6.0开始,对主题的支持已被弃用,转而支持使用CSS,并且在服务器端没有任何特殊支持。
Defining a theme(定义一个主题)
要在web应用程序中使用主题,必须设置org.springframework.ui.context.ThemeSource接口的实现。虽然WebApplicationContext接口扩展了ThemeSource,但将其职责委托给一个专门的实现。默认情况下,委托是org.springframework.ui.context.support.ResourceBundleThemeSource实现,它从类路径的根目录加载属性文件。要使用自定义的ThemeSource实现或配置ResourceBundleThemeSource的基本名称前缀,可以在应用程序上下文中使用保留名称ThemeSource注册bean。web应用程序上下文自动检测具有该名称的bean并使用它。
当您使用ResourceBundleThemeSource时,主题定义在一个简单的属性文件中。属性文件列出了组成主题的资源,如下例所示:
styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg
属性的键(也就是上面的styleSheet、background)是引用视图代码中主题元素的名称。对于JSP,通常使用spring:theme自定义标签来完成此操作,该标签与spring:message标签非常相似。以下JSP片段使用上一个示例中定义的主题来定制外观:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
</head>
<body style="background=<spring:theme code='background'/>">
...
</body>
</html>
Resolving Themes(解析主题)
定义主题后,如前一节所述,你将决定使用哪个主题。DispatcherServlet查找一个名为themeResolver的bean,以找出要使用的themeResolver实现。主题解析器的工作方式与LocaleResolver大致相同。它检测用于特定请求的主题,还可以更改请求的主题。下表介绍了Spring提供的主题解析器:
| 类 | 描述 |
|---|---|
FixedThemeResolver | 通过使用defaultThemeName属性,来选择固定主题。 |
SessionThemeResolver | 主题在用户的HTTP会话中会保留。它只需要为每个会话设置一次,但不会在会话之间持久化。(也就是仅当前会话有效,而且不会与别的会话共享同一主题) |
CookieThemeResolver | 所选主题存储在客户端的cookie中 |
Spring还提供了一个ThemeChangeInterceptor,它允许通过一个简单的请求参数对每个请求进行主题更改。
1.1.12 Multipart Resolver(多部分解析器)
org.springframework.web.multipart包中的MultipartResolver是一种用于解析包括文件上传在内的多部分请求的策略。其中有一个基于容器的StandardServletMultipartResolver实现用于解析Servlet多部分请求。请注意,基于Apache Commons FileUpload的CommonsMultipartResolver在Spring Framework 6.0及其新的Servlet 5.0+标准已不可用。
要启用多部分处理,您需要在DispatcherServlet Spring配置中声明一个名为multipartResolver的,类名为MultipartResolver的Bean。DispatcherServlet会检测它并将其应用于传入请求。当接收到内容类型为multipart/form数据的POST请求时,该解析器会解析请求内容并将当前HttpServlet请求包装为MultipartHttpServlet请求,用来提供对已解析文件的访问,此外还将请求的某些部分作为请求参数公开。
Servlet Multipart Parsing(Servlet 多部分解析)
Servlet多部分解析需要通过Servlet容器配置来启用。比如以下这样:
- 在
Java中,在Servlet注册时设置一个MultipartConfigElement。 - 在
web.xml中,在servlet声明中添加一个"<multipart-config>"部分。
以下示例显示如何在Servlet注册上设置MultipartConfigElement:
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// 一般还可以设置maxFileSize(文件的最大长度),
//maxRequestSize(请求最大长度),
//fileSizeThreshold(可接收的最小的文件,比如设置为1M,那么小于1M的文件就不会接收)
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
一旦Servlet的多部分配置完成,就可以添加一个名为multipartResolver的StandardServletMultipartResolver类型的Bean。
注意
虽然这个解析器变体会按原来的方式去使用你的Servlet容器中的多部分解析器,但这可能会因为容器实现的差异而让应用程序信息暴露。
默认情况下,它将尝试HTTP相关方法解析所有的multipart/内容类型,但这可能不支持所有Servlet容器。
有关详细信息和配置选项,请参阅StandardServletMultipartResolver文档。
1.1.13 Logging(日志记录)
Spring MVC中的DEBUG级别日志记录被设计得紧凑、迷你和友好。它关注那些在多次使用中都很有价值的信息,而不是那些只在调试特定问题时才有用的信息。
TRACE级别的日志通常遵循与DEBUG级别相同的原则(例如,不应该是一种大量产生的日志),但可以用于调试任何问题。此外,一些日志信息在TRACE级别和DEBUG级别下可能会显示不同级别的详细信息。
良好的日志记录来自于使用日志的经验。如果您发现任何不符合既定目标的内容,请告知我们。
Sensitive Data (敏感数据)
DEBUG和TRACE日志记录可能会记录敏感信息。这就是请求参数和标头在默认情况下被屏蔽的原因,并且必须通过DispatcherServlet上的enableLoggingRequestDetails属性显式启用它们的完整日志记录。
以下的示例展示了如何通过Java配置来实现上述操作:
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
写在最后
磨磨唧唧,断断续续写了差不多三万字(虽然很多时候都借鉴了翻译软件),不过这种知识进脑子又出去感觉,实在是太爽了,哈哈哈
















![[npm]package.json文件](https://img-blog.csdnimg.cn/ba0ff41b041446bd9abcd40b81ea59d4.png)


