从源码出发,理解SpringMVC(1)

写在最前

一边读源码,一边写理解。连续写了一周,终于大概搞定了。
之前想一次性就搞定的,后来发现自己想的太简单了。所以准备之后再写写HandlerMapping、handlerAdapter以及ViewResolver。
写的过程有些痛苦,写完收获很大,推荐将SpringMVC作为源码阅读的起点。

一、认识SpringMVC

  1. MVC模式是什么?
    MVC模式是一种架构模式,用于将代码在数据、逻辑、界面层级分离。

M-Model 模型(完成业务逻辑:有javaBean构成,service+dao+entity)
V-View 视图(做界面的展示 jsp,html……)
C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面)

  1. SpringMVC是什么?
    SpringMVC是一个MVC模式的WEB框架。
    更确切的说,SpringMVC是Spring框架的一个模块,类似IOC,AOP这样的模块,所以经常会听到SpringMVC可以和Spring无缝集成,其实Springmvc就是Spring的子模块,所以根本不需要进行整合。

  2. 为什么要使用SpringMVC?
    至于为什么要使用SpringMVC,上面已经说了SpringMVC就是Spring的子模块,所以这个问题一部分可以转换成为什么要使用Spring。除去为什么要使用Spring这个老问题,那么就是SpringMVC不存在和Spring整合的问题,与Struts2相比安全性高,上手快速。

二、SpringMVC的使用

  1. 我们要使用SpringMVC,要先将DispatcherServlet注册到容器中。那么为什么要这么做呢?
    从下面类关系图上可以看到,DispatcherServlet继承了HttpServlet,所以DispatcherServlet本质是一个Servlet。想要使用servlet,就需要将Servlet注册到Servlet容器(如Tomcat)中。
springmvc继承关系.png
  1. Servlet注册到Servlet容器
  • 比较常用的做法是在web.xml中配置<servelt>,拦截所有请求到DispatcherServlet去处理。
<web-app>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app</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>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>
  • 有些项目中,是没有web.xml的。是因为在Servlet 3.0以后,可以通过实现WebApplicationInitializer接口的方式将DispatcherServlet注册到容器中。
public class MyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(javax.servlet.ServletContext servletContext) throws ServletException {
        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
        ac.refresh();

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/");
    }
}

@Configuration
@ComponentScan("controller")
public class AppConfig {
}
  1. 使用SpringMVC,除了将DispatcherServlet注册到容器,还需要实现Controller控制器。
  • 实现Controller接口,实现handleRequest方法。
import org.springframework.web.servlet.mvc.Controller;

@Component("/test2")
public class BeanNameController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
            throws Exception {
        System.out.println("this is a BeanNameController");
        return null;
    }
}
  • 使用@Controller注解,配合@RequestMapping注解实现Controller
@Controller
public class RequesetMappingController {
   @RequestMapping("test1")
   public String testRequesetMappingController(){
       return "this is a RequesetMappingController";
   }
}

三、SpringMVC的流程

