这篇文章将深刻探讨Spring框架的一部分——Spring Web MVC的强大功能及其内部工做原理。html
在本文中,咱们将使用最新、最好的Spring Framework 5。咱们将重点介绍Spring的经典Web堆栈,该堆栈从框架的第一个版本中就崭露头角,而且如今依然是用Spring构建Web应用程序的主要方式。java
对于初学者来讲,为了安装测试项目,最好使用Spring Boot和一些初学者依赖项;还须要定义parent:web
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.M5</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 5,咱们还须要使用Spring Boot 2.x。截止到撰写本文之时,这依然是里程碑发布版,可在Spring Milestone Repository中找到。让咱们把这个存储库添加到你的Maven项目中:spring
<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>
你能够在Maven Central上查看Spring Boot的当前版本。apache
为了理解Spring Web MVC是如何工做的,咱们将经过一个登陆页面实现一个简单的应用程序。为了显示登陆页面,咱们须要为上下文根建立带有GET映射的@Controller注解类InternalController。编程
hello()方法是无参数的。它返回一个由Spring MVC解释为视图名称的String(在示例中是login.html模板):json
import org.springframework.web.bind.annotation.GetMapping; @GetMapping("/") public String hello() { return "login"; }
为了处理用户登陆,须要建立另外一个用登陆数据处理POST请求的方法。而后根据结果将用户重定向到成功或失败的页面。api
请注意,login()方法接收域对象做为参数并返回ModelAndView对象:浏览器
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.servlet.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是两个不一样对象的持有者:tomcat
链接这些是为了方便,这样控制器方法能够一次返回它们。
要渲染HTML页面,使用Thymeleaf做为视图模板引擎,该引擎具备可靠和开箱即用的与Spring的集成。
那么,当在浏览器中输入http:// localhost:8080/时,按Enter键,而后请求到达Web服务器,实际发生了什么?你如何从这个请求中看到浏览器中的Web表单?
鉴于该项目是一个简单的Spring Boot应用程序,所以能够经过Spring5Application运行它。
Spring Boot默认使用Apache Tomcat。所以,运行应用程序时,你可能会在日志中看到如下信息:
2017-10-16 20:36:11.626 INFO 57414 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2017-10-16 20:36:11.634 INFO 57414 --- [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2017-10-16 20:36:11.635 INFO 57414 --- [main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.23
因为Tomcat是一个Servlet容器,所以发送给Tomcat Web服务器的每一个HTTP请求天然都由Java servlet处理。因此Spring Web应用程序入口点是一个servlet,这并不奇怪。
简单地说,servlet就是任何Java Web应用程序的核心组件;它是低层次的,不会像MVC那样在特定的编程模式中诸多要求。
一个HTTP servlet只能接收一个HTTP请求,以某种方式处理,而后发回一个响应。
并且,从Servlet 3.0 API开始,你如今能够超越XML配置,并开始利用Java配置(只有很小的限制条件)。
做为一个Web应用程序的开发人员,咱们真正想要作的是抽象出如下繁琐和模板化的任务,并专一于有用的业务逻辑:
Spring DispatcherServlet可以提供这些。它是Spring Web MVC框架的核心;此核心组件接收全部请求到应用程序。
正如你所看到的,DispatcherServlet是很是可扩展的。例如,它容许你插入不一样的现有或新的适配器进行大量的任务:
首先,咱们将简单的HTTP请求的处理追踪到在控制器层中的一个方法,而后返回到浏览器/客户端。
DispatcherServlet具备很长的继承层次结构;自上而下地逐个理解这些是有价值的。请求处理方法最让咱们感兴趣。
理解HTTP请求,不管是在本地仍是远程的标准开发中,都是理解MVC体系结构的关键部分。
GenericServlet是Servlet规范的一部分,不直接关注HTTP。它定义了接收传入请求并产生响应的service()方法。
注意,ServletRequest和ServletResponse方法参数如何与HTTP协议无关:
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
这是最终被任何请求调用到服务器上的方法,包括简单的GET请求。
顾名思义,HttpServlet类就是规范中定义的基于HTTP的Servlet实现。
更实际的说,HttpServlet是一个抽象类,有一个service()方法实现,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是层次结构中第一个Spring-aware类。它使用从web.xml或WebApplicationInitializer接收到的servlet init-param值来注入bean的属性。
在请求应用程序的状况下,doGet(),doPost()等方法应特定的HTTP请求而调用。
FrameworkServlet集成Servlet功能与Web应用程序上下文,实现了ApplicationContextAware接口。但它也可以自行建立Web应用程序上下文。
正如你已经看到的,HttpServletBean超类注入init-params为bean属性。因此,若是在servlet的contextClass init-param中提供了一个上下文类名,那么这个类的一个实例将被建立为应用程序上下文。不然,将使用默认的XmlWebApplicationContext类。
因为XML配置如今已通过时,Spring Boot默认使用AnnotationConfigWebApplicationContext配置DispatcherServlet。可是你能够轻松更改。
例如,若是你须要使用基于Groovy的应用程序上下文来配置Spring Web MVC应用程序,则能够在web.xml文件中使用如下DispatcherServlet配置:
dispatcherServlet org.springframework.web.servlet.DispatcherServlet contextClass org.springframework.web.context.support.GroovyWebApplicationContext
使用WebApplicationInitializer类,能够用更现代的基于Java的方式来完成相同的配置。
HttpServlet.service()实现,会根据HTTP动词的类型来路由请求,这在低级servlet的上下文中是很是有意义的。然而,在Spring MVC的抽象级别,方法类型只是能够用来映射请求到其处理程序的参数之一。
所以,FrameworkServlet类的另外一个主要功能是将处理逻辑从新加入到单个processRequest()方法中,processRequest()方法反过来又调用doService()方法:
@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实现doService()方法。在这里,它增长了一些可能会派上用场的有用对象到请求:Web应用程序上下文,区域解析器,主题解析器,主题源等:
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映射。Flash映射基本上是一种模式,该模式将参数从一个请求传递到另外一个紧跟的请求。这在重定向期间可能很是有用(例如在重定向以后向用户显示一次性信息消息):
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()方法。
dispatch()方法的主要目的是为请求找到合适的处理程序,并为其提供请求/响应参数。处理程序基本上是任何类型的object,不限于特定的接口。这也意味着Spring须要为此处理程序找到适配器,该处理程序知道如何与处理程序“交谈”。
为了找到匹配请求的处理程序,Spring检查HandlerMapping接口的注册实现。有不少不一样的实现能够知足你的需求。
SimpleUrlHandlerMapping容许经过URL将请求映射到某个处理bean。例如,能够经过使用java.util.Properties实例注入其mappings属性来配置,就像这样:
/welcome.html=ticketController /show.html=ticketController
可能处理程序映射最普遍使用的类是RequestMappingHandlerMapping,它将请求映射到@Controller类的@ RequestMapping注释方法。这正是使用控制器的hello()和login()方法链接调度程序的映射。
请注意,Spring-aware方法使用@GetMapping和@PostMapping进行注释。这些注释依次用@RequestMapping元注释标记。
dispatch()方法还负责其余一些HTTP特定任务:
如今Spring已经肯定了请求的处理程序和处理程序的适配器,是时候来处理请求了。下面是HandlerAdapter.handle()方法的签名。请注意,处理程序能够选择如何处理请求:
@Nullable ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
有几种提供的处理程序类型。如下是SimpleControllerHandlerAdapter如何处理Spring MVC控制器实例(不要将其与@ Controller注释POJO混淆)。
注意控制器处理程序如何返回ModelAndView对象,而且不自行呈现视图:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return ((Controller) handler).handleRequest(request, response); }
第二个是SimpleServletHandlerAdapter,它将常规的Servlet做为请求处理器。
Servlet不知道任何有关ModelAndView的内容,只是简单地自行处理请求,并将结果呈现给响应对象。因此这个适配器只是返回null而不是ModelAndView:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ((Servlet) handler).service(request, response); return null; }
咱们碰到的状况是,控制器是有若干@RequestMapping注释的POJO,因此任何处理程序基本上是包装在HandlerMethod实例中的这个类的方法。为了适应这个处理器类型,Spring使用RequestMappingHandlerAdapter类。
注意,控制器方法一般不会使用HttpServletRequest和HttpServletResponse,而是接收和返回许多不一样类型的数据,例如域对象,路径参数等。
此外,要注意,咱们不须要从控制器方法返回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); }
argumentResolvers对象是不一样的HandlerMethodArgumentResolver实例的组合。
有超过30个不一样的参数解析器实现。它们容许从请求中提取任何类型的信息,并将其做为方法参数提供。这包括URL路径变量,请求主体参数,请求标头,cookies,会话数据等。
returnValueHandlers对象是HandlerMethodReturnValueHandler对象的组合。还有不少不一样的值处理程序能够处理方法的结果来建立适配器所指望的ModelAndViewobject。
例如,当你从hello()方法返回字符串时,ViewNameMethodReturnValueHandler处理这个值。可是,当你从login()方法返回一个准备好的ModelAndView时,Spring会使用ModelAndViewMethodReturnValueHandler。
到目前为止,Spring已经处理了HTTP请求并接收了ModelAndView对象,因此它必须呈现用户将在浏览器中看到的HTML页面。它基于模型和封装在ModelAndView对象中的选定视图来完成。
另外请注意,咱们能够呈现JSON对象,或XML,或任何可经过HTTP协议传输的其余数据格式。咱们将在即将到来的REST-focused部分接触更多。
让咱们回到DispatcherServlet。render()方法首先使用提供的LocaleResolver实例设置响应语言环境。假设现代浏览器正确设置了Accept头,而且默认使用AcceptHeaderLocaleResolver。
在渲染过程当中,ModelAndView对象可能已经包含对所选视图的引用,或者只是一个视图名称,或者若是控制器依赖于默认视图,则什么都没有。
因为hello()和login()方法二者都指定所需的视图为String名称,所以必须用该名称查找。因此,这是viewResolvers列表开始起做用的地方:
for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } }
这是一个ViewResolver实例列表,包括由thymeleaf-spring5集成库提供的ThymeleafViewResolver。该解析器知道在哪里搜索视图,并提供相应的视图实例。
在调用视图的render()方法后,Spring最终经过发送HTML页面到用户的浏览器来完成请求处理。
除了典型的MVC场景以外,咱们还可使用框架来建立REST Web服务。
简而言之,咱们能够接受Resource做为输入,指定POJO做为方法参数,并使用@RequestBody对其进行注释。也可使用@ResponseBody注释方法自己,以指定其结果必须直接转换为HTTP响应:
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; @ResponseBody @PostMapping("/message") public MyOutputResource sendMessage( @RequestBody MyInputResource inputResource) { return new MyOutputResource("Received: " + inputResource.getRequestMessage()); }
归功于Spring MVC的可扩展性,这也是可行的。
为了将内部DTO编组为REST表示,框架使用HttpMessageConverter基础结构。例如,其中一个实现是MappingJackson2HttpMessageConverter,它可使用Jackson库将模型对象转换为JSON或从JSON转换。
为了进一步简化REST API的建立,Spring引入了@RestController注解。默认状况下,这很方便地假定了@ResponseBody语义,并避免在每一个REST控制器上的明确设置:
import org.springframework.web.bind.annotation.RestController; @RestController public class RestfulWebServiceController { @GetMapping("/message") public MyOutputResource getMessage() { return new MyOutputResource("Hello!"); } }
在这篇文章中,咱们详细了介绍在Spring MVC框架中请求的处理过程。了解框架的不一样扩展是如何协同工做来提供全部魔法的,可让你可以事倍功半地处理HTTP协议难题。
你们扫描下方二维码关注下个人微信公众号,公众号内没有福利,只会按期生产技术性文章!