Spring MVC学习

  • 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,并显示在页面上

  1. 先看看总目录:


    image.png
  2. 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>
  1. 配置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)。

  1. 配置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>
  1. 编写Controller
@Controller
public class HelloController {

    @RequestMapping("/hello")
    public String hello(HttpServletRequest request, Model model){
        model.addAttribute("ip", request.getRemoteAddr());
        return "hello";
    }
    
}
  1. 编写 jsp文件(要和dispatcher-servlet中配置的位置相同)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title></title>
</head>
<body>
你的ip:${ip}
</body>
</html>
  1. 启动程序,输入访问地址


    成功
  • spring mvc结构图(基于4.0.2)

spring mvc 结构图
  • SpringMVC理解

DispatcherServlet

我们说过Spring MVC其实就是一个servlet,那我们就先来看看这个servlet。

  1. 结构


    DispatcherServlet

    由图可知,DispatcherServlet(继承)FrameworkServlet(继承)FrameworkServlet(继承)HttpServlet(继承)GenericServlet(实现)Servlet。

  2. 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这个方法:
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,像初始化容器这些我现在也不是很懂。

  1. 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方法中:


  1. Spring MVC 还有很多东西没有去理解,得学完spring之后再来看看。理解了springMVC的init和service内容以后,我们就可以自己写一个springMVC低配版了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容