流程图(百度借的)
  1. 首先来看SpringMVC流程图,对SpringMVC流程有个大概的理解**
    1.1 用户发送一个http请求,被前端控制器接收(DispatcherServlet)。
    1.2 前端控制器对请求做一个分发与封装,通过处理器映射器(HandleMapping)查找url对应的handler并将handler返回给前端控制器。这时候已经找到了handler,但是并没有执行。因为handler中只保存了Controller的类或者方法路径。
    1.3 前端控制器将包含Controller路径的Handler传递给处理器适配器(HandlerAdapter),由处理器适配器执行具体的业务后,将结果ModelAndView返回给前端控制器。
    1.4 前端控制器得到ModelAndView,通过视图名称查找对应的视图,将视图渲染。
    1.5 响应用户请求。

  2. SpringMVC为什么要这么设计,而不是直接由url找到对应的业务代码?
    所以先简单的解释一下,实现Controller控制器的方式有两种,可以通过注解,也可以通过实现接口。通过@Controller注解的方式,每一个类中可以存在多个映射的方法。通过实现接口的方式,每一个类中只存在一个映射的方法。因为两种方式迥异,为了查找方便,SpringMVC在执行初始化时,便加载所有Controller,将url和含有Controller对象或方法路径的Handler分别放入两个不同HandlerMapping中,以key和value的形式存储,保证了url和Handler的一一对应关系。当接受到url请求时,只需要遍历handlerMappings下的两个HandleMapping,就可以找到对应的handler。
    handlerMappings,它的作用就是包含上面提到的两个不同的HandlerMapping,分别是BeanNameUrlHandlerMappingRequestMappingHandlerMapping,值得注意的是,这两个HandlerMapping是读取配置文件,通过反射加载的两个对象。从框架层面上讲,这样设计的好处是,当框架业务扩展,增添其他方式注册Controller,也可以直接在配置文件中添加handlerMappings(handlerAdapter同理),而不需要改变原有框架代码。

四. 解读源码

DispatcherServlet的本质是servlet。所以就必然存在 init()、doGet()、doPost()或者service()方法。

  1. 初始化init()

1.1 HttpServletBean.init()
DispatcherServlet 的 init() 方法在其父类 HttpServletBean 中实现的,它覆盖了 GenericServlet 的 init() 方法,主要作用是加载 web.xml 中 DispatcherServlet 的 配置,并调用子类的初始化。

public final void init() throws ServletException {
    PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
            this.initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        } catch (BeansException var4) {
            ...
        }
    }
    // 让子类实现的方法,这种在父类定义在子类实现的方式叫做模版方法模式
    this.initServletBean();
}

ServletConfigPropertyValues :是静态内部类,使用 ServletConfig 获取 web.xml 中配置的参数

1.2 FrameworkServlet.initServletBean()
在 HttpServletBean 的 init() 方法中调用了 initServletBean() 这个方法,它是在 FrameworkServlet 类中实现的,主要作用是建立 WebApplicationContext 容器(有时也称上下文),加载 SpringMVC 配置文件中定义的 Bean 到该容器中,最后将该容器添加到 ServletContext。

@Override
protected final void initServletBean() throws ServletException {
     try {
            this.webApplicationContext = this.initWebApplicationContext();
            this.initFrameworkServlet();
        } catch (RuntimeException | ServletException var5) {}
}

SpringMVC 容器的父容器是Spring容器,也就是说,SpringMVC可以使用Spring容器中的Bean,但是Spring不可以调用SpringMVC 容器中的Bean。

protected WebApplicationContext initWebApplicationContext() {
    // 获取 ContextLoaderListener 初始化并注册在 ServletContext 中的根容器,即 Spring 的容器
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
    WebApplicationContext wac = null;
    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    // 将 Spring 的容器设为 SpringMVC 容器的父容器
                    cwac.setParent(rootContext);
                }
                //初始化SpringMVC容器
                this.configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // 如果 WebApplicationContext 为空,则进行查找,能找到说明上下文已经在别处初始化。
        wac = this.findWebApplicationContext();
    }
    if (wac == null) {
        // 如果 WebApplicationContext 仍为空,则以 Spring 的容器为父上下文建立一个新的。
        wac = this.createWebApplicationContext(rootContext);
    }
    if (!this.refreshEventReceived) {
        // 模版方法,由 DispatcherServlet 实现
        this.onRefresh(wac);
    }
    if (this.publishContext) {
        // 发布这个 WebApplicationContext 容器到 ServletContext 中
        String attrName = this.getServletContextAttributeName();
        this.getServletContext().setAttribute(attrName, wac);
    }
    return wac;
}

