本文后续将开启一个系列,顺着作者学习 Spring MVC 文档的脚步,从零开始搭建一个基于 Spring MVC 的 web 应用,并且根据 Spring MVC 的文档内容,选择现有的,用的比较多的,实现性比较好的特性,基于其代码实现,来讲解其源码和背后的原理,这既是对自己在 Spring 全家桶的学习的检验,也可以让我讲一讲自己对于 Spring MVC 的一些特性的理解。本人也是菜鸟程序员一枚,开始写这个系列的时候入行也不到半年。因此,尽量可以站在初学者的角度,来解决我们大家在学习的路上一些老师想当然,而我们却一直没有办法自己解决的问题。希望可以在不影响工作的情况下,尽量做到每周一更甚至每周两更,希望这个系列可以帮助到各位。相关代码可以移步 我的 github 仓库 ,如果有什么问题,也烦请大佬们在这个系列中,多多指正。
使用 Intellij 新建工程选项,在 maven 选项卡里面选择 maven-archetype-webapp,输入对应的工程名,点击创建,可以得到一个已经初始化了 webapp 文件。
在 main 文件夹下,分别创建 java 和 resources 文件夹,点击 File - Project Structure,将 main 和 resources 设置为 Source Folders (蓝色),和 Resource Folder (紫色)。
之后,在 java 的文件夹中设置好 package 的路径,本例的 package 名字为 com.test.myapp 。
为了让工程支持 Spring 的特性,需要在 pom.xml 中引入 springframework 的依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.0.RELEASE</version> </dependency> 复制代码
下面,就开始用代码实例解释 Spring MVC 的文档。
这一部分主要告诉我们:DispatcherServlet 是 Spring MVC 的中央处理器,它负责把请求分发到控制器中,这被称为“前端控制器”的设计模式,模式框架如下图所示:
因此,DispatcherServlet 前端的 Incoming Request 发到(Delegate)对应的 Controller 下面,在 Controller 处理了 Request,并且创建 model 之后,将响应和 model 封装到对应的 view template 中,之后 view template 将控制权交还到 DispatcherServlet,并由其返回响应。
而实际操作中,DispatcherServlet 由于继承了 HttpServlet 类,也需要在 web.xml 文件下进行声明。因此,可以照着文档中的例子进行 DispatchServlet 的配置:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Dispatched Servlet Demo</display-name> <servlet> <servlet-name>example</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> <!--load on startup 大于 0 代表这个容器在应用启动时就加载,而等于 0 则表示在这个容器被选择时加载--> <!--load-on-startup 的值越小,代表加载的优先级越大--> </servlet> <servlet-mapping> <servlet-name>example</servlet-name> <url-pattern>/example/*</url-pattern> </servlet-mapping> </web-app> 复制代码
如上述代码所示,我们配置了一个名为 example 的 dispatcherServlet,而当 url 的后缀为 example 时,默认将使用该 DispatcherServlet 进行请求(request)的转发和视图(view )的返回。
值得一提的是,我们在 web.xml 中定义了名为 example 的 DispatcherServlet,Spring 将为在 WEB-INF 中寻找名为 example-servlet.xml 的文件以获取对应的 bean 配置信息。因此,我们还需要创建 example-servlet.xml 的文件以设置对应的 bean 信息。
example-servlet 中需要我们指定 bean 的位置,即 Spring MVC 系统需要去哪个地方寻找 bean 的配置并自动装配。另一方面,需要配置 viewResolver 以便返回视图。因此,我们在 example-servlet.xml 中首先需要包含这些信息,下述代码配置了 example-servlet 中的 bean 的扫描路径,以及视图解析器 viewResolver。
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd"> <context:component-scan base-package="com.test.myapp.example"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/views/" p:suffix=".jsp" p:order="1"> </bean> </beans> 复制代码
首先解释一下头文件
头文件的配置更多的是理解一个类似于 package 的概念,在定义了 namespace(xmlns) 之后,你需要哪些元素或者组件,则定义他的规则以及 instance 的规则,以便后续的 xml 配置使用这些配置。
context部分表示了这个工程要去哪里去寻找这个应用的 component,并实现实例的自动装配,这里制定了一个针对 component 的过滤器(filter),使它只会在遇到 controller 的类时候进行自动装配。
在配置好 servlet 的转发后,我们需要配置其扫描路径下的 Controller 和对应的 jsp 视图。在整理完成对应用途的文件和文件夹之后,工程结构如下图所示:
![工程文件结构](/Users/shenruofan/Desktop/屏幕快照 2018-10-22 下午5.51.03.png)
首先在 com.test.my.example.controller 下创建对应的 controller 类:
package com.test.myapp.example.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping public class ExampleController { @RequestMapping(value="/hello", method = RequestMethod.GET) public String helloWorld() { return "hello"; } } 复制代码
值得注意的是,由于在 example-servlet.xml 中已经配置了 url 的格式,它表示这个 servlet 只在 /example/* 的 url 格式下面才会生效。因此当我们在 helloWorld 这个方法上指定 RequestMapping 为 "hello" 时,则令其实际生效的 url 应当为 /example/hello 。由于 helloWorld() 方法返回了 "hello",则 viewResolver 在 views 下面去寻找名为 hello.jsp 的文件。因此,我们还需要一个 hello.jsp 的视图文件
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>hello world</title> </head> <body> <h1>example::::::hello world!</h1> </body> </html> 复制代码
之后就是运行 tomcat 时所用到的相关配置,并且 maven 打包这个工程,本机使用了 localhost:8080 端口,则输入了 http://localhost:8080/example/hello 之后,网页出现了
![效果图](/Users/shenruofan/Desktop/屏幕快照 2018-10-23 下午3.09.32.png)
的样子,则说明这个 controller 成功返回了 view 视图。类比上述“前端控制器”的设计模式,我们可以看到:
当前端有请求进来时,比如 /example/hello,Spring MVC 将会识别这个 url 下生效的 servlet,然后该 servlet 会把它分发到 url 映射到的方法上,而该方法会返回一个视图的名称到 servlet;之后,servlet 会去找对应名字的 template,组装完成后,再返回到用户的前端。因此,我们才可以看到 hello.jsp 的前端样式展现在我们眼前。
这篇文档的最后提到了这样一句话:
当你的应用中只需要一个 DispatcherServlet
时,只配置一个根 context 对象也是可行的。
因此,我们可以试一下只配置一个 ContextConfig 的条件下,能不能导航到这个 hello.jsp 文件。
首先,更新 web.xml 的配置,使其只使用一个 ContextConfig 来管理 servlet 下面的 beans,配置内容如下:
<?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"> <display-name>Dispatched Servlet Demo</display-name> <!--使用根 context 來管理 servlet 的配置--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>WEB-INF/root-context.xml</param-value> </context-param> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value></param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> </web-app> 复制代码
这里注意一下 servlet-mapping 的 url 拦截问题,其实源文档的这种配置会有问题,如果不加 .do 的话,会把 jsp 解析的请求也拦截下来,造成 * noHandlerFound for .jsp 的问题,因此需要一个特殊的 url 后缀进行区分,防止 servlet 拦截不该拦截的请求。其次,我们需要配置 WEB-INF 路径下的 root-context.xml 文件,配置方法与上面的 example-servlet.xml 的相同。controller 亦然,需要主要的是,现在是当你输入 /hello.do 时,可以返回 hello.jsp 的视图。
之后也是运行 tomcat 时所用到的相关配置,并且 maven 打包这个工程,本机使用了 localhost:8080 端口,则输入了 http://localhost:8080/hello.do 之后,网页出现了
![效果图](/Users/shenruofan/Desktop/屏幕快照 2018-10-23 下午3.09.32.png)
证明我们的 root context 的配置是有效的。
通过上面的配置,看起来 RootContext 和 servlet 的配置文件对 Spring MVC 工程的作用是一样的,其实不然,根据官方文档:
Spring lets you define multiple contexts in a parent-child hierarchy. The applicationContext.xml defines the beans for the "root webapp context", i.e. the context associated with the webapp. The spring-servlet.xml (or whatever else you call it) defines the beans for one servlet's app context. There can be many of these in a webapp, one per Spring servlet (e.g. spring1-servlet.xml for servlet spring1, spring2-servlet.xml for servlet spring2). Beans in spring-servlet.xml can reference beans in applicationContext.xml, but not vice versa. All Spring MVC controllers must go in the spring-servlet.xml context. In most simple cases, the applicationContext.xml context is unnecessary. It is generally used to contain beans that are shared between all servlets in a webapp. If you only have one servlet, then there's not really much point, unless you have a specific use for it.
大概的意思是,ApplicationContext.xml 和 Servlet.xml 是父子之间的关系,其中一个 Spring mvc 工程里应当只有一个 ApplicationContext,然而可以有多个 Servlet 的配置;并且,Servlet.xml 可以去引用 ApplicationContext 中的 bean,但是反过来却不行。
本章讨论了 Spring MVC 中 web.xml,以及下面的 servlet.xml 或者 contextConfig.xml 的配置方法及注意事项,并用实际代码的方式,理解 Spring MVC 的前端控制器设计思想。