初始工程

使用Spring Boot和web,thymeleaf的starter来设置初始工程。xml配置如下:

<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.0.1</version>
   <relativePath/>
</parent> <dependencies>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-thymeleaf</artifactId>
   </dependency>
</dependencies>
测试项目

为了理解Spring Web MVC是如何工作的,可以先实现一个简单的Login功能。创建一个由@Controller来修饰的类InternalController,这个类包含一个处理GET请求的方法。hello()返回一个由Spring解释的视图名字的字符串。(在本例中是login.html

@GetMapping("/")
public String hello() {
   return "login";
}

为了处理用户登陆逻辑,创建另一个接受POST请求的带有Login数据的方法。然后根据处理结果返回成功或者失败页面。
注意,login()函数接受一个领域对象作为参数,返回的是ModelAndView对象。

@PostMapping("/login")
public ModelAndView login(LoginData loginData) {
   if (LOGIN.equals(loginData.getLogin()) 
     && PASSWORD.equals(loginData.getPassword())) {
       return new ModelAndView("success", 
         Collections.singletonMap("login", loginData.getLogin()));
   } else {
       return new ModelAndView("failure", 
         Collections.singletonMap("login", loginData.getLogin()));
   }
}

ModelAndView保存了两个不同的对象:

  • Model: 用来渲染页面用的的map

  • View: 模版页面。

将它们合并起来是为了方便,这样controller的方法就可以同时返回这两个了。
最后使用Thymeleaf作为模版引擎来渲染页面。

Java Web应用的基础-Servlet

当你在浏览器里键入http://localhost:8080/,然后按回车键,请求到达服务器的时候到底发生了什么?是如何在浏览器中看到这个web请求的数据的?
因为这个项目是一个简单的Spring Boot应用,所以可以通过Spring5Application的main方法运行项目。
Spring Boot默认使用Apache Tomcat运行程序,运行成功后可能会看到如下的相同的日志:

2018-04-10 20:36:11.626  INFO 57414 --- [main]  o.s.b.w.embedded.tomcat.TomcatWebServer  :  Tomcat initialized with port(s): 8080 (http)
2018-04-10 20:36:11.634  INFO 57414 --- [main]  o.apache.catalina.core.StandardService   :  Starting service [Tomcat]
2018-04-10 20:36:11.635  INFO 57414 --- [main]  org.apache.catalina.core.StandardEngine  :  Starting Servlet Engine: Apache Tomcat/8.5.23

因为Tomcat是一个Servlet容器,所以几乎所有的HTTP请求都是由Java Servlet处理的。自然的Spring Web的入口就是一个Servlet。
Servlet是所有Java Web应用的核心组件;它非常的底层,并且没有暴露任何具体的编程模式,例如MVC。
一个HTTP的Servelt只能接受HTTP请求,处理请求后返回响应。
最新的Servlet 3.0的API,可以不再使用XML配置,直接可以使用Java配置。

Spring MVC的核心-DispatcherServlet

作为Web开发者,我们希望抽象出以下枯燥的任务,而关注于有用的业务逻辑

  • 将HTTP请求映射到对应的处理函数

  • 将HTTP请求数据和header解析成DTO或者领域对象

  • 使用model-view-controller 设计模式

  • 从DTO,领域对象等直接生成响应

Spring的DispatcherServlet提供了以上的功能,它是Spring WEB MVC框架的核心,是应用接受所有请求的核心组件。
DispatcherServlet可扩展性非常强。例如:它允许你添加现有或者新的适配器来适应不同的任务:

  • 将请求映射到处理它的类或者函数(由HandlerMapping实现)

  • 使用特定模式来处理请求,例如一个普通的Servlet,一个复杂的MVC 工作流,或者只是一个方法。(由HandlerAdapter实现)

  • 通过名字解析试图对象,允许你使用不同的模版引擎,例如:XML,XSLT或者其他视图技术(由ViewResolver实现)

  • 默认使用Apache Commons 的文件上传组件解析文件上传,也可以自己实现。

  • LocalResolver实现本地化,包括cookie,session,HTTP的Accept Header,或者其他由用户定义的本地化。

处理HTTP请求

DispatcherServlet有一个很长的继承层级。自顶向下理解每个单独的概念是非常有必要的。

GenericServlet

GenericServlet是Servlet规范中的一部分,它定义了service()方法,来接受请求和返回响应。

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

服务器所有的请求都会调用这个方法。

HttpServlet

正如其名,HttpServelt是Servlet 规范中关于HTTP请求的实现。
更确切的说,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);
       // ...
   }