1.3 DispatcherServlet.onRefresh() 方法
建立好 WebApplicationContext(上下文) 后,通过 onRefresh(ApplicationContext context) 方法回调,进入 DispatcherServlet 类中。onRefresh() 方法中调用this.initStrategies(),提供 SpringMVC 的初始化。HandlerMappings,HandlerAdapters,ViewResolvers都是此时初始化的。

  protected void initStrategies(ApplicationContext context) {
        this.initMultipartResolver(context);
        this.initLocaleResolver(context);
        this.initThemeResolver(context);
        this.initHandlerMappings(context);
        this.initHandlerAdapters(context);
        this.initHandlerExceptionResolvers(context);
        this.initRequestToViewNameTranslator(context);
        this.initViewResolvers(context);
        this.initFlashMapManager(context);
    }

1.4 DispatcherServlet.initHandlerMappings()
这个方法从SpringMVC 的容器及 Spring 的容器中查找所有的 HandlerMapping 实例,并把它们放入到 handlerMappings 这个 list 中。

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
    if (this.detectAllHandlerMappings) {
        Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList(matchingBeans.values());
            // 按一定顺序放置 HandlerMapping 对象
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    } else {
        try {
            HandlerMapping hm = (HandlerMapping)context.getBean("handlerMapping", HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        } catch (NoSuchBeanDefinitionException var3) {
            ;
        }
    }

    if (this.handlerMappings == null) {
        this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class);
    }

}

DispatcherServlet.initHandlerAdapters()DispatcherServlet.initHandlerAdapters()相似

  1. 处理请求

2.1 doGet()、doPost()
HttpServlet 提供了 doGet()、doPost() 等方法,DispatcherServlet 中这些方法是在其父类 FrameworkServlet 中实现的,代码如下:

@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);
}

2.2 processRequest() 方法

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;
    // 返回与当前线程相关联的 LocaleContext
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    // 根据请求构建 LocaleContext,公开请求的语言环境为当前语言环境
    LocaleContext localeContext = buildLocaleContext(request);
    
    // 返回当前绑定到线程的 RequestAttributes
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    // 根据请求构建ServletRequestAttributes
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
    
//WebAsyncManager:用来管理异步请求的处理。什么时候要用到异步处理呢?就是业务逻辑复杂(或者其他原因),为了避免请求线程阻塞,需要委托给另一个线程的时候。
    // 获取当前请求的 WebAsyncManager,如果没有找到则创建
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);    
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    // 使 LocaleContext 和 requestAttributes 关联
    initContextHolders(request, localeContext, requestAttributes);

    try {
        // 由 DispatcherServlet 实现
        doService(request, response);
    } catch (ServletException ex) {
    } catch (IOException ex) {
    } catch (Throwable ex) {
    } finally {
        // 重置 LocaleContext 和 requestAttributes,解除关联
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }// 发布 ServletRequestHandlerEvent 事件
        publishRequestHandledEvent(request, startTime, failureCause);
    }
}

2.3 核心部分 doDispatch()
DispatcherServlet 的 doService() 方法主要是设置一些 request 属性,并调用 doDispatch() 方法进行请求分发处理,doDispatch() 方法的主要过程是通过 HandlerMapping 获取 Handler,再找到用于执行它的 HandlerAdapter,执行 Handler 后得到 ModelAndView ,ModelAndView 是连接“业务逻辑层”与“视图展示层”的桥梁,接下来就要通过 ModelAndView 获得 View,再通过它的 Model 对 View 进行渲染。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;  //processedRequest:加工过的请求
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;     //是否为上传上传
    // 获取当前请求的WebAsyncManager,如果没找到则创建并与请求关联
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    try {
        ModelAndView mv = null;
        Exception dispatchException = null;
        try {
            // 检查是否有 Multipart,有则将请求转换为 Multipart 请求
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);
            // 遍历所有的 HandlerMapping 找到与请求对应的 Handler,并将其与一堆拦截器封装到 HandlerExecution 对象中。
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null || mappedHandler.getHandler() == null) {
                noHandlerFound(processedRequest, response);
                return;
            }
            // 遍历所有的 HandlerAdapter,找到可以处理该 Handler 的 HandlerAdapter
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
            // 处理 last-modified 请求头 
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }
            // 遍历拦截器,执行它们的 preHandle() 方法
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }
            try {
                // 执行实际的处理程序
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            } finally {
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
            }
            applyDefaultViewName(request, mv);
            // 遍历拦截器,执行它们的 postHandle() 方法
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        } catch (Exception ex) {
            dispatchException = ex;
        }
        // 处理执行结果,是一个 ModelAndView 或 Exception,然后进行渲染
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    } catch (Exception ex) {
    } catch (Error err) {
    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // 遍历拦截器,执行它们的 afterCompletion() 方法  
            mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            return;
        }
        // Clean up any resources used by a multipart request.
        if (multipartRequestParsed) {
            cleanupMultipart(processedRequest);
        }
    }
} 

