Java框架-Spring MVC理解004-spring MVC处理请求

spring MVC处理请求

 首先,理一下思路:

 

springMVC请求处理的概念图:说明:

1.用户发起请求到前端控制器。

2.前端控制器通过处理器映射器查找hander。

3.处理器映射器返回执行链。 a)hander对象 b)拦截器(集合)

4.前端控制器通处理器适配器包装,执行hander对象。思考:为什么要通过适配器来执行?

5.通过模型hander处理业务逻辑。

6.处理业务完成后,返回ModeAndView对象,其中有视图名称,模型数据。

7.将视图名称和模型数据返回到前端控制器。

8.前端控制器通过视图解释器查找视图对象。

9.视图解释器返回真正的视图。

10.前端控制器通过返回的视图和数据进行渲染。

11.返回渲染完成的视图。

12.将最终的视图返回给用户,产生响应。
//=====================================================================================//

1)当请求到达springmvc前段控制器的时候,会到达DispatcherServlet的doService()方法

2)接着会调用doDispatcher()方法

3)接着会调用getHandler(processedRequest)获取当前的处理器 

4)看getHandler(processedRequest)方法,会返回当前请求的处理器链。当前处理器联封装了负责请求的处理器及其方法

5)根据请求的处理器获取处理器适配器,通过调用getHandlerAdapter()获取

6)接下来调用handler()方法处理请求 

7)接着进入handler()方法瞧一瞧,来到了类的AbstractHandlerMethodAdapter的handleInternal()方法 

8)最后执行调用 

9)调用结束会返回modelAndView对象 

源码分析

我们这里分两步:

  首先分析HttpServletBean、FrameworkServlet和DispatcherServlet这三个Servlet的处理过程,这样大家可以明白从Servlet容器将请求交给Spring MVC一直到DispatcherServlet具体处理请求之前都做了些什么,最后再重点分析Spring MVC中最核心的处理方法doDispatch的结构。
       一、HttpServletBean
  HttpServletBean主要参与了创建工作,并没有涉及请求的处理。之所以单独将它列出来是为了明确地告诉大家这里没有具体处理请求。
       二、FrameworkServlet
  Servlet的处理过程:首先是从Servlet接口的service方法开始,然后在Http-Servlet的service方法中根据请求的类型不同将请求路由到了doGet、doHead、doPost、doPut、doDelete、doOptions和doTrace七个方法,并且做了doHead、doOptions和doTrace的默认实现,其中doHead调用doGet,然后返回只有header没有body的response。
  在FrameworkServlet中重写了service、doGet、doPost、doPut、doDelete、doOptions、doTrace方法(除了doHead的所有处理请求的方法)。在service方法中增加了对PATCH类型请求的处理,其他类型的请求直接交给了父类进行处理;doOptions和doTrace方法可以通过设置dispatchOptionsRequest和dispatchTraceRequest参数决定是自己处理还是交给父类处理(默认都是交给父类处理,doOptions会在父类的处理结果中增加PATCH类型);doGet、doPost、doPut和doDelete都是自己处理。所有需要自己处理的请求都交给了processRequest方法进行统一处理。

下面来看一下service和doGet的代码,别的需要自己处理的方法都和doGet类似。
// org.springframework.web.servlet.FrameworkServlet
protected void service(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
  String method = request.getMethod();
  if (method.equalsIgnoreCase(RequestMethod.PATCH.name())) {
    processRequest(request, response);
  }else {
    super.service(request, response);
  }
}
protected final void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
  processRequest(request, response);
}

  我们发现这里所做的事情跟HttpServlet里将不同类型的请求路由到不同方法进行处理的思路正好相反,这里又将所有的请求合并到了processRequest方法。当然并不是说Spring MVC中就不对request的类型进行分类,而全部执行相同的操作了,恰恰相反,Spring MVC中对不同类型请求的支持非常好,不过它是通过另外一种方式进行处理的,它将不同类型的请求用不同的Handler进行处理,后面再详细分析。可能有的读者会想,直接覆盖了service不是就可以了吗?HttpServlet是在service方法中将请求路由到不同的方法的,如果在service中不再调用super.service(),而是直接将请求交给processRequest处理不是更简单吗?从现在的结构来看确实如此,不过那么做其实存在着一些问题。比如,我们为了某种特殊需求需要在Post请求处理前对request做一些处理,这时可能会新建一个继承自DispatcherServlet的类,然后覆盖doPost方法,在里面先对request做处理,然后再调用supper.doPost(),但是父类根本就没调用doPost,所以这时候就会出问题了。虽然这个问题的解决方法也很简单,但是按正常的逻辑,调用doPost应该可以完成才合理,而且一般情况下开发者并不需要对Spring MVC内部的结构非常了解,所以Spring MVC的这种做法虽然看起来有点笨拙但是是必要的。

