一、前言
太多的时候总是会遇到群里面问,报404怎么肥事呀,前台的参数怎么后台收不到呀……,上次就在群里面遇到过,围绕这一个点:input的name值是不是错了呀,人家妹子都截了好几次图说没有问题,然而这个话题还是继续了30多分钟,看不下去了就私聊 妹子说:debug FrameworkServlet.java中的processRequest方法,一步步的确定问题定位问题,该讨论卒!!
框架的出现大大的提升了开发效率,以及提升项目的规范性,但是也提升了学习的成本,以及遇到问题解决问题的成本,运气好的我们在网上可以找到解决的方案,但是假若找不到我们也要学会去定位问题,逐步的剖析问题而不是手足无措,群里面能解决的问题都比较有限,毕竟老鸟一般都在忙项目和家庭,所以还是需要依靠自己。
同时也说一下我写博客的大致的思路
- 要么就是以应用场景来写起,例如:不为人知的springboot的技巧就是完全以应用场景来驱动的
- 源码解析,一般通常都会给出流程图出来的,就算不想看下面的具体的解析,也可以依据这个流程图来自己看代码(很多时候框架考虑多种情况,具体调用的实现类不一样,要具体分析(不要喷我哟))
二 、把图呈上来
三、具体的核心代码分析:
在这里值分享核心的代码点和感兴趣的代码点
3.1 FrameworkServlet
3.1.1 service()
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
// 这个地方是防止一些特殊的请求,service中不能覆盖到。所以做的一个判断
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request, response);
} else {
/**
* 这个地方虽然是调用了父类的service()方法,但是最终调用的还是 doPost|doDelete …… 等方法
* 最核心的方法还是:processRequest
*/
super.service(request, response);
}
}
这个其实没有什么好说的,这个是j2ee的规范来的,service来处理业务请求,在super.service()方法中其实最后调用的还是processRequest(),这个是个核心的方法
3.1.1 processRequest()
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 这个地方是记录请求的开始的时间,做计时用的,下面有一个事件发布,其实我们监听那个事件就可以知道每次的请求的耗时
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
// 这逼完全是为了国际化的
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
/**
* 这个地方就是,spring对于reqeust的一些参数的封装的地方
* 很多的时候,都会新手去问为啥我前台传的参数,后台收不到,我的name值没有写错呀,遇到这种问题我们要学会去分析
* - 1、debut processRequest 方法,查看参数有没有到这个地方,如果有的同时还需要检查格式是否正确
* - 2、上一步没有问题的情况下,还有一中情况,就是java(spring)对于json的请求参数的支持需要加注解的
* - 2.1、有很多时候我们的参数是实体,那么我们还要去找对应的解析实体的部分
* 通过上面的一些解析,就能大概的查询到问题出在什么地方了。
*/
/** 是个自定义标签,大家可以百度一下如何自定标签
* iftk-doubt 这个地方没有明白下面的语句的意义是什么,当然也有可能我的理解有误。
* 但是肯定不想网上所说的那样,是取的上一次的 属性集,
* 因为这个是依据本次线程的来获取的,另外这里什么时候设置进去的是tomcat的请求就已经设置好了
* RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
*
* 初步估计这个可能涉及到获取相关的filter等信息,只是估计 待验证
*/
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
// 这个没有什么好说的,直接封装当前的 请求属性集
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
// 这个分支我们先不管,从名字中来看应该是做异步请求的部分
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
/**
* 这里才是把request同当前的线程绑定起来的实现
* 要注意一下这里的知识点:ThreadLocal 只能在同一个线程的生命周期中共享变量
* 如果在父子线程中要想共享变量的话,只能通过InheritableThreadLocal才能够实现
*
* 这里就是我们上次所说的springboot你不知道的点的实例三的实现代码,当然这个部分springmvc也是可以用的
*/
initContextHolders(request, localeContext, requestAttributes);
try {
// 调用子类来处理请求
doService(request, response);
} catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
} catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
} finally {
// 后续的做一些释放处理,因为这里可能涉及到,之前设置的filter等相关的信息
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
if (logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", failureCause);
} else {
if (asyncManager.isConcurrentHandlingStarted()) {
logger.debug("Leaving response open for concurrent processing");
} else {
this.logger.debug("Successfully completed request");
}
}
}
/**
* 不管如何这里都把事件发布出去。
* 这里我们可以通过 implements ApplicationListener<ServletRequestHandledEvent> 实现onApplicationEvent 方法
* 来获取相关的信息
* url=[/user/register];
* client=[0:0:0:0:0:0:0:1];
* method=[GET];
* servlet=[dispatcherServlet];
* session=[null];
* user=[null];
* time=[8ms];
* status=[OK]
*
*/
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
3.2 DispatcherServlet 这个是 SpringMvc 的核心的调度类
3.2.1 doService() 这个是 j2ee 的规范来的,servlet生命周期中处理请求的方法
方法的主要作用就是对 request 进一步的增强,设置一些属性。并没有太多精华的地方,着重的我们要看doDispatch(request, response);
这个实际的调度的功能
/**
* Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}
* for the actual dispatching.
* 这里翻译的大致意思是:将特定于DispatcherServlet的请求参数暴漏出来并委托给实际的调度者。
* 大概的意思是:通过判断之前封装的特殊的标记(一些springmvc封装的参数)。来找不同的处理者来进行处理
*/
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
" processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
}
/**
* isIncludeRequest
* public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
* 这里的是为<jsp:incluede page="xxx.jsp"/> 这中指令做的一个需求,这里可以看到j2ee的架构还是比较大的,同时历史的包袱
* 是很重的,在这里我们不做过多的讲解
*/
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// 提过框架对象处理程序和视图对象
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
// 还是国际化的部分
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
// 这个是springmvc 支持的主题
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
// 对于重定向的场景,这里把数据缓存起来,保证在重定向后还能获取一些数据
if (this.flashMapManager != null) {
// retrieveAndUpdate 这个地方是借助于session来缓存数据的
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
// 这里做实际的调度
doDispatch(request, response);
} finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
3.3 doDispatch 方法
这个方法是整个 request 的执行的调度的执行官,接受请求 request 然后借助九大组件的组合处理返回 response,所以这个作为请求的核心的入口,我们依据这条线来查看后续的所有的处理流程的源码
/** 大体的意思就是,这里将调用按照servlet映射的顺序来获取相应的handler来进行实际的处理
* Process the actual dispatching to the handler. 简单翻译:调度适配的handler来程序
* <p>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.
* <p>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 {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 首先判断请求的类型,是否为文件上传的类型
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 通过对于request的种类来获取相应的handler, 看下这个地方是如何获取 handler 的
// @iftk-mark 这个地方需要着重的看一下
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
/** 其实这个地方就是找寻controller的处理方法来的
* 找到合适的处理器来处理请求
* ha ==> public java.lang.String top.fkxuexi.business.controller.MultiYmlTestController.ymlTest()
*/
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
// 这个是对于header中是否包含last-modified的处理
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 这里就比较爽了,这个地方是 spring 的 interceptor 调用的地方
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
/**
*真正的调用适配起来来进行处理 , 调用controller的方法来进行处理 这个通常我们看
* @see org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
*/
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mv = mv;
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// 解析视图
applyDefaultViewName(processedRequest, mv);
/**
* HandlerInterceptor 执行HandlerInterceptor链,并执行post处理
*/
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception ex) {
dispatchException = ex;
} catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
3.3.1、上面的代码中,我们需要注意的有意义的代码
- HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
这个 handler 是何时注入进去的 - !mappedHandler.applyPreHandle(processedRequest, response)
这个地方的调用 interceptor 来做拦截处理,什么时候注入的,执行顺序如何(这个比较简单在spirng中,直接按照xml的顺序解析即可,在springboot 当中直接按照addInterceptor的顺序执行即可) - mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
这个地方就是实际的执行流程的处理的地方了,在这里我们将看到 springmvc 对于参数上的处理,以及如何通过反射调用我们 controller 的类的方法
3.3.2 上面没有提到的,但是需要思考的
- springmvc 是借助于 url 来定位我们的 controller 的类的方法的,但是我们这篇包括之前的博文都没有讲到,是如何定位的,这针对我们的 404 除了参数引起的外还有可能是 url 的解析问题引起的,所以这个看是得看看的,因为这个可能涉及到 spring 的ioc 的初始化过程,这个过程是spring的核心也是相当复杂的,所以我们以先捡软柿子捏的原则和态度,先搞定其他的,后面我们带这个问题在继续探讨
- 上面也有提到过,是如何借助于适配器模式,来为我们挑选 handler 的,这个上述的代码中也没有讲到,这也是我们以后需要带着这些问题去继续阅读源码的
四、借助于idae自定义标签:iftk-doubt
在众多的框架都兴衰的过程中,spring 一直一枝独秀经久不衰,由于 spring 的架构体系过于的庞大,很多类经常都是 2000 多行左右的,众多的代码众多的类,如何快速的标记我们感兴趣的点,同时标记我们有疑惑的点,我们需要善用我们的开发工具,todo
功能,todo 是编辑器为我们自带的,但是我们发现 spring 的源码中也有很多的 todo ,为了区别是 spring 自带的还是我们标注的,所以我们需要借助于自定义标签来做以区分。下面是如何自定义标签的方法:直接打开idea 的setting 在输出框中输出 todo 会出现下面的选项卡,直接输入自己自定的标签即可
- 疑惑的点:这个代码没有搞懂含义,
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
这些地方我都做有标记即上面大的那些标签,idea 的这个地方是做的相当的赞的。
上面所说道的参数的解析的部分以及如何把请求参数封装成我们方法中的model,这些都在spring的九大组件之一的 HandlerAdapter 的流程的解析当中,这里我们仅仅是分析一下流程,下面我们则具体到流程线上的某些点来具体的探讨,来探讨 spring 是如何的优雅的将这些处理器注入到处理流程当中的,来探讨 spring 如何优雅的使用设计模式的,当然这些或许不再下次的博文的讨论范畴当中,但是如果看 spring 的源码,这些还是必须要搞懂的毕竟精华就在这个地方。
博客首发地址csdn:https://blog.csdn.net/weixin_42849915
简书地址:https://www.jianshu.com/u/4b48be4cf59f
希望结识更多的乐于分享的伙伴一起前行