2.4 执行实际的处理程序调用了handle方法
当Controller是实现接口的方式时,这时候可以直接执行handleRequest()方法。是使用注解的方式时,这个时候就通过反射调用相应Controller中的方法。

@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return ((Controller)handler).handleRequest(request, response);
}

五、反射简单概括

为什么要使用反射,除了降低解耦性以外,其实没有其他方式去调用到其他Controller中的方法的。这里没有对象可以注入,也没有办法去直接调用其他Controller,所以这里只能使用反射的方式。
通过反射得到形参和实参后,在request.getParameterValues()中查找paremeter为参数名的valus[],进行赋值。
有一点需要注意的是,参数名字的获取。在SpringMVC框架中,使用的技术是ASM javaasisst反编译字节码

在JDK1.8之前,直接拿参数名字是拿不到的,使用反射拿到的参数名,得到的是‘arg0’,’arg1’...
JDK1.8开始,允许拿到参数名称,但也需要一些配置,这里不做过多解释

六、SpringMVC中使用的设计模式

  1. 模板方法模式
    框架中使用了很多的模板方法模式,比如DispatcherServlet本质是Servlet,具有init(),service(),destroy()等方法,tomcat运行时,会加载web.xml将中的servlet,servlet调用init()方法。init()中有很多工作需要准备,如初始化web容器,导入配置,初始化HandlerMappings、HandlerAdapters等等。
    这些工作不是全部都在DispatcherServlet中完成,有一部分工作其是在其父类FrameworkServlet中完成的。当DispatcherServlet初始化,会调用HttpServletBean中的init(),然后执行FrameworkServlet中的initServletBean()完成初始化。

  2. 迭代器模式
    集合对象获取迭代器,通过迭代器本身的遍历方法获取数据,而不用关心遍历的具体实现方式。
    Iterator var2 = this.handlerMappings.iterator();

  3. 适配器模式
    Controller的实现方法有多种,如果直接将url和Controller进行映射,那么我们就要区Map中Controller对象,然后分别对Controller进行处理。另外一方面,当需要扩展Controller的实现方式,就需要对原有代码更改,违反开闭原则。
    使用适配器模式,将多种Controller用Handler进行适配(不要和装饰者模式搞混,装饰者模式是对原有功能进行增强),使不同种类的对象可以在一起工作。

  4. 关于设计模式的补充
    框架中应用了很多设计模式,很多模式模式一眼看上去并不太和书上将设计模式的例子完全吻合。但仔细琢磨,思想却是一样的。也证明了设计模式是一种思想吧。

七、写在最后

在MVC模式中,Model层实体类有很多称呼,如:BO/PO/POJO/VO/entity 等等。
这些称呼的出现和设计理念有关,比如把一个对象用于写入数据库,那么就可以称之PO(persistent object);用作处理业务,就可以称之为BO(business object),如果想把一个业务对象写入数据库,根据设计理念,就要先把BO转换PO。
在Spring框架中也一样,不是所有的设计都是最合适的,也有一些设计是因为追寻当时的潮流理念而使用,阅读时要注意。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容