SpringMVC工作原理实例详解

介绍

SpringWeb MVC是Spring Framework中的一部分,当我们需要使用spring框架创建web应用的时候就需要引入springweb mvc。对于程序员来说,我们只需要增加@Controller ,@RequestMapping注解然后,浏览器中的请求就会到达springweb应用。我们只需要在 controller中编写相关逻辑即可。然而,请求是在哪里接收的?@Controller ,@RequestMapping注解做了什么?本文我们来探讨一下。

从一个项目开始

本文假定你已经能熟练的使用springmvc。为了展开后续的讨论,假设我们新建了一个spring-mvc-demo的项目。并由此项目来展开讨论。项目中有一个控制器,代码如下:

@Controller
@RequestMapping("/app")
public class AppController {
 @RequestMapping(method=RequestMethod.GET,value="/hello")
  public @ResponseBody String hello(HttpServletRequest request,String name) {
  return "Hello,"+name;
 }
}

控制器写好之后,我们将项目打车war包,放入tomcat容器中,并使用8080端口启动tomcat,运行项目,然后在浏览器中输入http://localhost:8080/app/hello?name=world.

我们在浏览器中可以看到:Hello,world的输出。

我们先记住这个例子,下面我们带着一些疑问继续看,这个请求是怎么被接收到的?请求是怎么交给AppController处理的?

Servlet是Java Web应用的基石

当你在浏览器中输入 http://loalhost:8080/ ,按下enter建,然后请求命中了服务器,这是怎么发生的?又如何从这个请求中得到浏览器中可见的页面?

本例中,我们给出的是一个简单的spring-mvc应用,并放入了tomcat中(springboot 内嵌tomcat启动其实也是一样的)。 Tomcat 是一个servlet容器,这点我想每个Java程序员都十分清楚,我们在没有使用spring-mvc之前,就是使用servlet+jsp来开发web应用。

由于Tomcat是一个web容器,每一个发送给Tomcat服务器的HTTP请求自然会被一个Java Servlet处理。所以,SpringMvc 必定有一个servlet,SpringWeb应用的入口必定是一个Servlet,这应该不难想到。

简单来说,Servlet是任何Java Web应用的核心组件(除非你不用servlet规范,比如你使用netty)。Servlet它是低层次的,并且不会像MVC那样强加于特定的编程模式。它只是可以让你写一个偶一个Servlet,一个HTTP Servlet可以接受一个HTTP请求,然后处理它,并返回一个响应。

而springmvc 就是使用了一个大的servlet,下面我们就来说这个大的servlet。

DispatcherServlet是Spring MVC的核心

上面我们已经提到Servlet 是Java web应用的基石,Spring应用入口必定是一个Servlet,这个Servlet 其实就是DispatcherServlet。

作为WEB应用的开发人员,我们真正想做的是抽象出以下繁琐和模板化的任务,并专注于有用的业务逻辑:

  • 映射一个HTTP请求到某个处理方法。
  • 将HTTP请求数据,和头信息转换成数据对象(DTO / domain object)。
  • 模型 - 视图 - 控制器 之间的交互。
  • 从DTO,域对象等生成响应

Spring DispatcherServlet提供了这些。它是Spring Web MVC框架的核心, 这个核心组件接收所有请求到您的应用程序。
DispatcherServlet具有很强的可扩展性。 例如,它允许您插入不同的现有或新适配器以执行大量任务:

  • 将请求映射到应该处理它的类或方法(HandlerMapping接口的实现)
  • 使用特定模式处理请求,例如常规servlet,更复杂的MVC工作流或者POJO bean中的方法(HandlerAdapter接口的实现)
  • 通过名称解析视图,允许您使用不同的模板引擎,XML,XSLT或任何其他视图技术(ViewResolver接口的实现)
  • 通过使用默认的Apache Commons文件上传实现或编写自己的MultipartResolver来解析multipart请求
  • 使用任何LocaleResolver实现解决语言环境,包括Cookie,会话,Accept HTTP标头或用于确定用户期望的语言环境的任何其他方式

