草庐IT

源码剖析Spring MVC如何将请求映射到Controller?

秃秃爱健身 2023-04-04 原文

文章目录

一、前言

最近有朋友问我:Spring MVC 中如何将请求映射到指定的Controller中的;

结合博主之前的 Spring MVC的请求执行流程 一文,这里做一个更细粒度的分析。

从Spring MVC的请求执行流程来看,DispatcherServlet#doDispatch()方法中会做请求的映射;具体体现在获取请求对应的HandlerExecutionChain逻辑中。

二、核心链路分析

1、确定请求映射的入口

DispatcherServlet#getHandler()

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    // 在Spring初始化的时候会加载所有的handlerMappings
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            // 获取请求对应的HandlerExecutionChain
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

1)HandlerMapping注入Spring容器

Spring启动时会加载所有HandlerMapping类型的Bean到IOC容器中,默认有5个,分别为:

  1. RequestMappingHandlerMapping,请求处理器
  2. BeanNameHandlerMapping
  3. RouterFunctionMapping
  4. ResourceHandlerMapping
  5. WelcomePageHandlerMapping

针对HTTP GET、POST、PUT等普通请求,RequestMappingHandlerMapping负责处理,并且其中包含了 所有可以处理的请求路径、以及 请求和相应Controller(具体的类、方法)的映射Mapping。

2)HandlerMethod注册到MappingRegistry

Spring启动时会加载RequestMappingHandlerMapping到IOC容器中,RequestMappingHandlerMapping中使用MappingRegistry 保存了所有的请求映射关系,使用HandlerMethod保存了一个请求映射

RequestMappingInfoHandlerMapping 继承了 抽象类 AbstractHandlerMethodMapping, AbstractHandlerMethodMapping又实现了InitializingBean接口、重写了InitializingBean#afterPropertiesSet()方法;

因此实例化RequestMappingInfoHandlerMapping时会进入到AbstractHandlerMethodMapping#afterPropertiesSet()方法

进入到initHandlerMethods()方法之后,会遍历IOC容器中所有的Bean,如果Bean被@Controller@RequestMapping 注解标注,则将Class中被@RequesMapping 注解标注的方法解析为HandlerMapping、并注册到MappingRegistry中。

1> 判断Class是否为一个Handler

所谓的判断Class是否为一个Handler,即判断Class是否被@Controller 或 @RequestMapping 注解标注;

AbstractHandlerMethodMapping#isHandler()方法用于判断某个类是否为一个拥有 MethodHandler的处理器。

具体的实现在其子类RequestMappingHandlerMapping中:

仅仅判断类有没有被@Controller 或 @RequestMapping 注解标注。

2> 解析Class中的所有HandlerMethod 并注册到MappingRegistry中

确定一个类被@Controller 或 @RequestMapping 注解标注 后,需要进一步确定Class类中的哪几个方法是HandlerMethod(被@RequestMapping注解标注)、可以处理什么URL路径;

遍历类的方法,Spring封装了几层,具体的执行链路如下:

解析方法的HandlerMethod信息 的逻辑 被封装到函数式接口(@FunctionalInterface)一路往下传递(从放在MetadataLookup中 到 MethodCallback 中)。

(1) 解析方法的HandlerMethod信息:

如果方法没有被@RequestMapping注解标注,则返回null,否则返回具体的HandlerMapping信息,比如:

(2) 将HandlerMethod注册到MappingRegistry中:

注册完一个HandlerMethod之后,MappingRegistry的内容如下:

2、请求路径匹配

请求进入到DispatcherServlet之后的时序图:

对应的代码执行链路如下:

1)解析请求路径

UrlPathHelper#getLookupPathForRequest()方法中会对请求进行路径解析;其中会从两个维度进行路径解析:

  1. mvc层面,处理请求本身的路径;
  2. SERVLET层面,处理请求 和 Servlet配置的Mapping 的关系;比如:
    servlet mapping = “/test/*”; request URI = “/test/a” -> “/a”.

1> MVC层面请求路径解析


这里主要做三件事:

(1)获取请求的ContextPath:

  • 就一般请求而言,请求的contextPath都为“”;
  • 如果在RequestDispatcher include中调用,则检测include请求URL;
/**
	 * Return the context path for the given request, detecting an include request
	 * URL if called within a RequestDispatcher include.
	 * <p>As the value returned by {@code request.getContextPath()} is <i>not</i>
	 * decoded by the servlet container, this method will decode it.
	 * @param request current HTTP request
	 * @return the context path
	 */
public String getContextPath(HttpServletRequest request) {
	String contextPath = (String) request.getAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE);
	if (contextPath == null) {
		contextPath = request.getContextPath();
	}
	if (StringUtils.matchesCharacter(contextPath, '/')) {
		// Invalid case, but happens for includes on Jetty: silently adapt it.
		contextPath = "";
	}
	return decodeRequestString(request, contextPath);
}

(2)获取请求HttpServletRequest的URI:

/**
	 * Return the request URI for the given request, detecting an include request
	 * URL if called within a RequestDispatcher include.
	 * <p>As the value returned by {@code request.getRequestURI()} is <i>not</i>
	 * decoded by the servlet container, this method will decode it.
	 * <p>The URI that the web container resolves <i>should</i> be correct, but some
	 * containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"
	 * in the URI. This method cuts off such incorrect appendices.
	 * @param request current HTTP request
	 * @return the request URI
	 */
public String getRequestUri(HttpServletRequest request) {
	String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE);
	if (uri == null) {
		uri = request.getRequestURI();
	}
	return decodeAndCleanUriString(request, uri);
}

