04-SpringMVC请求过程分析(一)
经过前面的铺垫我们开始分析SpringMVC的请求过程,由于整个过程较为复杂,本篇我们只讨论到请求寻址,后面的HandlerMethod调用及返回值处理下次再来分析。因为网上已经有很多流程图了,这里我就不再画流程图了,我们使用Spring官方描述的DispatcherServlet的处理流程来展开分析。
文章图片
DispatcherServletProcess.png 我们将上图我们对照源码来看
文章图片
DoService.png DispatcherServlet在接收到请求之后会将WebApplicatinContext和当前request绑定,并且将localeResolver、themeResolver、themeSource(其实是WebApplicatinContext)绑定在request中。接下来会判断是否是转发过来的请求,如果是则会绑定FlashMap相关属性。最后到达DispatcherServlet的核心doDispatch方法。我们来着重分析这个方法。核心代码如下
/**
* Process the actual dispatching to the handler.
* The handler will be obtained by applying the servlet's HandlerMappings in order.
* The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
* to find the first that supports the handler class.
* All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
* themselves to decide which methods are acceptable.
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception in case of any kind of processing failure
*/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 判断是否是上传文件请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 请求寻址,返回HandlerExecutionChain
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 通过HandlerMapping寻找合适的适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 执行HandlerMapping中配置的拦截器的前置处理逻辑
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 根据request中的请求路径匹配配置过的HandlerMapping并解析请求中的参数将其绑定在request中,随后与HandlerMethod中的方法绑定并调用HandlerMethod方法
// 得到ModelAndView以供后面解析(RequestBody等类型的返回结果会直接返回数据)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 如果返回了视图名称,这里会去拼接视图全路径(prefix+viewName+suffix)
applyDefaultViewName(processedRequest, mv);
// 执行HandlerMapping中配置的拦截器的后置处理逻辑
mappedHandler.applyPostHandle(processedRequest, response, mv);
..............................................
}
【04-SpringMVC请求过程分析(一)】上面这段代码粗略解释了每个方法都做了什么,由于现在大多使用Restful风格的接口,所有在后面的分析中我们会以Restful风格的请求url为例来分析。下面我们以http://localhost:8080/app/helloController/sayHello2/haha为例,Controller代码如下
@RestController
@RequestMapping("/helloController")
public class HelloController {@Autowired
private HelloService helloService;
@GetMapping("/sayHello")
public String sayHello(@RequestParam String guests){
helloService.sayHello(guests);
return "success";
}@GetMapping("/sayHellos")
public String sayHello(@RequestParam String[] guests){
Arrays.asList(guests).forEach( guest -> {
helloService.sayHello(guest);
});
return "success";
}@GetMapping("/sayHello2/{guest}")
public String sayHello2(@PathVariable String guest){
helloService.sayHello(guest);
return "success";
}
}
可以看出总共有3个方法,其中前两个为传统的API风格,最后一个是Restful风格。本文将以第三个方法为例来分析请求过程。
当用户发起请求经过DispatcherServlet的一系列处理后到达getHandlerAdapter方法,这个方法是整个处理过程中的关键,涉及到url寻址及请求路径参数解析。关键代码如下
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 这里的handlerMappings就是初始化时加入的BeanNameUrlHandlerMapping和RequestMappingHandlerMapping
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
// 之前说过BeanNameUrlHandlerMapping是将Bean名称与请求url做匹配,基本不会使用这个HandlerMapping,这里会通过RequestMappingHandlerMapping来处理
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 获取Handler,下面详细分析
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// Bean name or resolved handler?
// 如果返回的是beanName,则通过IOC容器获取bean
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
// 将HandlerMethod封装成HandlerExecutionChain,后面会详细分析
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
return executionChain;
}protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
// 处理url,主要是将配置的servletmapping前缀从url中去除,该方法比较简单,我们不详细分析,只接用源码的注释来说明
/**
* Return the path within the servlet mapping for the given request,
* i.e. the part of the request's URL beyond the part that called the servlet,
* or "" if the whole URL has been used to identify the servlet.
* Detects include request URL if called within a RequestDispatcher include.
* E.g.: servlet mapping = "/*";
request URI = "/test/a" -> "/test/a".
* E.g.: servlet mapping = "/";
request URI = "/test/a" -> "/test/a".
* E.g.: servlet mapping = "/test/*";
request URI = "/test/a" -> "/a".
* E.g.: servlet mapping = "/test";
request URI = "/test" -> "".
* E.g.: servlet mapping = "/*.test";
request URI = "/a.test" -> "".
* @param request current HTTP request
* @return the path within the servlet mapping, or ""
*/
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
this.mappingRegistry.acquireReadLock();
try {
// 这里就开始真正的映射handlerMethod并解析url参数,下面详细分析
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List matches = new ArrayList<>();
// 这里是从urlLookup中获取RequestMappingInfo,一般来说url不带参数({xxx})的请求都可以从这里读到
List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
// 如果在urlLookup中已经拿到了mapping,直接进行匹配操作
if (directPathMatches != null) {
// 下面详细分析
addMatchingMappings(directPathMatches, matches, request);
}
// 如果没有从urlLookup中得到,则通过mappingLookup来获取mapping
if (matches.isEmpty()) {
// No choice but to go through all mappings...
// 下面详细分析
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
// 已经匹配到的话会对能匹配到的mapping进行排序,得到最佳匹配项
if (!matches.isEmpty()) {
Comparator comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
// 匹配项超过1个会做一次判断,默认还是第一个结果
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
// 将最佳匹配项的handlerMethod与request绑定
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
// 将最佳匹配项的mapping与request绑定
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}private void addMatchingMappings(Collection mappings, List matches, HttpServletRequest request) {
// 这里的mappings是在初始化过程中能和当前url匹配的所有RequestMappingInfo,每个mapping会封装有当前mapping能处理的mappingName、params、methods、paths等参数
for (T mapping : mappings) {
// 调用RequestMappingInfo中的getMatchingCondition来获取RequestMappingInfo
T match = getMatchingMapping(mapping, request);
if (match != null) {
// 将RequestMappingInfo封装成Match
matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
}
}
}
喝口水休息休息,本篇文章可能篇幅较长,但此时已经接近终点。希望大家坚持一下,下面我们继续。
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
// 获取当前RequestMappingInfo的各种Condition属性(初始化HandlerMapping的时候创建的)
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (methods == null || params == null || headers == null || consumes == null || produces == null) {
return null;
}
// 通过patternsCondition来匹配request。下面专门分析
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {
return null;
}
// 自定义的Condition,一般为空的
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null) {
return null;
}
// 根据上面生成的patterns和custom以及其他默认属性创建新的RequestMappingInfo并返回
return new RequestMappingInfo(this.name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
// 和上面getHandlerInternal方法中一样,是用来处理ServletMapping和请求路径用的
String lookupPath = this.pathHelper.getLookupPathForRequest(request);
// 真正做匹配的地方,下面专门分析
List matches = getMatchingPatterns(lookupPath);
return (!matches.isEmpty() ?
new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher,
this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions) : null);
}public List getMatchingPatterns(String lookupPath) {
List matches = new ArrayList<>();
// this.patterns就是RequestMapping中配置的url,本例中是/helloController/sayHello2/{guest}
for (String pattern : this.patterns) {
// 获取匹配结果,下面单独分析
String match = getMatchingPattern(pattern, lookupPath);
if (match != null) {
matches.add(match);
}
}
if (matches.size() > 1) {
matches.sort(this.pathMatcher.getPatternComparator(lookupPath));
}
return matches;
}private String getMatchingPattern(String pattern, String lookupPath) {
if (pattern.equals(lookupPath)) {
return pattern;
}
// 这里会调用doMatch方法执行真正的匹配逻辑,通过这么久的学习过程我们应该熟悉看到doXXX方法意味着什么!
if (this.pathMatcher.match(pattern, lookupPath)) {
return pattern;
}
if (this.useTrailingSlashMatch) {
if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
return pattern +"/";
}
}
return null;
}protected boolean doMatch(String pattern, String path, boolean fullMatch,
@Nullable Map uriTemplateVariables) {
// 这里会将匹配模板切分成数组,本例中是["helloController","sayHello2","{guest}"]
String[] pattDirs = tokenizePattern(pattern);
// 这里将实际url切分成数组,本例中是["helloController","sayHello2","haha"]
String[] pathDirs = tokenizePath(path);
int pattIdxStart = 0;
int pattIdxEnd = pattDirs.length - 1;
int pathIdxStart = 0;
int pathIdxEnd = pathDirs.length - 1;
// Match all elements up to the first **
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String pattDir = pattDirs[pattIdxStart];
if ("**".equals(pattDir)) {
break;
}
// 这里会进行Ant风格匹配。
// 注意后面在handlerAdapter处理时还会调用一次这个方法,在这个方法里面会解析url参数并将其绑定在request中
if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
return false;
}
pattIdxStart++;
pathIdxStart++;
}
// 循环结束后如果请求路径数组全部结束说明已经匹配完了
if (pathIdxStart > pathIdxEnd) {
// Path is exhausted, only match if rest of pattern is * or **'s
// 模板数组也匹配结束,判断模板和请求路径是否均以'/'结尾。不是返回true
// 此时得到的pattern是/helloController/sayHello2/{guest}
if (pattIdxStart > pattIdxEnd) {
return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator));
}
if (!fullMatch) {
return true;
}
if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
return true;
}
for (int i = pattIdxStart;
i <= pattIdxEnd;
i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
// .....................后面还有很多.............................
return true;
}
至此已经匹配到mapping就是/helloController/sayHello2/{guest},此时会从mappingLookup中获取对应的HandlerMethod,然后将其封装成Match并返回HandlerMethod。最后在我们分析的第一个方法getHandler中会将得到的HandlerMethod封装成HandlerExecutionChain,最后返回给DispatcherServlet。
// 封装过程很简单,就是将handler保存下来,就不展开说明了
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
现在我们分析完了url寻址获取HandlerMethod的全过程,下篇文章我们来分析获取HandlerAdapter和调用Handler的过程。
由于本人能力有限,难免会有表述不清或错误的地方,还希望各位不吝指教,大家共同学习,一起进步。
推荐阅读
- 全过程工程咨询——时间管理(12)
- https请求被提早撤回
- 遇到不正当请求怎么办
- 普通人进阶的三个过程-很多人都知道,但却本末倒置
- Android系统启动之init.rc文件解析过程
- 会玩才有未来
- 使用Promise对微信小程序wx.request请求方法进行封装
- AnyProxy抓取http/https请求
- JavaScript|vue 基于axios封装request接口请求——request.js文件
- spring|spring boot中设置异步请求默认使用的线程池