处理HTTP请求

首先,让我们来追踪一个简单的HTTP请求到达controller中的方法,然后返回到 浏览器/客户端的处理过程。
DispatcherServlet 有一个很长的继承关系。它的继承关系是这样的:

GenericServlet <- HttpServlet <- HttpServletBean <- FreamworkServlet <- DispatcherServlet

GenericServlet

GenericServlet是Servlet规范的一部分,它并不直接关注HTTP。它定义了一个service()方法用来接收传递过来的请求,并产生响应。(这里的请求和响应不是指HTTP请求)

public abstract void service(ServletRequest req, ServletResponse res)
 throws ServletException, IOException;

注意,这里的参数中的ServletRequest,ServletResponse并不是和HTTP协议绑定的,Http有具体协议Servlet。

HttpServlet

顾名思义,HttpServlet类就是规范中定义的基于HTTP的Servlet实现。

更实际的是,HttpServlet是一个具有service()方法实现的抽象类,它通过HTTP方法类型分割请求,大致如下所示:

protected void service(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException {

  String method = req.getMethod();
  if (method.equals(METHOD_GET)) {
    // ...
    doGet(req, resp);
  } else if (method.equals(METHOD_HEAD)) {
    // ...
    doHead(req, resp);
  } else if (method.equals(METHOD_POST)) {
    doPost(req, resp);
    // ...
  }

根据请求的不同, get,post方法会分别被不同方法处理。

HttpServletBean

上面我们展示过 DispatcherServlet的继承关系,在这个继承链中,HttpServletBean是进入spring的第一个层次。从HttpServletBean开始往下的几个servlet都是spring中的类。HttpServletBean 就是一个servlet,它继承自HttpServlet,就像是我们在使用servlet+jsp开发时候定义的servlet一样。

根据servlet的生命周期我们知道,servlet会被容器初始化,初始化时候,其init()方法会被调用。在springmvc框架中 HttpServletBean使用从web.xml或WebApplicationInitializer收到的servlet init-param值来注入bean的属性。
在请求应用程序的情况下,为这些特定的HTTP请求调用doGet(),doPost()等方法。

FrameworkServlet

FrameworkServlet将Servlet功能与Web应用程序上下文集成,实现ApplicationContextAware接口。但它也能够自行创建Web应用程序上下文。

正如上面所说,FrameworkServlet的超类HttpServletBean将init-param注入为bean属性。所以,如果servlet的contextClass init-param中提供了context类的名字,那么这个context类的实例将会被创建,用作应用的context。否则,将会使用XmlWebApplicationContext作为默认的。

DispatcherServlet: 统一请求处理

有了上面铺垫,我们这里可以开始关键的内容,即DispatcherServlet统一请求处理。在Springmvc的项目中,我们通常会在web.xml配置一个servlet,如下:

  <servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring/spring-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

上面我们提到,DispatcherServlet继承关系,其父类中正儿八经的servlet类是HttpServletBean这个servlet类是应用启动入口。其生命周期的第一阶段init()方法完成初始化工作。

doService()方法设置请求信息

DispatcherServlet 初始化之后,便可以工作了。当请求到达之时,会调用其doService()方法。doService()方法的代码如下:

@Override
  protected void doService(HttpServletRequest request, HttpServletResponse response)
   throws Exception {
  // 删除一下‘非核心代码'方便查看
    // Make framework objects available to handlers and view objects.
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
  try {
      doDispatch(request, response);
    }
    finally {
      if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        return;
      }
      // Restore the original attribute snapshot, in case of an include.
      if (attributesSnapshot != null) {
        restoreAttributesAfterInclude(request, attributesSnapshot);
      }
    }

 }

可以看到,doService()方法先设置一些 request信息,这些信息在后续的处理过程中会用到。设置完后,它调用doDispatch() 方法。

doDispatch()方法分发请求

doService()方法最终调用了doDispatch(),看名知意,这个方法是做分发工作的。其代码如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response)
    throws Exception {
    //删除一些代码方便阅读
    HandlerExecutionChain mappedHandler = null;
    try {
      ModelAndView mv = null;
      Exception dispatchException = null;

      try {
        // 删除一些代码方便阅读
       mappedHandler = getHandler(processedRequest, false);
       HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
        try {
          // Actually invoke the handler.
          mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        }
        finally {
          if (asyncManager.isConcurrentHandlingStarted()) {
            return;
          }
        }
        applyDefaultViewName(request, mv);
        mappedHandler.applyPostHandle(processedRequest, response, mv);
      }
      catch (Exception ex) {
        dispatchException = ex; // 这里捕获了异常TypeMismatchException
      }
      processDispatchResult(processedRequest, response, mappedHandler, mv,                                      dispatchException);
    }
    catch (Exception ex) {
    }
    finally {
      // 删除一些代码
    }
}