HttpServletBean

在这个继承关系中HttpServletBean是第一个Spring的类。从web.xml或者WebApplicationInitialzer获取的初始参数来注入bean的属性。
在应用中的请求分别调用doGet,doPost等方法来处理不同的HTTP请求。

FrameworkServlet

FrameworkServlet实现了ApplicationContextAware,集成Web的Application Context。不过它也可以创建自己的Application Context。
正如上述所言,父类HttpServletBean通过将初始参数作为bean的属性注入。因此如果contex的类名在contextClass这个初始参数中,那么就有这个参数创建application context的实例,否则默认使用XmlWebApplicationContext
由于XML配置现在并不是Spring推荐的配置方式了。Spring Boot默认使用AnnotationConfigWebApplicationContext来配置DispatcherServlet

DispatcherServlet: 统一处理请求

HttpServlet.service()通过HTTP的请求类型将不同的请求发送到不同的方法,这个在底层的servlet实现的很好。但是,在SpringMVC的抽象层次中,不能仅靠方法类型来路由请求。
同样的,FrameworkServlet的另一个主要功能就是将不同的处理使用processRequest()组合在一起。

@Override
protected final void doGet(HttpServletRequest request, 
 HttpServletResponse response) throws ServletException, IOException {
   processRequest(request, response);
} @Override
protected final void doPost(HttpServletRequest request, 
 HttpServletResponse response) throws ServletException, IOException {
   processRequest(request, response);
}
DispatcherServlet: 对请求加入有用的信息

DispatcherServlet实现doService()方法。它向请求中加入了一些有用的对象,然后在web 的请求处理中传递下去,例如:web application context, locale resolver, theme resolver, theme source等

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());

同时,doService()加入了输入输出的Flash Map,Flash Map是将参数从一个请求传递到另一个请求的基本模式。在重定向中很有用。(例如在重定向之后向用户展示一段简单的信息)

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());

接着doService()将会调用doDispatch()方法来分发请求。

DispatcherServlet: 分发请求

dispatch()的主要目的就是找到一个合适的处理请求的处理器并且传递request/response参数。处理器可以是任何对象,并不局限于一个特定的接口。同样也意味着Spring需要找到如何使用这个处理器的适配器。
为了给请求找到合适的处理器,Spring会遍历实现HandlerMapping接口的注册的实现。有很多不同的实现可以满足我们各种需求。
SimpleUrlHandlerMapping使用URL将请求映射到处理bean中。RequestMappingHandlerMapping可能是最广泛使用的映射处理器。它将请求映射到@Controller类下的@RequestMapping修饰的方法上。这个就是上面那个例子中的hello()login()

注意,上面两个方法分别是@GetMapping@PostMapping修饰的。这两个注解来源于@RequestMapping
dispatch()同时也可以处理一些其他的HTTP的任务:

  • 如果资源不存在,对GET请求进行短路处理。

  • 对相应的请求使用multipart 解析。

  • 如果处理器选择异步处理请求,对请求进行短路处理。

处理请求

现在Spring确定了处理请求的处理器和处理器的适配器,是时候处理请求了。下面是HandlerAdapter.handle()的签名。比较重要的一点是处理器可以选择如何处理请求:

  • 直接将响应写入到response body 然后返回null

  • 返回一个由DispatcherServlet渲染的ModelAndView对象。

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

Spring提供了很多类型的处理器,下面是SimpleControllerHandlerAdapter如何处理Spring MVC的controller实例的(不要和@Controller搞混,这里是一个类)。

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

