Spring mvc 和Servlet
自从昨天学了servlet,我隐约感觉spring mvc和servlet有着千丝万缕的关系。果然,随机打开web.xml一看,果然,spring mvc需要在web.xml注册一个servlet。所以我大胆推测spring mvc就是一个servlet,spring mvc在启动过程中,先是调用了servlet的init方法,扫描所有的类,找出所有注解了@controller的类,在这些类中,再把注解了@RequestMapping的方法和url绑定起来,可能是存在某个map里面去了。当有请求过来的时候,调用servlet的service方法,service方法的作用就是从这个map里面根据url找出对应的方法,利用反射原理调用该方法(可能我又得去看看反射),把参数注入进去,得到我们想要的结果。为了验证猜想是否正确,我们来看看spring mvc的源码。
最简单spring MVC demo(maven)
demo效果:返回客户端的ip,并显示在页面上
-
先看看总目录:
image.png pom文件:引入相关的包
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.huangzp.test</groupId>
<artifactId>spring-mvc-test</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- spring mvc 的打包方式需要配置成war-->
<packaging>war</packaging>
<properties>
<org.springframework-version>4.2.7.RELEASE</org.springframework-version>
</properties>
<dependencies>
<!-- servlet 相关包,因为spring mvc实际上就是一个servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Spring MVC 相关包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${org.springframework-version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- maven的jetty插件,无需安装容器,只要mvn jetty:run 即可跑容器 -->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>8.1.5.v20120716</version>
<configuration>
<stopPort>9966</stopPort>
<stopKey>foo</stopKey>
<scanIntervalSeconds>10</scanIntervalSeconds>
<webApp>
<contextPath>/</contextPath>
</webApp>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 配置web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xmlns:websocket="http://www.springframework.org/schema/websocket">
<!-- 字符集 过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Spring view分发器 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/applicationContext.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>
<!-- servlet 监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/*.xml</param-value>
</context-param>
</web-app>
因为spring mvc 本质上还是一个servlet,所以我们要在web.xml 配置servlet,要在pom引入servlet相关的包(servlet-api)。
- 配置dispatcher-servlet
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"
default-autowire="byName">
<!-- 开启spring的扫描注入,使用如下注解 -->
<!-- @Component,@Repository,@Service,@Controller-->
<context:component-scan base-package="com.huangzp.test"/>
<!-- 开启springMVC的注解驱动,使得url可以映射到对应的controller -->
<mvc:annotation-driven />
<!-- View Resolver for JSPs -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/page/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
- 编写Controller
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello(HttpServletRequest request, Model model){
model.addAttribute("ip", request.getRemoteAddr());
return "hello";
}
}
- 编写 jsp文件(要和dispatcher-servlet中配置的位置相同)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title></title>
</head>
<body>
你的ip:${ip}
</body>
</html>
-
启动程序,输入访问地址
成功
spring mvc结构图(基于4.0.2)

SpringMVC理解
DispatcherServlet
我们说过Spring MVC其实就是一个servlet,那我们就先来看看这个servlet。
-
结构
DispatcherServlet
由图可知,DispatcherServlet(继承)FrameworkServlet(继承)FrameworkServlet(继承)HttpServlet(继承)GenericServlet(实现)Servlet。
- init()
在之前servlet的学习中我们知道如果一个类继承HttpServlet,那么肯定会实现init(),service(),destroy()三个方法;我们猜DispatcherServlet/FrameworkServlet/FrameworkServlet中某个类会实现init()接口,然后把url会method的对应关系保存起来。那我们来看看是不是。
那我们要怎么看呢?我们要怎么才能找到存放url和method的map呢?我们是按着init()接口一步一步找下去?还是有什么另外的蹊径呢?在我们完整的启动了一次spring mvc项目以后,我们会发现console端会有这样的日志输出:
七月 16, 2018 8:06:12 上午 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping registerHandlerMethod
信息: Mapped "{[/hello2],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String com.huangzp.test.HelloController.hello2(javax.servlet.http.HttpServletRequest,org.springframework.ui.Model)
七月 16, 2018 8:06:12 上午 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping registerHandlerMethod
信息: Mapped "{[/hello],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String com.huangzp.test.HelloController.hello(javax.servlet.http.HttpServletRequest,org.springframework.ui.Model)
registerHandlerMethod单看方法名,再结合下面的Mapped...emmmm...这应该就是我们要找的把url和method存到一起的一个方法。所以我们用全文搜索registerHandlerMethod,然后找个断点debug一下。我们先来看看它的调用栈:

我们可以看到springmvc先是调用了HttpServletBean的init()方法 ,然后再调用了FrameworkServlet的一系列方法,再调用了AbstracBeanFactory/AbstractAutowireCapableBeanFactory的一系列方法,最后才调用RequestMappingHandlerMapping/AbstractHandlerMethodMapping的方法。(感慨一下idea的debug功能真的超级方便。)
再focus registerHandlerMethod这个方法:

这里传入三个参数,一个是method(方法,反射),一个是handler(方法所在bean的名字),最后一个是mapping(可以理解成我们的方法上的注解RequestMapping)。方法做的事很简单,method和handler封装成一个新的结构体HandlerMethod,mapping 和 HandlerMethod保存在AbstractHandlerMethodMapping的handlerMethods的map里面,然后mapping中的url和mapping又保存到另外一个map(urlMap)中。我们之前的预测是url和method保存在一个map,但是这里却拆成两步,首先是url和mapping,然后是根据mapping找出HandlerMethod,这又是为什么呢?我们可以看到这个mapping有一些成员变量,比如method,这个就是限制了什么请求方式可以访问这个url。加入我们在RequestrianMapping中有特别声明了只有post的请求才能方法某个方法,但是实际上请求的是get方式。这时候springmvc会根据这个url找出对应的mapping,再看看mapping中的method是否有get,如果没有就返回你错误,如果有就返回你正确的HandlerMethod。
实际上init当然不止初始化urlMap和handlerMethods那么简单,在此之前还做了很多操作。只不过这不在我考究范围。因为还没看spring,像初始化容器这些我现在也不是很懂。
-
service()
通过剖析init(),我们知道urlMap和handlerMethods两个数据结构,我们猜想service的作用就是先从urlMap找到对应的mapping,然后再检查mapping是否合理,如果合理再从handlerMethods中返回对应的HandlerMethod,再通过反射调用相关的方法。那我们就来看看吧。
我们把断点打到controller上,得到调用栈如下:
调用栈
通过调用栈我们可以看出,确实是先调用了service()方法,再经过一系列的调用来到了DispatcherServlet的doDispatch上,在这个方法中,springmvc先是得到一个handler(HandlerMethod),然后再根据handler里面的method反射调用我们的方法。那么springmvc是怎么得到我们的handler的呢?
doDispatch
从doDispatch中就可以看到,handler是调用这个方法来的,所以我们从这里作为入口,一步一步debug进去。
handler
可以看到,才两三步,就已经来到我们熟悉的AbstractHandlerMethodMapping中了,没错,刚刚我们剖析的urlMap和handlerMethods都是在这个类中,所以接下来就顺理成章了。在lookupHandlerMethod方法中:

- Spring MVC 还有很多东西没有去理解,得学完spring之后再来看看。理解了springMVC的init和service内容以后,我们就可以自己写一个springMVC低配版了。





