前言:
接触spring mvc的时间并不算长。在用这个web框架之前曾经接触过django, flask, asp.net mvc, play等框架。它们都是基于mvc模式的web框架,在基于model, view, controller互相分离和松耦合的约定基础上,它们的实现细节和一些使用方式有不少差别。这里结合mvc模式来分析一下spring mvc里对请求处理的流程。并结合讨论做一个简单的示例。
mvc模式和spring mvc
mvc模式
相信凡是使用过各种web框架做开发的,很少没有人听说过mvc架构模式的。它是一种架构的设计思想,而不是一个具体的实现细节模式。可以将其当做一种设计的指导思想,而根据它所做出的具体设计却可能有很多的差别。一个典型的描述mvc模式的图如下:
如果用一句话来概括mvc模式的话,它是一种通过将视图(view)和业务逻辑以及领域模型(model)分离并由控制器(controller)来协调交互的模式。它本质上实现了各部分职责的分离以及松耦合。比如view部分,只需要关注展示内容而不关心内容是从哪里来的。而model则重点关注数据对象的建模和访问。而controller则关注怎么将外部的请求映射到具体的模型访问并采取合适的视图来展示。
在上图这个笼统的概念模型中,当一个HTTP请求过来的时候,它可能会读取数据或者更新数据,也可能同时有两者。这个时候由controller负责将请求对应到具体的访问。然后controller再选择合适的视图来返回对应的HTTP response。
spring mvc结构
毫无疑问,spring mvc也是基于mvc模式来构建的。那么它的架构有什么特点呢?总的来说,它的体系结构如下图:
它的结构和前面的稍微有点不同。首先它有一个dispatcherServlet来专门做请求的转发,同时也根据返回的结果选择对应的view生成response返回。在下面的Handler部分,则有对应于应用具体的controller,它和model交互,并将交互结果返回给dispatcherServlet。现在,我们再结合一个具体示例来理解里面的概念。
一个简单的示例
配置和工程结构
首先,我们可以创建一个maven-webapp工程。因为目前maven提供的webapp插件比较老,只能支持比较老版本的servlet。一种简单的办法是可以参考我前面一篇文章里提供的一个maven archetype。这样可以快速的构建一个对应的支持servlet 3.1的webapp。如果安装了前面提到的maven archetype之后,创建该工程的过程就比较简单了,只需要选择new->project->maven project->select archetype的时候选择我们新添加的那个archetype,如下图:
在我们设置好必须的package name和project name之后,得到如下的项目结构图:
我们来看每个部分文件的内容:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.yunzero</groupId> <artifactId>FirstSpringWeb</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>Servlet 3 Web Application</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java-version>1.8</java-version> </properties> <dependencies> <!-- Servlet 3.1 API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.2.0.RELEASE</version> </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.2</version> </dependency> <!-- test dependencies --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>${project.artifactId}-${project.version}</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>${java-version}</source> <target>${java-version}</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.6</version> <configuration> <warSourceDirectory>src/main/webapp</warSourceDirectory> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build> </project>
这里主要依赖的库是spring-webmvc,它是spring mvc主要的库文件。里面的log4j主要引用的log4j2。
web.xml:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> </web-app>
对于spring mvc的工程来说,也许最重要的就是web.xml文件了。它定义了整个项目的基础属性配置。结合里面具体的内容,我们来详细的过一下。在servlet这个部分有一个dispatcherservlet的定义:org.springframework.web.servlet.DispatcherServlet 在前面描述spring mvc体系结构的时候,我们已经提到过这么一个dispatcherServlet。它主要是起到一个请求转发和返回视图的作用。所以这里就必须有一个它的定义。然后在servlet-mapping部分就有一个它映射到某个具体的url。在本示例中,它直接映射到根路径,表示以后所有的请求都要通过它来分派。在实际项目中我们可以根据需要定义多个dispatcherServlet,它们分别对应不同的映射路径。servlet里面还要一个配置就是load-on-start,我们将它配置为1表示在工程一启动的时候就自动加载它,如果没有设置它的话,则要等到第一个真正的请求过来的时候才会启动dispatcherServlet。
在讨论下面的内容之前,我们详细的看一下dispatcherServlet。既然是基于spring的容器来做的应用开发框架。每个dispatcherServlet都有它自己的webApplicationContext,和我们普通的应用类似。它的applicationContext的层次结构如下图:
从图中可以看到,dispatcherServlet的applicationContext和Root WebApplicationContext是一个层次关系。它们每个所包含的内容不一样。dispatcherServlet的WebApplicationContext仅仅包含该servlet内的controller, requestMapping以及view等信息。而对于应用整个范围来说的service,它们一般应该定义在root WebApplicationContext中。这也就是为什么我们后面会有一个org.springframework.web.context.ContextLoaderListener的定义。
在前面的定义中,dispatcherServlet有专门的定义它的applicationContext的地方,而对于全局应用的applicationContext,则定义在ContextLoaderListener中。spring mvc里面对于配置的定义受到很多convention over configuration的影响,所以它们很多对应文件命名和配置有一些规律可以遵循。
以前面的配置为例,我们定义了dispatcherServlet,它的名字为dispatcher,那么在没有特殊指定的情况下,spring mvc会在WEB-INF目录下寻找dispatcher-servlet.xml文件来构造它的applicationContext定义文件。所以这里默认的配置文件名就是servlet名-servlet.xml的格式。当然,我们也可以在配置里面指定配置文件所在的地方。一种典型的配置如下:
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:application-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
这里通过init-param部分将对应配置文件设置在classpath下的application-servlet.xml文件里。
除了dispatcherServlet的配置文件定义,还要一个就是ContextLoaderListener,它对应着全局applicationContext的定义和配置。在默认的情况下,spring mvc会查找WEB-INF目录下的appliationContext.xml文件。它就是一个典型的spring bean定义配置文件。和前面的类似,我们如果要对文件做自定义的话,一个典型的配置如下:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/spring/beans-service.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
在我们这个示例里,因为目前没有牵涉到servlet相关配置以外的。关于全局applicationContext的配置文件默认是空的。
现在,我们再来看看dispatcher-servlet.xml文件里的内容:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd"> <mvc:annotation-driven/> <context:component-scan base-package="com.springinpractice.ch03.web"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> </beans>
这部分前面的<mvc:annotation-driven/>表示工程将通过扫描定义的annotation的方式来处理HTTP请求。比如有的类里面加上Controller, RequestMapping的annotation,因为有了这个配置,spring mvc将能够识别它们并将它们和对应的请求和映射起来。
<context:component-scan>则是典型的IOC的配置,在前面讲述spring IOC的文章里已经提到过。这里不再赘述。
这里还要一个部分的定义就是org.springframework.web.servlet.view.InternalResourceViewResolver,它表示我们选择定义视图的方式。这是我们选择的ViewResolver的一种,也是做web开发最常见的方式。prefix, postfix表示我们将对应的view展示文件,一般都是.jsp文件的配置。在前面的示例中,它们表示需要将jsp文件放到WEB-INF/jsp目录下。这样,关于spring mvc里基本的配置就已经结束了。我们在这里的基础上来编写一个示例。
Model
首先我们定义一个示例里需要的domain object:
package com.springinpractice.ch03.model; public class Member { private String firstName; private String lastName; public Member() {} public Member(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String toString() { return firstName + " " + lastName; } }
内容非常简单,无需解释。
Controller
在前面的讨论里我们已经知道,controller是起到一个承接model, view的桥梁作用的。它主要是映射一个请求,然后访问model,再选择view,返回结果。我们这里一个典型定义的controller如下:
package com.springinpractice.ch03.web; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import com.springinpractice.ch03.model.Member; @Controller public class RosterController { private List<Member> members = new ArrayList<Member>(); public RosterController() { members.add(new Member("John", "Lennon")); members.add(new Member("Paul", "McCartney")); members.add(new Member("George", "Harrison")); members.add(new Member("Ringo", "Starr")); } @RequestMapping("/list") public String list(Model model) { model.addAttribute("members", members); return "list"; } @RequestMapping("/member") public String member(@RequestParam("id") Integer id, Model model) { model.addAttribute("member", members.get(id)); return "member"; } }
我们在RosterController类上面加了一个@Controller的annotation,这表示spring mvc将该类作为一个controller来处理。然后在后面的list, member方法里面,我们有一个@RequestMapping的annotation,它用来定义请求和方法的映射。比如@RequestMapping("/list")表示当我们访问路径为"/list"的时候,list方法将用来处理这个请求。
我们再看list方法,该方法有一个Model的参数,它用来绑定我们需要传递给view的内容。比如前面的model.addAttribute("members", members);就是将members这个列表以名字为“members”的参数传递到一个view中。这样view知道传过来的是什么以及怎么去解析。那么,我们怎么来选择让哪个view来展示这个传递的结果呢?在这个方法里,我们返回的是String,这里指定了是list。基于同样的设定,spring mvc里会去jsp目录下面找名字为list.jsp的文件。这样,我们只要定义好对应的文件就可以了。
这样,从controller里将内容绑定传输到view的过程就已经实现了。现在我们再来看看view里怎么展示这些内容。
View
我们先来看看对应的list.jsp文件的内容:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Roster</title> </head> <body> <h1>Roster</h1> <ul> <c:forEach var="member" items="${members}" varStatus="status"> <li> <a href="member?id=${status.index}"> <c:out value="${member}"></c:out> </a> </li> </c:forEach> </ul> </body> </html>
这里,我们用到了core taglib,里面重点就是${members}这个变量就是我们从controller里传过来的。需要注意的是在controller里我们传递的时候是名字为"members",在这里的名字就必须和那里的一致。关于taglib里的一些用法,我们可以参考相关的文档。
现在如果我们启动服务器并浏览页面: http://localhost:8080/FirstSpringWeb/list
我们将看到如下的显示结果:
在前面的代码里还要一个member的方法,那里也有一个对应的请求处理和结果显示过程,我们可以按照同样的流程来分析。
form 处理
前面那一部分我们只是阐述了当我们获取数据和展示的时候,怎么来做。如果我们需要通过表达来提交信息给服务器呢?这个时候的做法稍微有点不一样。我们来看一个新建的controller:
package com.springinpractice.ch03.web; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.springinpractice.ch03.model.Member; @Controller public class NomineeController { private static final Logger logger = LogManager.getLogger(NomineeController.class); @RequestMapping(value = "/form", method = RequestMethod.GET) public String form(@ModelAttribute("nominee") Member member) { return "form"; } @RequestMapping(value = "/form", method = RequestMethod.POST) public String processFormData(@ModelAttribute("nomiee") Member member, Model model) { logger.info("Processing nominee: " + member); Map<String, Object> map = model.asMap(); logger.info("model[member]=" + map.get("member")); logger.info("model[nominee]=" + map.get("nominee")); return "thanks"; } }
这里一个重要的差别就是在@RequestMapping的定义里,我们设置了method属性。实际上如果我们没有设置这个属性,spring mvc会默认的对访问该路径的所有http方法都响应。但是根据http的规范,如果我们希望提交表单数据的话,我们应该选择http post方法,而不是随便什么方法都可以。所以通过设置method属性可以选择处理不同的请求。
这里还要一个值得注意的地方就是@ModelAttribute("nominee")的定义。因为在提交表单的时候经常会有一些这样的情况。我们在表单里定义了若干个字段,然后需要将它们映射到某个实体类上面来。然后在某些情况下需要提交表单时又要将实体类映射到这些字段上去。如果这些都通过手工来做的话这将是一个非常繁琐的过程。于是这里可以用ModelAttribute来绑定处理,减少了很多人工的操作。它是怎么起作用的呢?在controller的form方法里,我们定义了该属性。如果我们打开对应的form.jsp文件看看:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Nominee a member for the award</title> </head> <body> <h1>Nominee a member for the award</h1> <form:form modelAttribute="nominee"> <div>First name: <form:input path="firstName"/></div> <div>Last name: <form:input path="lastName"/></div> <div><input type="submit" value="Submit"></div> </form:form> </body> </html>
这里会有一个<form:form modelAttribute="nominee">的部分。它必须和controller里指定的一致,用来表示form提交字段和定义的实体字段的映射。所以我们可以看到它们显示的内容和我们定义的实体类字段名是一样的。
另外,在处理post请求的方法里,我们将modelAttribute属性映射上之后,实际上就已经将表单转化成Member对象了。在典型的应用里就可以做一些将它们保存到数据等操作了。这里为了简单起见只是做了一个简单的日志记录。
这样,一个简单的包含查看和提交数据的spring mvc应用就完成了。
总结
spring mvc的体系结构依然是遵循典型的mvc模式来设计的。它有一个专门的front controller,也就是dispatcherservlet来分发请求并返回响应。在spring mvc框架里,每个servlet都有自己的applicationContext,它和全局的applicationContext是一个继承关系。所以一种比较典型的方法是针对web servlet的配置和应用的全局公用配置分开定义。所以经常会有dispatcher-servlet.xml, applicationContext.xml来分别定义它们。关于怎么命名和寻找这些配置文件它们是有一定的惯例可以遵循的。
另外,spring mvc里通过典型的annotation driven的方式来标记controller,这样spring mvc就知道哪些可以用来查找和映射HTTP请求。我们通过@RequestMapping里的value来设置映射的请求访问路径,而method来映射访问该路径的HTTP请求方法。spring mvc里为了controller和view之间的松耦合,它通常用Model的参数来传递数据,然而在选择view的时候,controller只需要返回对应view名字的string类型参数就可以了。它可以自动去配置好的目录查找。
在提交表单对象给服务器的时候,@ModelAttribute属性可以很大程度上简化表单字段和我们定义的领域模型之间的映射工作。一个要点就是要保证view里面设定的form属性和controller里的一样。
参考材料
http://www.codesenior.com/en/tutorial/Spring-ContextLoaderListener-And-DispatcherServlet-Concepts
http://simone-folino.blogspot.com/2012/05/dispatcherservlet-vs.html
https://weblogs.java.net/blog/sgdev-blog/archive/2014/07/05/common-mistakes-when-using-spring-mvc
http://syntx.io/difference-between-loading-context-via-dispatcherservlet-and-contextloaderlistener/
http://stackoverflow.com/questions/9016122/contextloaderlistener-or-not
相关推荐
Maven坐标:org.springframework:spring-webmvc:5.0.8.RELEASE; 标签:springframework、spring、webmvc、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档...
《看透Spring MVC:源代码分析与实践》是一本深入探讨Spring MVC框架核心机制的书籍。通过对源代码的解析,读者可以深入了解这个广泛使用的Java Web开发框架的工作原理,从而更好地运用和优化自己的项目。该书提供了...
Spring MVC 是一个基于Java的轻量级Web应用框架,它是Spring框架的重要组成部分,主要用于构建Web应用程序的后端控制器。...通过学习,开发者可以熟练地运用Spring MVC来构建高效、可维护的Web应用。
NULL 博文链接:https://shmilyaw-hotmail-com.iteye.com/blog/2244136
Maven坐标:org.springframework:spring-webmvc:5.3.10; 标签:springframework、spring、webmvc、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 ...
Maven坐标:org.springframework:spring-webmvc:5.2.15.RELEASE; 标签:springframework、spring、webmvc、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档...
Maven坐标:org.springframework:spring-webmvc:5.3.15; 标签:spring、webmvc、springframework、jar包、java、中文文档; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 ...
下面我们将深入探讨Spring MVC的基础知识。 1. **核心组件**: - DispatcherServlet:作为Spring MVC的前端控制器,负责接收HTTP请求,并根据配置分发到相应的处理器。 - HandlerMapping:映射请求到对应的...
Maven坐标:org.springframework:spring-webmvc:5.3.7; 标签:springframework、spring、webmvc、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 ...
Spring MVC属于SpringFrameWork的后续产品,已经融合在..., 精通Spring MVC4zui适合已经熟悉Spring编程基础知识并迫切希望扩展其Web技能的开发人员。通过阅读本书,读者将深度把握Spring MVC的各项特性及实用技巧。
Maven坐标:org.springframework:spring-webmvc:5.3.15; 标签:spring、webmvc、springframework、jar包、java、中英对照文档; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容...
Database interaction using Spring and Hibernate/JPA- Spring Data JPA- Spring Data MongoDB- Messaging, emailing and caching support- Spring Web MVC- Developing RESTful web services using Spring Web ...
Maven坐标:org.springframework:spring-webmvc:5.0.10.RELEASE; 标签:spring、webmvc、springframework、jar包、java、API文档、中文版; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可...
Maven坐标:org.springframework:spring-webmvc:5.3.7; 标签:springframework、spring、webmvc、中英对照文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容...
Maven坐标:org.springframework:spring-webmvc:5.1.3.RELEASE; 标签:springframework、spring、webmvc、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档...
读书笔记:精通Spring MVC 4
书名:《看透Spring MVC:源代码分析与实践》 作者:韩路彪 Spring MVC相关的源码分析