(3)获取requestUri和contextPath的差值:

  • 将给定的“mapping”(即:contextPath)与“requestUri”的开头匹配,如果匹配,则返回额外的部分;
  • 该方法用于解决 HttpServletRequest返回的上下文路径和servlet路径中没有分号内容的问题。
/**
 * Match the given "mapping" to the start of the "requestUri" and if there
 * is a match return the extra part. This method is needed because the
 * context path and the servlet path returned by the HttpServletRequest are
 * stripped of semicolon content unlike the requestUri.
 */
@Nullable
private String getRemainingPath(String requestUri, String mapping, boolean ignoreCase) {
	int index1 = 0;
	int index2 = 0;
	for (; (index1 < requestUri.length()) && (index2 < mapping.length()); index1++, index2++) {
		char c1 = requestUri.charAt(index1);
		char c2 = mapping.charAt(index2);
		if (c1 == ';') {
			index1 = requestUri.indexOf('/', index1);
			if (index1 == -1) {
				return null;
			}
			c1 = requestUri.charAt(index1);
		}
		if (c1 == c2 || (ignoreCase && (Character.toLowerCase(c1) == Character.toLowerCase(c2)))) {
			continue;
		}
		return null;
	}
	if (index2 != mapping.length()) {
		return null;
	}
	else if (index1 == requestUri.length()) {
		return "";
	}
	else if (requestUri.charAt(index1) == ';') {
		index1 = requestUri.indexOf('/', index1);
	}
	return (index1 != -1 ? requestUri.substring(index1) : "");
}

2> Servlet层面请求路径解析

如果配置了alwaysUseFullPath,则不会做Servlet层面的请求路径解析;

此处不对Servlet层面请求路径的解析进行过多讲解,一般不会走进去。

2)根据请求路径找到相应的HandlerMethod

在解析完请求的路径之后,对MappingRegistry加一个读锁,然后再做路径匹配;

真正的请求路径匹配逻辑在AbstractHandlerMethodMapping#lookupHandlerMethod()方法中;

  • 根据请求路径lookupPath从MappingRegistry的urlLookup缓存中找到路径对应的请求映射信息RequestMappingInfo
  • 由于REST ful风格接口的缘故,可能根据请求路径会找到多个RequestMappingInfo,因此需要再对RequestMappingInfo做条件匹配,找到相应RequestMethod的RequestMappingInfo;
  • 然后再根据找到的唯一的RequestMappingInfo从MappingRegistry的mappingLookup缓存中找到请求映射信息对应的HandlerMethod,也就找到了具体的某个Controller中的某个方法。

1> 查找RequestMappingInfo

因为Rest ful风格的缘故,可能会找到多个RequestMappingInfo。

MappingRegistry的urlLookup缓存是在SpringBoot启动时初始化的,见文章上半部分。

动态地址做正则匹配

如果请求是动态地址,例如:@GetMapping("/get/{orderId}"),则无法从MappingRegistry的urlLookup缓存中获取到请求路径对应的RequestMappingInfo,此时需要遍历所有的RequestMappingInfo,做正则匹配,进而找到具体的HandlerMethod

2> 查找HandlerMethod

因为请求的RequestMethod为GET,所以GET类型的RequestMappingInfo符合条件;


MappingRegistry的mappingLookup缓存也是在SpringBoot启动时初始化的,见文章上半部分。

最后将获取到的HandlerMethod一路向上返回;

三、总结

Spring启动时会加载HandlerMapping的所有实现类;包括:负责处理HTTP请求的RequestMappingHandlerMapping

  • RequestMappingHandlerMapping中使用MappingRegistry 保存了所有的请求映射关系,使用HandlerMethod保存了一个请求映射

  • RequestMappingInfoHandlerMapping间接实现了InitializingBean接口,因此RequestMappingInfoHandlerMapping实例化时会进去到重写后的InitializingBean#afterPropertiesSet()逻辑;

    1. 遍历IOC容器中所有的Bean,如果Bean被@Controller@RequestMapping 注解标注,则将Class中被@RequesMapping 注解标注的方法解析为HandlerMapping、并注册到MappingRegistry中。

      • 将请求路径 和 RequestMappingInfo 作为KV保存在urlLookup缓存中;

        private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
        
      • 将RequestMappingInfo 和 HandlerMethod 作为kv保存在mappingLookup缓存中。

        private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
        
    2. 请求打过来之后,首先会对请求路径从两个维度进行处理;

      • APP层面,处理请求本身的路径;
        • 将requestURI路径和请求头的contextPath做差值。
        • 比如:requestURI是“/test/a”,contextPath是“/test”,则返回的请求路径为“/a”。
      • SERVLET层面,处理请求 和 Servlet配置的Mapping 的关系;
        • 比如:servlet mapping = “/test/*”,假如requestURI是“/test/a”,则会去找“/a”路径。
    3. 然后根据解析后的请求路径去MappingRegistry中的urlLookup缓存找RequestMappingInfo;

      • 由于Rest ful风格的存在,可能根据一个请求路径找到多个RequestMappingInfo;

      • 所以需要进一步通过RequestMethod找到执行类型(GET/POST/PUT/DELTE)的RequestMappingInfo

      • 动态地址无法根据请求路径找到具体的RequestMappingInfo,需要遍历所有的RequestMappingInfo做正则匹配,找到具体的RequestMappingInfo。

        比如:@GetMapping("/get/{orderId}")

    4. 然后再根据RequestMappingInfoMappingRegistry中的mappingLookup缓存找HandlerMethod。

      • HandlerMethod中保存了请求需要执行的Class(Controller)、Method;

有关源码剖析Spring MVC如何将请求映射到Controller?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  9. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  10. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

随机推荐