大家好,欢迎来到IT知识分享网。
SpringMvc启动流程及如何响应请求
- 前言
- Spring父子容器
- Tomcat解析Web.xml流程创建子容器
- springMvc初始化策略初始化文件解析器initMultipartResolver初始化国际化应用initLocaleResolver初始化主题initThemeResolver初始化HandlerMapping初始化handler适配器initHandlerAdapters初始化异常initHandlerExceptionResolversinitRequestToViewNameTranslator初始化视图解析器initViewResolvers初始化重定向initFlashMapManager补充一点:拦截器再补充一点:参数的解析器
- springMvc是如何响应一个请求的接收请求dispatcherServlet#doDispatch判断文件上传映射handler寻找适配器如果是get请求,判断是否用浏览器缓存前置拦截器执行handler执行方法处理返回值将返回值写进ModelAndView如果没有试图将设置默认试图后置拦截器渲染视图最后执行拦截器afterCompletion
前言
springMvc基于web.xml启动流程是根据tomcat启动会去解析webapps下的web.xml,然后才启动的springMvc。
Spring父子容器
Spring 父子容器是指 Spring 容器的层次结构,其中一个容器作为另一个容器的父容器。父容器可以提供资源和 bean 给子容器,子容器可以覆盖或扩展父容器的 bean 定义。
这种父子关系在 Spring Web 应用中非常常见。通常情况下,在 Web 应用中会有两个 Spring 容器:Root WebApplicationContext 和 Servlet WebApplicationContext。
- Root WebApplicationContext(父容器):在一个 Spring Web 应用启动时,ContextLoaderListener 会被用来启动一个 Root WebApplicationContext。这个容器通常会包含所有的 Service、Repository等等的 Bean,这些 Bean 通常会被应用的所有 Servlet 使用。
- Servlet WebApplicationContext(子容器):对于每一个 DispatcherServlet,都会创建一个 Servlet WebApplicationContext。每个这样的容器都会有 Root WebApplicationContext 作为其父容器。Servlet WebApplicationContext 通常会包含 Controller、View Resolver、Local Resolver 等和处理 HTTP 请求相关的 Bean。
子容器可以看到父容器中的所有 Bean,而父容器则不能看到子容器中的任何 Bean。这就意味着,你可以在一个 Controller 中注入一个在 Root WebApplicationContext 中定义的 Service,但是你不能在一个 Service 中注入一个在 Servlet WebApplicationContext 中定义的 Controller。
Tomcat解析Web.xml流程
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!--Spring配置文件--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!--Spring监听器--> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!--SpringMVC前端控制器--> <servlet> <servlet-name> SpringMVC </servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <!-- 配置springMVC需要加载的配置文件--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/springMVC.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
Tomcat启动时候会先创建servlet容器,然后解析web.xml。
1、首先加载conf/web.xml文件,然后加载web应用程序中的WEB-INF/web.xml文件。
2、按照以下顺序读取和处理web.xml中的元素:context-param -> listener -> filter -> servlet。
- context-param元素用于向ServletContext提供键值对,即应用程序上下文信息,可以写在任意位置。
- servlet元素用于定义servlet的名字和类,servlet-mapping元素用于指定访问servlet的URL,servlet-mapping必须出现在servlet之后,servlet的初始化顺序按照load-on-startup元素指定的值,如果值为正数或零,则按照从小到大的顺序初始化,如果值为负数或未定义,则在第一次请求时初始化。
先解析context-param,然后创建ServletContext对象,并将context-param转换为键值对交给ServletContext,然后再解析listener-class,并创建监听器实例。
如果监听器类实现了ServletContextListener接口,那么它的contextInitialized(ServletContextEvent sce)方法和contextDestroyed(ServletContextEvent sce)方法会在ServletContext对象创建和销毁时被调用,这两个方法的参数sce可以通过getServletContext()方法获取ServletContext对象。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener { public ContextLoaderListener(WebApplicationContext context) { super(context); } @Override public void contextInitialized(ServletContextEvent event) { initWebApplicationContext(event.getServletContext()); } @Override public void contextDestroyed(ServletContextEvent event) { closeWebApplicationContext(event.getServletContext()); ContextCleanupListener.cleanupAttributes(event.getServletContext()); } }
调用contextInitialized会执行方法:
// xml会在这里创建 if (this.context == null) { this.context = createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; //创建spring容器,这里调用refresh() configureAndRefreshWebApplicationContext(cwac, servletContext); } } // 在servlet域中设置根容器(在子容器就可以直接拿到了) servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
- 创建WebApplicationContext,设置必要的参数,如配置文件位置contextConfigLocation。
- 调用refresh()。在这个过程中,XmlWebApplicationContext 会解析 XML 配置文件,加载 bean 定义,并创建并初始化所有的 bean。
- 将这个容器当作父容器存到servletContext里边。
创建子容器
当创建完成父容器之后,就开始创建子容器了。解析servlet标签,这通常包括 实例Spring 的 DispatcherServlet。
1、在web.xml文件中配置dispatcher servlet的servlet-name,servlet-class,init-param和load-on-startup等信息。
2、会根据load-on-startup的值,按照从小到大的顺序初始化servlet实例,并调用它们的init()方法
在httpServlet中重写了init()方法
@Override public final void init() throws ServletException { // 解析 init-param 并封装只 pvs 中(xml) PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); if (!pvs.isEmpty()) { // 将当前的这个 Servlet 类转化为一个 BeanWrapper,从而能够以 Spring 的方法来对 init-param 的值进行注入 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); initBeanWrapper(bw); // 修改servlet状态,并将pvs里边的值赋值给servlet bw.setPropertyValues(pvs, true); } // 初始化Servlet,创建Spring容器 initServletBean(); }
上边代码解释,将web.xml里边servlet标签的参数,赋值给servlet里,然后修改servlet的状态。
最后调用initServletBean->initWebApplicationContext()方法生成WebApplicationContext。
protected WebApplicationContext initWebApplicationContext() { // 获得ContextLoaderListener存的父容器 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; ... if (wac == null) { // xml会在这里创建 wac = createWebApplicationContext(rootContext); } //refreshEventReceived 它会在容器加载完设置为true (通过事件onApplicationEvent) // springboot在这初始化组件 if (!this.refreshEventReceived) { synchronized (this.onRefreshMonitor) { onRefresh(wac); } } if (this.publishContext) { // 将当前容器放到servlet域中, 可以再创建子容器 String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); } return wac; }
在createWebApplicationContext方法中会创建WebApplicationContext。
- 设置环境变量
- 设置父容器
- 启动容器也就是refresh
在onRefresh方法中会加载关于mvc的一些东西。
springMvc初始化策略
初始化文件解析器initMultipartResolver
Spring MVC的initMultipartResolver方法是用来初始化multipart解析器的。MultipartResolver是一个接口,在Web开发中,multipart解析器主要用于处理HTTP请求中的multipart/form-data类型的数据,这种数据类型通常用于文件上传。
Spring MVC提供了两种multipart解析器:
- CommonsMultipartResolver:基于Apache Commons FileUpload库实现的multipart解析器。
- StandardServletMultipartResolver:基于Servlet 3.0规范实现的multipart解析器。
在Spring MVC中,如果你定义了一个名为”multipartResolver”的bean,那么Spring MVC就会使用你定义的这个bean作为multipart解析器。否则,Spring MVC就不会处理multipart/form-data类型的请求。
LOCALE_RESOLVER_BEAN_NAME = “localeResolver”;
一种是我们自定义上传文件,
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 设置最大上传文件为5MB --> <property name="maxUploadSize" value=""/> </bean>
这样在处理上传文件的请求时,就会自动调用CommonsMultipartResolver来处理请求中的文件上传。
@Bean public CommonsMultipartResolver multipartResolver() { CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); multipartResolver.setMaxUploadSize(); return multipartResolver; }
如果我们没有自定义上传文件的bean。
那么就会利用spi去配置文件里边取值,然后加载到子容器中。
// 通过PropertiesLoaderUtils工具类加载DispatcherServlet.properties //DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties"; ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); //spi defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
上边代码意思是从配置文件中找到 DispatcherServlet.properties,然后通过类名找到对应的k-v,也就是找到LocaleResolver对应的value,
找到利用spring去createBean,并将值赋值给multipartResolver。
image-
初始化国际化应用initLocaleResolver
initLocaleResolver方法用于初始化 DispatcherServlet 中的 LocaleResolver。在Spring MVC中,LocaleResolver是一个接口,它定义了如何解析客户端的地区信息。地区信息通常被用于国际化应用,可以根据不同的地区显示不同的信息。
该方法的主要逻辑如下:
- 首先,尝试从Spring上下文中获取名为localeResolver的bean,如果获取到了,就使用该bean作为地区解析器。
- 如果没有获取到,那么就会使用getDefaultStrategy方法来获取一个默认的地区解析器。具体的实现类是在DispatcherServlet.properties中配置的和上边的一样也是利用SPI技术。
以下是代码的逻辑:
private void initLocaleResolver(ApplicationContext context) { try { this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class); catch (NoSuchBeanDefinitionException ex) { this.localeResolver = getDefaultStrategy(context, LocaleResolver.class); } }
在Spring MVC中,有几个预定义的LocaleResolver的实现:
- AcceptHeaderLocaleResolver: 根据HTTP的Accept-Language头来解析地区信息。
- FixedLocaleResolver: 提供固定的地区信息,无论请求是什么。
- CookieLocaleResolver: 将地区信息保存在浏览器的Cookie中。
- SessionLocaleResolver: 将地区信息保存在HTTP Session中。
如果我们没有自己配置LocaleResolver的Bean,Spring MVC将默认使用AcceptHeaderLocaleResolver。
初始化主题initThemeResolver
初始化 ThemeResolver 的方法。ThemeResolver 是一个接口,定义了如何解析应用程序的主题的规则。主题是Spring MVC中用于改变视图层外观的一种机制,例如颜色、CSS样式等。在Spring MVC中,可以为每个用户设置不同的主题,或者为所有用户设置一个统一的主题。
具体查找和上边一样,查找的是themeResolverbean,默认实现的是FixedThemeResolver,保存到themeResolver 。
初始化HandlerMapping
initHandlerMappings方法用于初始化处理器映射(Handler Mapping),这里也是和上边一样如果我们自己定义HandlerMapping,就用我们自己定义的,我们可以定义多个,如果没有自定义用默认的,默认有三个实现,分别是
- BeanNameUrlHandlerMapping:这个处理器映射使用 Spring bean 的名称作为 URL 来映射请求。例如,一个名为 “/test” 的 bean 将会处理来自 “/test” URL 的请求。这种方式在实际开发中用的比较少,因为它不够灵活且易于混淆。
- RequestMappingHandlerMapping:这个是最常用的处理器映射。它会扫描 Spring 容器中的所有控制器(Controller),找出带有 @RequestMapping 注解的方法,然后根据注解的参数(例如,HTTP 方法、URL 等)来建立请求与方法之间的映射。在处理 HTTP 请求时,它会找到与请求参数匹配的那个方法来处理请求。
- SimpleUrlHandlerMapping:这个处理器映射允许我们显式地指定 URL 与处理器之间的映射关系。
然后将handlerMapping都保存到handlerMappings集合中。
RequestMappingHandlerMapping查找handler流程
到这里其实没有完,我们在用默认实现的时候,是spring帮我们创建的,也就是createBean,我们经常用的RequestMappingHandlerMapping类图:
他是实现了InitializingBean,在初始化时候会执行afterPropertiesSet方法然后调用initHandlerMethods方法,这个方法会去解析所有的handler。
在 Spring MVC 中,RequestMappingHandlerMapping 是负责处理标有 @RequestMapping 注解的 Controller 的。它的工作流程如下:
- 首先,当 Spring 容器启动的时候,RequestMappingHandlerMapping 会进行初始化,在初始化的过程中,afterPropertiesSet 方法会被调用。这个方法会进一步调用 initHandlerMethods 方法。
- 在 initHandlerMethods 方法中,首先会从 Spring 容器中获取所有的 Bean。对于获取到的每一个 Bean,RequestMappingHandlerMapping 都会检查它是否标有 @Controller 或者 @RequestMapping 注解。只有标有这些注解的 Bean 才会被认为是一个有效的 Controller。
- 如果一个 Bean 被认为是一个有效的 Controller,那么 RequestMappingHandlerMapping 会进一步检查这个 Controller 中的所有方法。对于每一个方法,如果它上面有 @RequestMapping 注解,那么 RequestMappingHandlerMapping 会根据这个注解以及它所在的 Controller 的信息,创建一个 RequestMappingInfo 对象。
- RequestMappingInfo 对象包含了请求的 URL、请求的方法、请求的参数等信息。这个对象会被用来与进来的 HTTP 请求进行匹配,以决定哪一个方法应该被用来处理这个请求。
- 创建headlMethod
headlMethod
构建RequestMappingInfo
最后将保存k-v,将path-List保存到pathLookup map中,为什么是list,因为相同的路径有get、post区分。
将 RequestMappingInfo-headlMethod保存到registry map中。
我们自定义实现HandlerMapping
实现HandlerMapping接口或者继承AbstractHandlerMapping。
首先,创建一个CustomHandlerMapping类,它继承自AbstractHandlerMapping:
public class CustomHandlerMapping extends AbstractHandlerMapping { //保存映射关系 Map<String, Object> pathHandlers = new HashMap<>(); public CustomHandlerMapping() { // 添加一些自定义的映射 // 此处仅作示例,一般我们不会在构造函数中添加映射 // 更常见的是在配置类中或者通过其他方式动态添加映射 this.pathHandlers.put("/custom", new CustomHandler()); } @Override protected Object getHandlerInternal(HttpServletRequest request) throws Exception { // 根据请求的URL路径查找对应的处理器 String path = request.getServletPath(); return this.pathHandlers.get(path); } } class CustomHandler { // 这里可以添加自定义的处理器方法 }
然后,在Spring MVC配置中注册这个自定义的HandlerMapping。当一个请求的URL路径为”/custom”时,CustomHandlerMapping就会返回CustomHandler作为处理器。
自定义的HandlerMapping与Spring MVC默认的HandlerMapping(比如RequestMappingHandlerMapping)是可以共存的(需要手动注入了,因为有默认的了,就不加载默认的了)。当一个请求到来时,Spring MVC会按照HandlerMapping的顺序来查找处理器。你可以通过实现Ordered接口或者使用@Order注解来调整HandlerMapping的顺序。
初始化handler适配器initHandlerAdapters
初始化处理器适配器。处理器适配器(HandlerAdapter)负责调用处理器(Handler)的处理方法,最后的方法都封装成了handler,具体怎么调度,不同的handler有不同的handlerAdapter来负责调用。
和上边一样的,从应用程序上下文中获取所有类型为HandlerAdapter的bean。如果找不到任何HandlerAdapter的bean,那么它会使用一组默认的处理器适配器。这组默认的处理器适配器包括RequestMappingHandlerAdapter、HttpRequestHandlerAdapter和SimpleControllerHandlerAdapter、HandlerFunctionAdapter,并将其保存到handlerAdapters结合中。
- RequestMappingHandlerAdapter:这个类是用于处理带有@RequestMapping注解的方法。它会根据请求的参数,头部,属性等来解析方法的参数,并调用反射来执行方法。
- HttpRequestHandlerAdapter:这个类是用于处理实现了HttpRequestHandler接口的类,这个接口只有一个方法handleRequest,用于直接处理请求和响应。它会将HttpServletRequest和HttpServletResponse对象作为参数传递给handleRequest方法,并将返回值封装成一个ModelAndView对象。
- SimpleControllerHandlerAdapter:这个类是用于处理实现了Controller接口的类,这个接口也只有一个方法handleRequest,用于返回一个ModelAndView对象。它会将HttpServletRequest和HttpServletResponse对象作为参数传递给handleRequest方法,并直接返回其返回值。
- HandlerFunctionAdapter:这个类是用于处理实现了HandlerFunction接口的类,这个接口是一个函数式接口,用于定义一个函数,接受一个ServerRequest对象并返回一个Mono对象。它会将HttpServletRequest和HttpServletResponse对象转换成ServerRequest和ServerResponse对象,并调用apply方法来执行函数,并将返回值转换成一个ModelAndView对象。
所以当我们自定义了handlerMapping就需要自定义HandlerAdapter来对他的handler进行匹配,当然了,也可以利用默认的。
初始化异常initHandlerExceptionResolvers
负责将在处理器处理过程中抛出的异常转换为对应的模型-视图结果。
在initHandlerExceptionResolvers()方法中,获取所有类型为HandlerExceptionResolver的Bean。如果找不到任何HandlerExceptionResolver的Bean,那么它会使用一组默认的处理器异常解析器。
- ExceptionHandlerExceptionResolver:用于处理通过@ExceptionHandler注解处理的方法抛出的异常。
- ResponseStatusExceptionResolver:用于处理通过@ResponseStatus注解处理的异常。
- DefaultHandlerExceptionResolver:用于处理Spring MVC自己抛出的一些特定的异常。
initRequestToViewNameTranslator
initRequestToViewNameTranslator方法用于初始化RequestToViewNameTranslator的,RequestToViewNameTranslator是一个接口,用于根据请求解析出一个默认的视图名称,当ModelAndView对象不为null,但是没有指定视图时,就会使用这个接口来获取视图名称。
寻找bean名字为viewNameTranslator试图解析器,没有的话用DefaultRequestToViewNameTranslator,并存放到viewNameTranslator。
工作原理是:它会去掉请求URL的前缀和后缀,然后将剩下的部分作为视图名称。例如,如果我们设置prefix为”/app/“,suffix为”.html”,stripExtension为false,那么请求的URL”/app/home.html”就会被解析为”home.html”。如果我们设置stripExtension为true,那么就会被解析为”home”。
初始化视图解析器initViewResolvers
默认是寻找所有类型为ViewResolver的bean,将他们作为试图解析器,默认的试图解析器是InternalResourceViewResolver。它们是用于将控制器返回的逻辑视图名解析为具体的视图对象,这个对象可以是一个 JSP、一个 HTML 页面、一个 PDF 视图、一个 Thymeleaf 模板等。
初始化重定向initFlashMapManager
默认的是SessionFlashMapManager。
FlashMap是Spring MVC提供的一种临时存储数据的方式,它用于存储一次请求的数据,并在下一次请求中使用。FlashMap通常用于重定向请求时传递数据,比如,当你在处理POST请求后重定向到一个GET请求时,你可能希望重定向的GET请求能够访问POST请求处理的一些结果数据,这就可以使用FlashMap来实现。例如:
可以这样存储Flash属性:
@RequestMapping(method = RequestMethod.POST) public String handlePostData(@ModelAttribute("data") Data data, RedirectAttributes redirectAttrs) { // 处理POST请求... // 将一些数据存储在FlashMap中,以便在重定向后的GET请求中使用 redirectAttrs.addFlashAttribute("message", "123"); // 重定向到GET请求 return "redirect:/get-data"; }
然后在重定向后的GET请求中提取Flash属性:
@RequestMapping(method = RequestMethod.GET) public String displayData(Model model) { // 如果存在Flash属性,它们会自动添加到Model中 if (model.containsAttribute("message")) { System.out.println("Message-FlashMap: " + model.getAttribute("message")); } // 处理GET请求... }
springMvc初始化完毕,启动完成。
补充一点:拦截器
拦截器是HandlerInterceptor接口,保存handlerMapping的adaptedInterceptors集合中。拦截器主要用来添加头信息,验证请求参数,记录请求日志等。不同的handlerMapping都有不同的adaptedInterceptors集合。
匹配路径的拦截器有MappedInterceptor类。
我们可以通过以下来添加拦截器:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private MyInterceptor myInterceptor; @Override //添加到所有的HandlerMapping中 public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor).addPathPatterns("/**"); } }
再补充一点:参数的解析器
使用HandlerMethodArgumentResolver接口的实现来解析方法参数。当DispatcherServlet创建RequestMappingHandlerAdapter实例时,会初始化一组默认的HandlerMethodArgumentResolver实现,并将其添加到argumentResolvers列表中。
自定义的参数解析器依赖WebMvcConfigurer接口的addArgumentResolvers方法,可以通过实现WebMvcConfigurer接口,并覆盖addArgumentResolvers方法,来添加自定义的参数解析器。
RequestMappingHandlerAdapter在bean初始化的时候会添加默认参数解析器,同时也会添加默认的
springMvc是如何响应一个请求的
在springMvc启动完成之后只是初始化了,但是没有做一些操作,比如说寻找handler、寻找异常信息…下面会说到。
接收请求
当一个HTTP请求发出后,它首先被服务器的网络层接收,然后被应用服务器(例如Tomcat)处理。在应用服务器中,存在一个servlet容器,它负责管理所有的servlet,包括Spring MVC的核心组件——DispatcherServlet。
请求是发送到servlet的service方法里边的。被HttpServlet所实现,service方法会根据 HTTP 请求是 GET、POST、PUT 还是 DELETE 等,进一步调用 Servlet 的 doGet(),doPost(),doPut(),doDelete() 等方法。
HttpServletRequest 和 HttpServletResponse 对象是由 Servlet 容器(Tomcat)在调用 Servlet 的 service() 方法之前创建和初始化的。
所有的doGet、doPost…最终都会走到FrameworkServlet的processRequest方法。
在processRequest方法中:看三个关键点,
1、doService真正处理的逻辑,
2、其中doService是放在try catcht里边的,在执行方法之前会把requestAttributes存放到线程池里边,在整个doService共享,在finally后删除。
3、在finally中,会发布事件ApplicationEvent事件
然后就是dispatcherServlet去处理doService逻辑了
dispatcherServlet#doDispatch
doService是对request的参数上的一些封装,封装完成之后进入doDispatch,以下都是在doDispatch里边完成
判断文件上传
如果是文件上传,通过我们一开始初始化的文件解析器,调用resolveMultipart方法,去解析req里边的参数信息
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request); return parseFileItems(fileItems, encoding);
并返回DefaultMultipartHttpServletRequest。
如果不是文件上传不做处理。
映射handler
先遍历pathLookup找到对应的list
- 首先,调用getPathMatcher方法,获取一个PathMatcher对象,这个对象用于匹配和解析URL路径
- 然后,调用trimTrailingSlash方法,去掉请求的URL路径的最后一个斜杠,如果有的话
- 接着,调用pathLookup.get方法,根据请求的URL路径作为键,从Map中获取对应的List值
- 最后,如果没有找到匹配的List,就尝试使用通配符”“或者”*”来匹配URL路径,并再次调用pathLookup.get方法,直到找到匹配的List或者没有更多的通配符
举个例子更好的理解:
,假设我们有如下的URL模式:
- /user/{userId}/info
- /user/*/info
- /user/**/info
当一个请求到来,比如”/user/123/info”,Spring MVC会按照从精确到模糊的顺序来寻找匹配的处理器。
- 先尝试完全匹配。Spring MVC会用”/user/123/info”去pathLookup这个map中查找,看是否有精确匹配的RequestMappingInfo。
- 如果没有找到精确匹配,Spring MVC会尝试用”/user/123/“去pathLookup中查找,”“代表任何内容。也就是说,它会看是否有匹配”/user/123/*”的RequestMappingInfo。
- 如果还没有找到,Spring MVC会继续尝试用”/user//“去pathLookup中查找。这里的”*”代表任何内容,所以它实际上是在看是否有匹配任何用户ID和任何信息的RequestMappingInfo。
- 如果还是没有找到,Spring MVC会使用更模糊的匹配规则,用”/user/“去pathLookup中查找,”“代表任何路径,包括子路径。也就是说,它会看是否有匹配任何以”/user/”开头的RequestMappingInfo。
得到List后,如何筛选的过程是这样的:
首先,遍历这个List,对每个RequestMappingInfo,调用各自的getMatchingCondition方法,这个方法会根据请求的其他条件,比如请求方法,参数,头部等,判断是否匹配,并返回一个新的RequestMappingInfo,包含了匹配的条件然后,如果返回的RequestMappingInfo不为null,就说明匹配成功,就将它和对应的HandlerMethod封装成一个Match对象,并放到一个列表中
接着,对这个列表进行排序,根据一个MatchComparator来比较它们的优先级,优先级的判断依据有以下几个方面
- URL路径是否是完全匹配:比如说,对于请求路径 “/user/123″,如果有一个 RequestMappingInfo 的 URL 路径为 “/user/123″,而另一个为 “/user/*”,那么 “/user/123” 的优先级更高,因为它是完全匹配的。
- URL模式的具体性:比如说,对于请求路径 “/user/123″,如果有一个 RequestMappingInfo 的 URL 路径为 “/user/{id}”,而另一个为 “/user/“,那么 “/user/{id}” 的优先级更高,因为它的具体性更强。又比如说,对于请求路径 “/user/123/profile”,如果有一个 RequestMappingInfo 的 URL 路径为 “/user/{id}/profile”,而另一个为 “/user/“,那么 “/user/{id}/profile” 的优先级更高,因为它有更多的路径段。
- 是否有其他条件,如请求方法,参数,头部等:比如说,对于一个 GET 请求,如果有一个 RequestMappingInfo 只匹配 GET 方法,而另一个匹配所有的方法,那么只匹配 GET 方法的 RequestMappingInfo 的优先级更高。
- 是否有自定义条件:如果 RequestMappingInfo 定义了自定义条件,并且这些自定义条件实现了 Comparable 接口,那么就会根据这些自定义条件的 compareTo 方法来确定哪个 RequestMappingInfo 的优先级更高。
最后,返回排序后的第一个Match对象中的HandlerMethod作为最佳匹配的处理器方法
以上只是RequestMappingHandlerMapping的查找过程,默认不是有三个嘛,会循环这三个的getHandler,也就是重复以上的所有的步骤,直到返回一个handler,就不再往下查找了。
如果没有找到handler,用默认的handler,如果没有默认的,则再response设置404,并且抛出错误NoHandlerFoundException。
然后获取拦截器,将所有的拦截器和handlerMethod封装到HandlerExecutionChain并返回。当然再将拦截器添加到chain的时候,会判断如果是MappedInterceptor,会调用match来判断是否适合这个。
寻找适配器
之后的就简单多了,适配器,在我们初始化的时候设置了四个默认的适配器。所有的适配器实现了HandlerAdapter接口,实现了supports方法,意思就是这个适配器匹配这个handler不。
以@Request,举例来说,再RequestMappingHandlerMapping生成的handlerMethod,对应的适配器就是RequestMappingHandlerAdapter。然后返回这个适配器。
如果是get请求,判断是否用浏览器缓存
浏览器对于GET和HEAD请求有一个缓存机制。如果之前访问过一个URL,浏览器会保存这个URL的响应内容和头信息。然后,当再次向这个URL发起请求时,浏览器会在请求头中加入”If-Modified-Since”字段,这个字段的值就是之前响应头中”Last-Modified”字段的值,表示浏览器上次获得这个URL资源的时间。
在服务器端,如果请求是GET或HEAD,就会通过HandlerAdapter的getLastModified方法获取处理器的最后修改时间(也就是我们写的Controller方法)。如果处理器的最后修改时间早于或等于”If-Modified-Since”字段的值,说明自从浏览器上次请求该URL以来,URL对应的资源没有变化。于是,服务器会返回304 Not Modified状态码,告诉浏览器它的缓存还是有效的,无需重新发送数据,这样可以节省网络带宽和服务器的处理时间。
如果处理器的最后修改时间晚于”If-Modified-Since”字段的值,说明浏览器的缓存已经过期,服务器需要处理这个请求,然后返回新的数据和更新的”Last-Modified”值。
前置拦截器
执行拦截器里边的preHandle方法,如果返回false。不再往下执行,反身把已经执行了preHandle的afterCompletion方法执行了。
执行handler
获取参数,遍历参数,通过不同的参数类型,用相应的参数解析气来给他赋值。
@RequestMapping("/greet") public String greet(@RequestParam("name") String name, Model model) { model.addAttribute("message", "Hello, " + name); return "greeting"; }
遍历所有的 HandlerMethodArgumentResolver 实例,并调用它们的 supportsParameter 方法来判断用哪个 HandlerMethodArgumentResolver 来解析这两个参数的值。
对于 @RequestParam 注解的 name 参数,RequestParamMethodArgumentResolver 会从 HTTP 请求的查询参数中获取名为 “name” 的参数值,并将其转换为 String 类型。
对于 Model 类型的 model 参数,ModelMethodProcessor 会为其创建一个新的 Model 对象,以便在方法体中添加属性。
执行方法
利用反射执行
method.invoke(getBean(), args);
处理返回值
如果有返回值,将处理返回值。会根据返回值类型和value筛选出HandlerMethodReturnValueHandler,来进行封装返回值,如果没有找到对应的handler则报错。
RequestResponseBodyMethodProcessor是处理使用了@ResponseBody注解的方法返回值的HandlerMethodReturnValueHandler实现类。当一个控制器方法使用了@ResponseBody注解,这意味着该方法的返回值应直接写入到HTTP响应的body中,而不是被解析成一个视图名。实际上是将控制器方法的返回值序列化为JSON、XML或其他格式,然后写入到HTTP响应的body中。
当控制器方法的返回类型是ModelAndView时,会被ModelAndViewResolverMethodReturnValueHandler处理,直接返回一个ModelAndView对象。
@RequestMapping("/example") public ModelAndView example() { ModelAndView modelAndView = new ModelAndView("exampleView"); modelAndView.addObject("attribute", "value"); return modelAndView; }
Spring MVC会调用ModelAndViewResolverMethodReturnValueHandler处理返回的ModelAndView对象。处理器会将ModelAndView对象传递给视图解析器,视图解析器解析视图名并将模型数据添加到视图中,生成最终的HTTP响应。
将返回值写进ModelAndView
所有的返回都会尝试去创建ModelAndView
- 如果处理器方法返回的是一个ModelAndView对象,那么Spring MVC会直接使用这个对象。这个对象包含了视图(View)和模型(Model)两部分,视图决定了如何渲染响应页面,模型决定了响应页面中的数据。
- 如果返回的是一个View对象,或者一个字符串(通常是视图名称),那么Spring MVC会使用这个View或者根据字符串解析出的View,再结合Model(也就是处理器方法往Model中添加的数据),创建一个ModelAndView对象。这相当于处理器方法只指定了视图,而没有指定模型。
- 如果处理器方法返回的是其他类型的对象,这个对象通常会被当作Model的一部分,Spring MVC会创建一个新的ModelAndView对象,其中Model就是这个对象,而没有指定视图。
- 如果处理器方法上使用了@ResponseBody注解,或者这个处理器方法所在的类上使用了@RestController注解,那么这个方法的返回值会被直接写入HTTP响应体,不会创建ModelAndView对象,这个方法返回null。这相当于处理器方法不需要视图和模型,只需要返回一个原始数据。
- 如果请求已经被处理(比如在HandlerAdapter或者HandlerInterceptor中已经直接写入了HTTP响应),那么也不会创建ModelAndView,这个方法返回null。这相当于处理器方法不需要执行任何逻辑。
如果没有试图将设置默认试图
如果modelAndView不为null,但是view为null,则设置一个默认的view。
后置拦截器
执行拦截器HandlerInterceptor的postHandle方法
渲染视图
在上边的执行流程中,被trye catch包裹住,如果发生异常会被接住。
如果有异常,会用异常的HandlerExceptionResolver来生成ModelAndView。
然后开始真正的渲染试图:
如果view不为空:遍历所有的视图解析器ViewResolver,去解析ModelAndView,如果有返回值直接返回。在视图渲染过程中,View对象会使用ModelAndView中的Model(模型)数据。这个Model包含了处理器方法添加的所有属性,View可以使用这些属性来生成HTML页面,然后发送给用户。
最后执行拦截器afterCompletion
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/48720.html