请求映射原理
问题情景
你是否有这样的疑问:我编写的Controller中,我写的请求Mapping,SpringBoot怎么知道我要的是这一个,它是如何精准的执行我需要的方法的?还有,欢迎页的显示,我并没有写的Mapping,但是SpringBoot能找到,为什么?那么接下来,本文将共同带你一起一步一步深入源码,请准备好你的IDEA,一步一步跟着本文来慢慢理解,相信你会有所收获,我们带着问题来看看!
问题剖析
我们知道,在springboot中,我们的web请求都是交给springmvc中来做的,而springmvc中的web请求全部都要经过DispatcherServlet,所以,想研究这一块的问题,我们就必须往DispatcherServlet中看一看
IDEA中查找源码DispatcherServlet(shift+shift可以打开查找)
然后找到后,快捷键ctrl+h,可以打开继承树,继承关系一目了然
点进HttpServlet查看,我们知道,在原生的servlet中,有get请求时会执行doGet方法,再进入子类,HttpServletBean中查找是否有这样的方法(Ctrl+f12可以查看该类中的代码结构),并没有看见doGet,那可能就是它的子类,继续往下走,看见FrameworkServlet,一查,发现有doGet,再看,调用的时本类的doService
只可惜是个抽象方法,它还有子类,一定是交给了子类去实现,再往下走到DispatcherServlet中,查找doServce
,其它的乱七八糟的代码我们不看,看样子就是些赋值啥的语句,我们找到与原生请求与响应相关的代码,找到如下:
try {
doDispatch(request, response);
}
....
}
发现就是调用的一个方法,我们再找这个方法,ctrl点进这个方法,发现是一个本类中的内部方法,接着来看看内部方法里面有什么,方法如下:
@SuppressWarnings("deprecation")
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
....
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);//这是一个文件上传请求
// Determine handler for the current request.(注意!中文翻译:确定当前请求的处理程序)
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
.....
}
}
}
那就让我们DeBug一下看看,那句注释后面的一句话是如何执行的吧!把断点打在用getHandler方法的语句上 (handler大家可以理解为就是我们写的一些Controller)
正常运行后访问:localhost:8080/user?,再debug运行,这样可以让我们看见主页,单步运行后跳到本类的一个方法,如下:(已将英文注释翻译)
点开第一个RequestMappingHandlerMapping,找到mappingRegistry(注册映射),发现里面有两个,其中有一个个是我们自己写的/user访问路径,还有一个是内置的error页面,回想好像springboot中web场景会有这样的404页面,是不是有点眉目了?别急这才刚刚开始。
那再让我们分析一下源码
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
for:each循环,就是总共有四个,找到能够处理这样请求的Mapping,交给它去执行请求。
那么关键点就落在了下面这句代码上
HandlerExecutionChain handler = mapping.getHandler(request);
如上源码,这句话就在于匹配对应的程序处理请求,我们debug步入看看
来到AbstractHandlerMapping中的getHandler方法。
第一句话就很重要,将传进来的request去找到内部处理的程序,我们再步入看看
到了RequestMappingInfoHandlerMapping中的getHandlerInternal方法
再步入,来到AbstractHandlerMethodMapping中的getHandlerInternal方法
步过 运行,来到lookupHandlerMethod方法,我们看见,lookupPath变量被解析为了/user,也就是说,当前servlet映射中的映射查找的路径是/user,然后再进入到lookupHandlerMethod方法,将原生的request和要查找的/user传入进行匹配,最终匹配出我们要执行的处理程序,但是这里可能处理程序有两个,因此是一个list集合,而springmvc是不允许我有两个请求的!所以可以看见,在源码中matches.size() > 1就是一个体现,如果大于一个,那就控制台输出一个错误,因此这就完成了一个匹配!执行了我们在RequstMapping(“/user”)中的处理请求。
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
...
总结
所有的请求映射都在HandlerMapping中。
- SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
- SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- - 如果有就找到这个请求对应的handler - 如果没有就是下一个 HandlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。
自定义 HandlerMapping:
有的时候比如说同一组api有不同的版本,比如v1,v2我们可以在controller中写两组mapping(比如v1/user,v2/user),但同时我们也可以放在两个包下,都是/user,这个时候我们就可以自定义handlermapping,把v1/user映射到一个包下的/user,把v2/user映射到另外一个包下的/user。