下面就来看processRequest方法,processRequest是FrameworkServlet类在处理请求中最核心的方法。
// org.springframework.web.servlet.FrameworkServlet
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
  long startTime = System.currentTimeMillis();
  Throwable failureCause = null;
  // 获取LocaleContextHolder中原来保存的LocaleContext
  LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
  // 获取当前请求的LocaleContext
  LocaleContext localeContext = buildLocaleContext(request);
  // 获取RequestContextHolder中原来保存的RequestAttributes
  RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();

  //获取当前请求的ServletRequestAttributes
  ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

  WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
  asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
  //将当前请求的LocaleContext和ServletRequestAttributes设置到LocaleContextHolder和RequestContextHolder
  initContextHolders(request, localeContext, requestAttributes);

  try {
  //实际处理请求入口
  doService(request, response);
  }catch (ServletException ex) {
  failureCause = ex;
  throw ex;
  }catch (IOException ex) {
    failureCause = ex;
    throw ex;
  }catch (Throwable ex) {
    failureCause = ex;
    throw new NestedServletException("Request processing failed", ex);
  }finally {
  //恢复原来的LocaleContext和ServletRequestAttributes到LocaleContextHolder和RequestContextHolder中
  resetContextHolders(request, previousLocaleContext, previousAttributes);
  if (requestAttributes != null) {
  requestAttributes.requestCompleted();
}
  //省略了log代码
  //发布ServletRequestHandledEvent消息
  publishRequestHandledEvent(request, response, startTime, failureCause);
}
}

  processRequest方法中的核心语句是doService(request,response),这是一个模板方法,在DispatcherServlet中具体实现。在doService前后还做了一些事情(也就是大家熟悉的装饰模式):首先获取了LocaleContextHolder和RequestContextHolder中原来保存的LocaleContext和RequestAttributes并设置到previousLocaleContext和previousAttributes临时属性,然后调用buildLocaleContext和buildRequestAttributes方法获取到当前请求的LocaleContext和RequestAttributes,并通过initContextHolders方法将它们设置到LocaleContextHolder和Request-ContextHolder中(处理完请求后再恢复到原来的值),接着使用request拿到异步处理管理器并设置了拦截器,做完这些后执行了doService方法,执行完后,最后(finally中)通过resetContextHolders方法将原来的previousLocaleContext和previousAttributes恢复到Locale-ContextHolder和RequestContextHolder中,并调用publishRequestHandledEvent方法发布了一个ServletRequestHandledEvent类型的消息。

  这里涉及了异步请求相关的内容,除了异步请求和调用doService方法具体处理请求,processRequest自己主要做了两件事情:

  ①对LocaleContext和RequestAttributes的设置及恢复;

  ②处理完后发布了Servlet-RequestHandledEvent消息。


  首先来看一下LocaleContext和RequestAttributes。LocaleContext里面存放着Locale(也就是本地化信息,如zh-cn等),RequestAttributes是spring的一个接口,通过它可以get/set/removeAttribute,根据scope参数判断操作request还是session。这里具体使用的是ServletRe-questAttributes类,在ServletRequestAttributes里面还封装了request、response和session,而且都提供了get方法,可以直接获取。下面来看一下ServletRequestAttributes里setAttribute的代码(get/remove都大同小异)。

// org.springframework.web.context.request.ServletRequestAttributes
public void setAttribute(String name, Object value, int scope) {
if (scope == SCOPE_REQUEST) {
if (!isRequestActive()) {
throw new IllegalStateException(
"Cannot set request attribute - request is not active anymore!");
}
this.request.setAttribute(name, value);
}else {
HttpSession session = getSession(true);
this.sessionAttributesToUpdate.remove(name);
session.setAttribute(name, value);
}
}

  设置属性时可以通过scope判断是对request还是session进行设置,具体的设置方法非常简单,就是直接对request和session操作,sessionAttributesToUpdate属性后面讲到Session-AttributesHandler的时候再介绍,这里可以先不考虑它。需要注意的是isRequestActive方法,当调用了ServletRequestAttributes的requestCompleted方法后requestActive就会变为false,执行之前是true。这个很容易理解,request执行完了,当然也就不能再对它进行操作了!你可能已经注意到,在刚才的finally块中已调用requestAttributes的requestCompleted方法。
  现在大家对LocaleContext和RequestAttributes已经有了大概的了解,前者可以获取Locale,后者用于管理request和session的属性。不过可能还是有种没有理解透的感觉,因为还不知道它到底怎么用。不要着急,我们接下来看LocaleContextHolder和Request-ContextHolder,把这两个理解了也就全部明白了!
先来看LocaleContextHolder,这是一个abstract类,不过里面的方法都是static的,可以直接调用,而且没有父类也没有子类!也就是说我们不能对它实例化,只能调用其定义的static方法。这种abstract的使用方式也值得我们学习。在LocaleContextHolder中定义了两个static的属性。