第二个是SimpleServletHandlerAdapter它对一个普通的servlet适配。
servlet并不知道ModelAndView,完全自己处理请求,将返回写入到相应的body中。因此它的适配器就直接返回null。

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

在本例中,controller是由@RequestMapping修饰的POJO,因此处理器会使用HandlerMethod来封装它的方法。Spring使用RequestMappingHandlerAdapter来适配这种处理器类型。

处理参数,返回处理器函数的值

注意,一般来说controller并不会接收HttpServletRequestHttpServletResponse作为参数,但是它可以接收和返回很多种其他类型,例如:领域对象,路径参数等。
同样,也不强求一个controller返回一个ModelAndView实例。可以选择返回一个视图名称,ResponseEntity,或者是一个可以被转换成JSON的POJO。
RequestMappingHandlerAdapter可以保证从HttpServletRequest中解析方法需要的参数,同时创建ModelAndView对象返回。
下面这段代码就是RequestMappingHandlerAdapter中保证这件事情的:

ServletInvocableHandlerMethod invocableMethod 
 = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
   invocableMethod.setHandlerMethodArgumentResolvers(
     this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
   invocableMethod.setHandlerMethodReturnValueHandlers(
     this.returnValueHandlers);
}

argumentResolversHandlerMethodArgumentResolver实例中有不同实现。一共有30多种不同的参数解析器的实现。他们可以从请求参数将函数需要的参数解析出来。包括:url路径变量,请求体参数,请求头,cookies,session等。
returnValueHandlersHandlerMethodArgumentResolver实例中有不同实现。同样也有很多不同的返回值处理器来处理方法返回的结果,创建ModelAndView对象。
例如:当函数hello()返回一个string的时候,ViewNameMethodReturnValueHandler处理这个值。login()返回一个ModelAndView对象的时候,Sring使用ModelAndViewMethodReturnValueHandler处理这个值。

渲染视图

现在Spring已经处理了HTTP请求,获取了ModelAndView实例,现在它需要在用户浏览器渲染HTML页面了。它依赖于由Model和选择的模版组成的ModelAndView对象。
同样的,Spring也可以渲染JSON ,XML或者其他HTTP协议接受的类型。这些将在接下来的REST相关了解更多。
现在回去看一下DispatcherServletrender()首先使用LocaleResolver实例设置返回的Local。首先假设浏览器已经正确设置Accetp头。默认使用AcceptHeaderLocaleResolver来处理。
在渲染过程中,ModelAndView可以包含一个视图的名字或者是已经选择的视图,或者如果controller依赖于默认视图也可以没有。
既然hello()login()方法制定了字符串名字作为视图名称,所以需要使用viewResolvers来查找视图。

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

ViewResolver的实现有很多,这里使用了由thymeleaf-spring5提供的ThymeleafViewResolver实现。解析器知道去哪里查找视图,并且提供相应的视图实例。
调用完render()之后,Spring就完成了将HTML页面渲染到用户浏览器的任务。