这个方法主要作用就是找到合适的 handler 来处理请求。handler通常是一个某个类型的对象,并且不限定特定的接口。因此spring需要找到这个handler的适配器。这个Handler通常是一个HandlerMethod实例,

为了找到与请求匹配handler,spring需要从已注册的HandlerMapping接口实现类里边去找。这个查找过程就是在上面的getHandler() 方法完成得到的是一个HandlerExecutionChain。 这里体现了责任链模式。

这个getHandler() 会遍历一个HandlerMapping的map。由于我们一般都使用注解形式:@Controller,@RequestMapping注解。因此这里找到HandlerMapping实现就是RequestMappingHandlerMapping

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

getHandlerAdapter()方法找到最终的handler适配器,找到的适配器就是RequestMappingHandlerAdapter,(因为我们使用的是@RequestMapping注解形式)。

本例中,我们定义了AppController 的hello()方法,并用@Controller,@RequestMapping对其分别进行注解,因此这里得到的适配器HandlerAdapter 所适配HandlerMethod就是 AppController 的hello()方法的 。

HandlerAdapter处理请求

上面通过 确定了HandlerAdapter之后,就要执行handle() 方法了,即上面代码中,try语句块里边的ha.handle()。handle()方法定义为:

ModelAndView handle(HttpServletRequest request,
           HttpServletResponse response,
           Object handler) throws Exception;

这个方法有两种处理方式:

  • 自己将数据写到response,然后return null
  • 返回一个ModelAndView 对象,让DispatcherServlet自己来处理后面渲染工作。

HandlerAdapter有多重类型,例如

SimpleControllerHandlerAdapter处理spring mvc 的controller实例(注意,不要把这里的controller实例和@Controller注解POJO混淆,这里controller 指的是org.springframework.web.servlet.mvc.Controller ),并返回ModelAndView,代码如下:

public ModelAndView handle(HttpServletRequest request,
 HttpServletResponse response, Object handler) throws Exception {
  return ((Controller) handler).handleRequest(request, response);
}

SimpleServletHandlerAdapter 适配的是 Servlet作为request handler的情况,Servlet是不知道MovelAndView的,所以,它的方法并不负责渲染页面,因此没有返回ModelAndView,只是返回null:

public ModelAndView handle(HttpServletRequest request,
 HttpServletResponse response, Object handler) throws Exception {
  ((Servlet) handler).service(request, response);
  return null;
}

RequestMappingHandlerAdapter 就是我们上面提到的,用来处理@Controller和@RequestMapping注解的handler。

渲染视图

handle()方法调用之后, DispatcherServlet 可以得到一个ModelAndView,当然也可能是null。对于ModelAndView不为null的时候,DispatcherServlet 将会调用render()方法。ModelAndView中可能已经包含了一个view或者只是一个view的名字。如果controller方法指定的是一个字符串形式的视图名字,那么就需要进行试图查找:

for (ViewResolver viewResolver : this.viewResolvers) {
  View view = viewResolver.resolveViewName(viewName, locale);
  if (view != null) {
    return view;
  }
}

render()方法完成之后,最终的HTML页面会被发送至浏览器端。

当然,springmvc不仅能渲染出页面,也可以返回JSON形式或者XML形式。这种情况controller方法一般都是由@RequestBody标注的。这种情况就需要 HttpMessageConverter,例如渲染JSON的时候可以使用Jackson包,我们要返回的对象将由,MappingJackson2HttpMessageConverter来转换。

到此,我们就大概说完了springmvc的整个流程。所以,springmvc其实就是一个大的Servlet,接收请求,分发执行请求,我们的每一个controller中的方法都是一个handler,然后最终渲染视图。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • springboot @ComponentScan注解原理解析

    这篇文章主要介绍了springboot @ComponentScan注解原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 @ComponentScan 告诉Spring从哪里找到bean. 如果你的其他包都在@SpringBootApplication注解的启动类所在的包及其下级包,则你什么都不用做,SpringBoot会自动帮你把其他包都扫描了. 如果你有一些bean所在的包,不在启动类的包及其下级包,那么你需要手动加上@Compone

  • Spring RabbitMQ死信机制原理实例详解

    死信队列:没有被及时消费的消息存放的队列,消息没有被及时消费有以下几点原因: 1.有消息被拒绝(basic.reject/ basic.nack)并且requeue=false 2.队列达到最大长度 3.消息TTL过期 采用死信机制的好处是可以提高系统的稳定性,当消息消费失败后,消息进入死信队列,可以对消息进行补偿,可以达到最终一致性的目标. 具体例子如下: @Bean public Queue deadQueue() { return new Queue(DEAD_QUEUE_NAME, tr

  • SpringCloud Eureka自我保护机制原理解析

    这篇文章主要介绍了SpringCloud Eureka自我保护机制原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1. 自我保护机制演示 eureka在频繁修改微服务名称的时候,可以会出现如下现象: 2. 什么是自我保护模式? 默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒).但是当网络分区故障发生时,微服务与EurekaServer之间无法正常通信

  • Spring Security实现禁止用户重复登陆的配置原理

    这篇文章主要介绍了Spring Security实现禁止用户重复登陆的配置原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 系统使用了Spring Security做权限管理,现在对于系统的用户,需要改动配置,实现无法多地登陆. 一.SpringMVC项目,配置如下: 首先在修改Security相关的XML,我这里是spring-security.xml,修改UsernamePasswordAuthenticationFilter相关Bean

  • Spring事件Application Event原理详解

    这篇文章主要介绍了Spring 事件Application Event原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Spring 的事件(Application Event)为 Bean 与 Bean 之间的消息通信提供了支持.当一个 Bean 处理完一个任务之后,希望另一个 Bean 知道并能做相应的处理,这时我们就需要让另一个 Bean 监听当前 Bean 所发送的事件.(观察者模式) Spring 的事件需要遵循以下流程: 自定

  • Spring Cloud @RefreshScope 原理及使用

    @RefreshScope那些事 要说清楚RefreshScope,先要了解Scope Scope(org.springframework.beans.factory.config.Scope)是Spring 2.0开始就有的核心的概念 RefreshScope(org.springframework.cloud.context.scope.refresh)是spring cloud提供的一种特殊的scope实现,用来实现配置.实例热加载. Scope -> GenericScope -> R

  • SpringMVC工作原理实例详解

    介绍 SpringWeb MVC是Spring Framework中的一部分,当我们需要使用spring框架创建web应用的时候就需要引入springweb mvc.对于程序员来说,我们只需要增加@Controller ,@RequestMapping注解然后,浏览器中的请求就会到达springweb应用.我们只需要在 controller中编写相关逻辑即可.然而,请求是在哪里接收的?@Controller ,@RequestMapping注解做了什么?本文我们来探讨一下. 从一个项目开始 本文

  • Java包装类的缓存机制原理实例详解

    这篇文章主要介绍了Java包装类的缓存机制原理实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 java 包装类的缓存机制,是在Java 5中引入的一个有助于节省内存.提高性能的功能,只有在自动装箱时有效 Integer包装类 举个栗子: Integer a = 127; Integer b = 127; System.out.println(a == b); 这段代码输出的结果为true 使用自动装箱将基本类型转为封装类对象这个过程其实

  • Java CAS底层实现原理实例详解

    这篇文章主要介绍了Java CAS底层实现原理实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.CAS(compareAndSwap)的概念 CAS,全称Compare And Swap(比较与交换),解决多线程并行情况下使用锁造成性能损耗的一种机制. CAS(V, A, B),V为内存地址.A为预期原值,B为新值.如果内存地址的值与预期原值相匹配,那么将该位置值更新为新值.否则,说明已经被其他线程更新,处理器不做任何操作:无论哪种情

  • C++与Lua交互原理实例详解

    首先,不同语言交互,我们一般把这种编程方式称为混合编程.开发中为什么要多语言混合使用,只用c++不行吗?答案是因为脚本语言语法糖多使用方便.沙盒式安全机制使系统更稳定.简单易学降低开发成本等,那么,只用脚本不行吗?那也是不合理的,因为与系统api的接口.计算密集性模块的性能要求等是脚本语言不擅长的,这一部份仍然需要c/c++来完成.因此,为了综合各自的优势,就出现了混合编程的需要. 那么,在一个程序的生命周期里,具体哪些部份c++写,哪些部份是脚本写?它们的交互接口又在哪里?一般与系统紧密相关的

  • JAVA线程池原理实例详解

    本文实例讲述了JAVA线程池原理.分享给大家供大家参考,具体如下: 线程池的优点 1.线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用. 2.可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃. 线程池的创建 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQu

  • PHP Curl多线程原理实例详解

    给各位介绍一下Curl多线程实例与原理.不对之处请指教相信许多人对php手册中语焉不详的curl_multi一族的函数头疼不已,它们文档少,给的例子 更是简单的让你无从借鉴,我也曾经找了许多网页,都没见一个完整的应用例子.curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle

  • JavaScript解析机制与闭包原理实例详解

    本文实例讲述了JavaScript解析机制与闭包原理.分享给大家供大家参考,具体如下: js解析机制: js代码解析之前会创建一个如下的词法环境对象(仓库):LexicalEnvironment{ } 在扫描js代码时会把: 1.用声明的方式创建的函数的名字; 2.用var定义的变量的名字存到这个词法环境中; 3.同名的时候:函数声明会覆盖变量,下面的函数声明会覆盖上面的同名函数; 4.函数的值为:对函数的一个引用; 变量的值为undefined; 5.如果用函数表达式的方式创建一个函数: va

  • JavaScript模板引擎实现原理实例详解

    本文实例讲述了JavaScript模板引擎实现原理.分享给大家供大家参考,具体如下: 1.入门实例 首先我们来看一个简单模板: <script type="template" id="template"> <h2> <a href="{{href}}" rel="external nofollow" > {{title}} </a> </h2> <img src

  • Node.js控制台彩色输出的方法与原理实例详解

    前言 我们都知道,在nodejs环境下使用普通的console.log,console.error,console.info输出都是不会有颜色的,如果你不知道,那你现在知道啦

  • Java三种IO模型原理实例详解

    Java中IO的模型分为三种,同步阻塞的BIO.同步非阻塞的NIO.异步非阻塞的AIO. BIO[同步阻塞] 在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行. NIO[同步非阻塞] NIO本身是基于事

随机推荐