// org.springframework.context.i18n.LocaleContextHolder
private static final ThreadLocal<LocaleContext> localeContextHolder =
new NamedThreadLocal<LocaleContext>("Locale context");

private static final ThreadLocal<LocaleContext> inheritableLocaleContextHolder =
new NamedInheritableThreadLocal<LocaleContext>("Locale context");

 

这两个属性都是ThreadLocal<LocaleContext>类型的,LocaleContext前面已经介绍了,ThreadLocal大家应该也不陌生,很多地方都用了它。

LocaleContextHolder类里面封装了两个属性localeContextHolder和inheritableLocale-ContextHolder,它们都是LocaleContext,其中第二个可以被子线程继承。Locale-ContextHolder还提供了get/set方法,可以获取和设置LocaleContext,另外还提供了get/setLocale方法,可以直接操作Locale,当然都是static的。这个使用起来非常方便!比如,在程序中需要用到Locale到时候,首先想到的可能是request.getLocale(),这是最直接的方法。不过有时候在service层需要用到Locale的时候,再用这种方法就不方便了,因为正常来说service层是没有request的,这时可能就需要在controller层将Locale拿出来,然后再传进去了!当然这也没什么,传一下就好了,但最重要的是怎么传呢?服务层的代码可能已经通过测试了,如果想将Locale传进去可能就需要改接口,而修改接口可能会引起很多问题!而有了LocaleContextHolder就方便多了,只需要在server层直接调用一下LocaleContextHolder.getLocale()就可以了,它是静态方法,可以直接调用!当然,在Spring MVC中Locale的值并不总是request.getLocale()获取到的值,而是采用了非常灵活的机制,在后面的LocaleResolver中再详细讲解。
  RequestContextHolder也是一样的道理,里面封装了RequestAttributes,可以get/set/removeAttribute,而且因为实际封装的是ServletRequestAttributes,所以还可以getRequest、getResponse、getSession!这样就可以在任何地方都能方便地获取这些对象了!另外,因为里面封装的其实是对象的引用,所以即使在doService方法里面设置的Attribute,使用RequestContextHolder也一样可以获取到。
  在方法最后的finally中调用resetContextHolders方法将原来的LocaleContext和Request-Attributes又恢复了。这是因为在Sevlet外面可能还有别的操作,如Filter(Spring-MVC自己的HandlerInterceptor是在doService内部的)等,为了不影响那些操作,所以需要进行恢复。
最后就是publishRequestHandledEvent(request,response,startTime,failureCause)发布消息了。在publishRequestHandledEvent内部发布了一个

ServletRequestHandledEvent消息,代码如下:
// org.springframework.web.servlet.FrameworkServlet
private void publishRequestHandledEvent(
HttpServletRequest request, HttpServletResponse response, long startTime, Throwable failureCause) {
// publishEvents可以在配置Servlet时设置,默认为true
if (this.publishEvents) {
// 无论请求是否执行成功都会发布消息
long processingTime = System.currentTimeMillis() - startTime;
int statusCode = (responseGetStatusAvailable ? response.getStatus() : -1);
this.webApplicationContext.publishEvent(
new ServletRequestHandledEvent(this,
request.getRequestURI(), request.getRemoteAddr(),
request.getMethod(), getServletConfig().getServletName(),
WebUtils.getSessionId(request), getUsernameForRequest(request),
processingTime, failureCause, statusCode));
}
}

 

  当publishEvents设置为true时,请求处理结束后就会发出这个消息,无论请求处理成功与否都会发布。publishEvents可以在web.xml文件中配置Spring MVC的Servlet时配置,默认为true。我们可以通过监听这个事件来做一些事情,如记录日志。
下面就写一个记录日志的监听器。

@Component
public class ServletRequestHandledEventListener implements ApplicationListener<ServletRequestHandledEvent> {
final static Logger logger = LoggerFactory.getLogger("RequestProcessLog");
@Override
public void onApplicationEvent(ServletRequestHandledEvent event) {
logger.info(event.getDescription());
}
}

  我们可以看到,只要简单地继承ApplicationListener,并且把自己要做的事情写到onApplicationEvent里面就行了。很简单吧!当然要把它注册到spring容器里才能起作用,如果开启了注释,只要在类上面标注@Component就可以了。
  首先是在service方法里添加了对PATCH的处理,并将所有需要自己处理的请求都集中到了processRequest方法进行统一处理,这和HttpServlet里面根据request的类型将请求分配到各个不同的方法进行处理的过程正好相反。然后就是processRequest方法,在processRequest里面主要的处理逻辑交给了doService,这是一个模板方法,在子类具体实现,另外就是对使用当前request获取到的LocaleContext和RequestAttributes进行了保存,以及处理完之后的恢复,在最后发布了ServletRequestandledEvent事件。

  DispatcherServlet在004中说明......

posted @ 2018-05-15 21:22  Hello,波仔  阅读(202)  评论(0编辑  收藏  举报