深入理解Spring MVC(山东数漫江湖)的更多相关文章

  1. 深入分析Spring 与 Spring MVC容器(山东数漫江湖)

    1 Spring MVC WEB配置 Spring Framework本身没有Web功能,Spring MVC使用WebApplicationContext类扩展ApplicationContext, ...

  2. 透彻理解Spring事务设计思想之手写实现(山东数漫江湖)

    前言 事务,是描述一组操作的抽象,比如对数据库的一组操作,要么全部成功,要么全部失败.事务具有4个特性:Atomicity(原子性),Consistency(一致性),Isolation(隔离性),D ...

  3. Spring MVC 到 Spring Boot 的简化之路(山东数漫江湖)

    背景 从Servlet技术到Spring和Spring MVC,开发Web应用变得越来越简捷.但是Spring和Spring MVC的众多配置有时却让人望而却步,相信有过Spring MVC开发经验的 ...

  4. Spring mvc详解(山东数漫江湖)

    Spring mvc框架 Spring web MVC 框架提供了模型-视图-控制的体系结构和可以用来开发灵活.松散耦合的 web 应用程序的组件.MVC 模式导致了应用程序的不同方面(输入逻辑.业务 ...

  5. SSM三大框架整合详细总结(Spring+SpringMVC+MyBatis)(山东数漫江湖)

    使用 SSM ( Spring . SpringMVC 和 Mybatis )已经很久了,项目在技术上已经没有什么难点了,基于现有的技术就可以实现想要的功能,当然肯定有很多可以改进的地方.之前没有记录 ...

  6. Spring中获取request的几种方法,及其线程安全性分析(山东数漫江湖)

    前言 本文将介绍在Spring MVC开发的web系统中,获取request对象的几种方法,并讨论其线程安全性. 原创不易,如果觉得文章对你有帮助,欢迎点赞.评论.文章有疏漏之处,欢迎批评指正. 欢迎 ...

  7. Spring归纳小结(山东数漫江湖)

    前言 如果说有什么框架是Java程序员必然会学习.使用到的,那么Spring肯定是其中之一.本篇博客,将根据博主在日常工作中对Spring的使用做一个系统的归纳小结. Spring的一些概念和思想 S ...

  8. Spring boot 集成Dubbox(山东数漫江湖)

    前言 因为工作原因,需要在项目中集成dubbo,所以去查询dubbo相关文档,发现dubbo目前已经不更新了,所以把目光投向了dubbox,dubbox是当当网基于dubbo二次开发的一个项目,dub ...

  9. 理解Spring MVC Model Attribute和Session Attribute

    作为一名 Java Web 应用开发者,你已经快速学习了 request(HttpServletRequest)和 session(HttpSession)作用域.在设计和构建 Java Web 应用 ...

随机推荐

  1. hashMap原理(java8)

    (1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的. HashMap最多只允许一条记录的键为null,允许多 ...

  2. DNS域名解析协议

    一. 根域 就是所谓的“.”,其实我们的网址www.baidu.com在配置当中应该是www.baidu.com.(最后有一点),一般我们在浏览器里输入时会省略后面的点,而这也已经成为了习惯. 根域服 ...

  3. 搭建github

    http://www.cnblogs.com/liuxianan/p/build-blog-website-by-hexo-github.html

  4. 把jar包加入本地maven库内

    1首先,在项目的pom.xml文件中加入 <dependency><groupId>taobao-alidayu</groupId>  //名字随便取不要跟已有的重 ...

  5. 查询MySQL某字段相同值得重复数据

    1.先查询重复的id: SELECT book_id,COUNT(*) AS COUNT FROM xs_book_source WHERE site_id=5 GROUP BY book_id HA ...

  6. Maven 3-Maven依赖版本冲突的分析及解决小结 (阿里,美团,京东面试)

    举例A依赖于B及C,而B又依赖于X.Y,而C依赖于X.M,则A除引B及C的依赖包下,还会引入X,Y,M的依赖包(一般情况下了,Maven可通过<scope>等若干种方式控制传递依赖).这里 ...

  7. hdu1950 Bridging signals

    LIS nlogn的时间复杂度,之前没有写过. 思路是d[i]保存长度为i的单调不下降子序列末尾的最小值. 更新时候,如果a[i]>d[len],(len为目前最长的单调不下降子序列) d[++ ...

  8. Android Fragment 使用详解

    虽然网上有很多关于Fragment的文章,但我这里还是要写这篇笔记,因为我在编写程序的过程中发现了一个问题,至今未解决,希望得到大家的帮助: PS:当我在Fragment中定义一个名为setIndex ...

  9. php写错命名空间 导致catch不到异常

    写的微信回调接口出错了, 由于手里的调试工具(包括微信官方的开发者接口调试工具)不能把HTTP错误的详情dump出来,只会显示空白,所以打算在程序里加上try catch 捕获错误直接输出.重新测试, ...

  10. bzoj2144: 跳跳棋(二分/倍增)

    思维好题! 可以发现如果中间的点要跳到两边有两种情况,两边的点要跳到中间最多只有一种情况. 我们用一个节点表示一种状态,那么两边跳到中间的状态就是当前点的父亲,中间的点跳到两边的状态就是这个点的两个儿 ...