目录

引言

在前面的几个章节中我们已经简单的完成了一个简易版的spring,已经包括容器,依赖注入,AOP和配置文件解析等功能。这一节我们来实现一个自己的springMvc。

关于MVC/SpringMVC

springMvc是一个基于mvc模式的web框架,SpringMVC框架是一种提供了MVC(模型 - 视图 - 控制器)架构和用于开发灵活和松散耦合的Web应用程序的组件。

MVC模式使得应用程序的不同部分分离,同时提供这些元素之间的松散耦合。

  • 模型(Model)封装了应用程序数据,通常是指普通的bean。
  • 视图(View)负责渲染模型数据,一般来说它生成客户端浏览器可以解释HTML输出。
  • 控制器(Controller)负责处理用户请求并获取请求结果,将其传递给视图进行渲染。

SpringMVC

SpringMVC处理请求流程

首先我们来了解一下SpringMVC在处理http请求的整个流程中都在做些什么事。

//图片来源于网络

从上图中我们可以总结springmvc的处理流程:

  1. 客户端发送请求,被web容器(tomcat等)拦截到,web容器将请求交给DispatcherServlet。
  2. DispatcherServlet收到请求后将请求交给HandlerMapping(处理器映射器)查找该请求对应的Handler(处理器)。实际上这个过程在图上是分为两个的,这是因为一个请求url可能会有多个请求处理器,比如GET请求,POST请求等就是不同的处理器对象来处理的,所以需要一个HandlerAdapter(处理器适配器)来根据不同的请求参数来获取对应的处理器对象。
  3. 获取到请求的处理器对象后,执行处理器的请求处理流程。这里的处理流程一般是值我们在开发中定义的业务流程。
  4. 处理流程执行完毕后将返回的结果包装为一个ModelAndView对象返回给DispatcherServlet。
  5. DispatcherServlet通过ViewResolver(视图解析器)将ModelAndView解析为View。
  6. 通过View渲染页面,响应给用户。

上面就是一个http请求从开始带完成响应中由SpringMVC完成的流程,我们的SpringMVC没有实际的那么复杂,不过相应的功能都会进行实现。

SpringMVC分析

我们知道SpringMVC是实现了MVC模式的一个web框架,所以肯定有ModelViewController三种角色。从上图中我们还可以看到一个十分重要的类:DispatcherServlet。接下来我们单独来分析。

Controller

控制器(Controller)负责处理用户请求并获取请求结果,将其传递给视图进行渲染。

在SpringMVC中Controller负责来处理由DispatcherServlet分发来的请求,并将进过业务处理后的请求结果包装成一个Model提供给View使用。在SpringMVC中将请求映射到对应的Controller上是给我们提供了两种不同的方法:

  1. 实例级别的映射,每一个请求都有一个对应的类实例来处理,类似于Struts2。这种方法实际很少使用。要实现这种类型需要实现一个Controller接口。
  2. 方法级别的映射,请求映射到bean的方法上,这样每一个Controller可以对应多个请求,同时也能更易于保证并发请求的线程安全。
实例级别的映射

考虑如何实现实例级别的映射?

在实例级别映射中每一个请求对应一个不同的类,即一个URL<==>一个Class,这样我们可以将beanName和请求地址进行对应。

同时我们框架需要提供一个请求处理的入口供使用者实现业务代码。这里我们定义一个Controller接口,接口中提供一个处理方法。

接口中包含一个handlerRequest的处理方法,所有实例级别的Controller都需要Controller接口。在handlerRequest方法中完成业务逻辑。因为目前我们还无法判断业务逻辑完成后需要返回那种类型的值,所以用Object代替。

这里要确定方法应该返回什么,我们首先得明白返回值用来干嘛的?这个返回的值包含了业务处理的结果。并且返回给页面用于页面渲染。所以肯定是持有一个结果值和需要返回的具体的页面。考虑到返回的值不仅仅是业务处理的结果,可能用户需要设置一些其他的值给页面,我们定义一个map类型来接收。

添加hasView()方法是因为我们返回的并不是必须要有页面的信息,比如返回json值。

view的工作非常简单,就是讲我们返回的值响应给浏览器。所以view的接口是这样的:

方法级别的映射

用过SpringMVC的都很清楚,上面那种实例级别映射的方式基本上都不会使用。实例级别的映射一旦项目中的请求多了将会导致项目中的类特别的多。我们平时用的多的是方法级别的映射。

我们这里方法级别的映射不需要实现Controller接口,这里我们仿造SpringMVC使用@Controller来表示一个控制器,使用@RequestMapping来表示不同的请求。

RequestMethod是指http请求类型,包括GEt,POST,PUT等类型,一个枚举类型。

方法映射的处理除了映射到方法上外其他和实例映射类似。

请求分发

客户端发送请求,后端接收到请求后,需要将请求分发到对应的处理器上,从宏观角度说就是请求交给DispatcherServlet,然后由DispatcherServlet分发给不同的处理器。我们很明显需要知道请求是如何分发到处理器上的。

不同类型的映射对应的请求处理器肯定是不一样的,比如对于实例映射是通过实现Controller接口,处理也是关于接口的,而方法映射是通过注解实现。即不同的方式,映射方式不同,请求处理器也不一样。

简单的方法就是分别定义处理不同类型的处理器,然后在DispatcherServlet中通过判断确定具体的处理器。这样处理思路很简单,但是问题在于假设我们要再添加一种处理的方式,那么就需要改变原有的代码,很明显的违反了开闭原则,也会给代码维护带来麻烦。所以我们希望有一种DispatcherServlet能避开这种改变的方式。

我们这里需要一个能够根据传递进来的不同的请求来调用不同的处理器,而且能够简单的进行扩展而不改动原代码。这里肯定就需要用到设计模式了,那么使用什么设计模式呢? 策略模式

HandlerMapping

我们定义一个用于请求处理器映射的接口,该接口的作用就是获取一个请求具体的请求处理器,不同方式的处理方式分别实现该接口。

BeanNameUrlHandlerMapping就是实例映射的处理器映射器,RequestMappingHandlerMapping就是方法映射的处理器。而如何将请求和HandlerMapping对应起来呢?我们能想到的就是url了,这里我们定义一个urlMaps用来存储url和处理器映射器的对应关系。

HandlerAdapter

现在我们有了HandlerMapping后就可以获取到某一种类型的处理方式的处理对象。但是实际上我们还是没有获取到实际的处理器,所以我们还需要根据请求来获取到实际的处理器,这里我们定义一个HandlerAdapter来获取实际的处理器。

handler(...)就是具体的处理方法,实际上就是执行控制器,而support主要是用于判断是否是一个处理器对象。

这里很明显对于实例映射来说我们只需要执行方法中的handlerRequest(...)方法即可,但是对于方法映射就不是那么简单了,不同的方法根据@RequestMapping表示不同的请求。所以我们还需要一个类来表示不同的方法信息,便于请求传递过来后直接取用。

类的定义中classRequestMapping表示作用在类上的@RequestMappingmethodRequestMapping表示作用在方法上的@RequestMappingmethod表示作用在类上的方法信息。match(...)方法是用来检测当前请求与这个RequestMappingInfo是否相匹配。

扫描注册

基本上我们的准备工作完成了,现在我们需要考虑如何来识别我们的控制器和生成RequestMappingInfo的信息。

首先创建的时机肯定是在项目启动的时候就讲这些信息初始化好,因为请求过来后会立刻使用到这些信息。而初始化这些信息的行为在哪里发生呢?我的第一反应是交给DispatcherServlet,因为直观来讲是它来使用,实际上真正的使用这些信息的事HandlerMapping的实现类,通过请求和RequestMappingInfo等信息来获取实际的处理器,所以初始化的信息应该交给HandlerMapping的实现类。

我们要明白的事提取带有Controller注解的bean或者是实现类Controller接口的类肯定是在bean初始化之后进行的,所以我们需要提供一个在初始化后获取控制器类型的接口。同时获取已经初始化好的类那么肯定会使用到ApplicationContext。我们现在对RequestMapping接口修改下。

afterPropertiesSet()方法中获取控制器类型的bean。在我们之前完成的代码中只提供了通过beanName获取bean的方法,所以这里我们还需要提供一种获取所有的执行类型的方法。

public void afterPropertiesSet() {
String[] beanNameForType = applicationContext.getBeanNameForType(Object.class);
for(String beanName:beanNameForType){
Class type = applicationContext.getType(beanName);
//判断是否是控制器类型
if (isHandler(type)) {
//注册控制器的类型
detectHandlerMethod(type);
}
}
}

DispatcherServlet

好了,到现在对于控制器的准备已经差不多了,现在我们需要来实现DispatcherServlet了。

DispatcherServlet名字来看就知道这是一个Servlet,我们的框架是基于Servlet来完成的,SpringMVC框架本身也是基于Servlet的。当然也可以根据其他技术来实现,比如基于Filter的Struts2。

我们先来捋一下DispatcherServlet需要完成的任务吧:

  1. 创建ApplicationContext容器对象
  2. 从容器中获取HandlerMappingHandlerAdapter对象。
  3. 分发请求
  4. view转发

熟悉Servlet的都应该知道Servlet提供了一系列生命周期的API,上面的这些事情都需要在Servlet生命周期的不同阶段来完成。

  • 容器对象的初始化和获取HandlerMappingHandlerAdapter对象在init(...)完成。
  • 请求分发由service(HttpServletRequest req, HttpServletResponse res)完成。
  • destroy()完成关闭后的处理。

View

在之前我们定义控制器的时候有说到由控制器来返回一个ModelAndView对象,该对象确定具体返回哪一个页面和处理结果的数据。这样不仅需要提供ModelAndView对象,同时还需要提供一个View的对象,现在我们希望这个过程能够尽量的简单,使用者可以仅仅提供一个视图的名称,然后框架就可以自动的找到对应的页面然后进行渲染。

我们现在需要重新定义ModelAndViewView类。

这样用户可以传递一个名称过来,然后由HandlerAdapter根据传递的handler来生成ModelAndView,同时也可以自定义ModelAndView对象。

ViewResolver

当我们前面的准备工作都做好了并不代表就已经可以完成了,因为对于不同的视图可能会有不同的操作,比如直接转发给一个URL,可能还会重定向到另一个URL,或者直接就是返回json串的。所以我们还需要定义不同的视图解析器来将ModelAndView解析成相应的View。

这里定义了一个解析JSP视图的解析器,同理也可以定义其他的处理器。

定义好了视图解析器后我们还需要定义几个用于处理不同情况的视图类。

这里的View类型还可以根据不同的需求添加其他类型的处理器,比如freemarker、JSTL等。对于json处理我们还需要像SpringMVC那样来定义一个@ResponseBody的注解。当使用了该注解的时候我们就将返回值转换为JSON串然后直接通过response返回给客户端即可。

小结

SpringMVC到这里就基本结束了,总得来说这一篇的内容稍微比较麻烦。主要是涉及到的内容较多,再加上这段时间比较忙,平时就下班后抽时间整理,目前也只是将思路基本捋完。代码也只是整理了一个框架,内容还没有进行填充。所以文章中可能会有一些错误的地方,大家如果发现了可以指出来。后面有时间会将代码实现的。代码都在这里:Spring

总结

Spring的手写框架差不多就是这些了,写这一系列文章是为了巩固我的Spring学习的成果,当然如果能帮助大家学习Spring当然是更好了。Spring的内容十分的繁杂,涉及的内容多,这一系列文章只能帮助大家对Spring的原理有一个最初的了解,在看Spring源码的过程中不至于完全就是一头雾水。因为技术水平的原因,文章中可能还存在着一些错误,欢迎大家指出。

Spring系列之手写一个SpringMVC的更多相关文章

  1. 看年薪50W的架构师如何手写一个SpringMVC框架

    前言 做 Java Web 开发的你,一定听说过SpringMVC的大名,作为现在运用最广泛的Java框架,它到目前为止依然保持着强大的活力和广泛的用户群. 本文介绍如何用eclipse一步一步搭建S ...

  2. 自己手写一个SpringMVC 框架

    一.了解SpringMVC运行流程及九大组件 1.SpringMVC 的运行流程   · 用户发送请求至前端控制器DispatcherServlet · DispatcherServlet收到请求调用 ...

  3. Spring系列之手写注解与配置文件的解析

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 Spring系列之AOP的原理及手动实现 引入 在前面我们已经完成了IOC,DI,AOP的实现,基本的功能都已经 ...

  4. 【Spring系列】- 手写模拟Spring框架

    简单模拟Spring 生命不息,写作不止 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 一个有梦有戏的人 @怒放吧德德 分享学习心得,欢迎指正,大家一起学习成长! 前言 上次已经学习了 ...

  5. 【Spring系列】自己手写一个 SpringMVC 框架

    参考文章 一.了解SpringMVC运行流程及九大组件 1.SpringMVC的运行流程 1)用户发送请求至前端控制器DispatcherServlet 2)DispatcherServlet收到请求 ...

  6. 自己手写一个SpringMVC框架

    前端框架很多,但没有一个框架称霸,后端框架现在Spring已经完成大一统.所以学习Spring是Java程序员的必修课. Spring框架对于Java后端程序员来说再熟悉不过了,以前只知道它用的反射实 ...

  7. 手写一个SpringMVC框架(转)

    一:梳理SpringMVC的设计思路 本文只实现自己的@Controller.@RequestMapping.@RequestParam注解起作用,其余SpringMVC功能读者可以尝试自己实现. 1 ...

  8. 摊牌了!我要手写一个“Spring Boot”

    目前的话,已经把 Spring MVC 相关常用的注解比如@GetMapping .@PostMapping .@PathVariable 写完了.我也已经将项目开源出来了,地址:https://gi ...

  9. 手把手教你手写一个最简单的 Spring Boot Starter

    欢迎关注微信公众号:「Java之言」技术文章持续更新,请持续关注...... 第一时间学习最新技术文章 领取最新技术学习资料视频 最新互联网资讯和面试经验 何为 Starter ? 想必大家都使用过 ...

随机推荐

  1. 六、OpenStack—neutron组件介绍与安装

    一.neutron介绍 Neutron 概述:传统的网络管理方式很大程度上依赖于管理员手工配置和维护各种网络硬件设备:而云环境下的网络已经变得非常复杂,特别是在多租户场景里,用户随时都可能需要创建.修 ...

  2. Golang的模块管理Module

    Golang 1.11版本终于支持了官方的模块依赖管理功能,1.11以前想要实现依赖管理只能够通过借助第三方库来实现,1.11以前的版本Golang项目必须依赖以GOPATH,从当前版本开始Golan ...

  3. 2.SSM整合_多表_一对一或多对一的增删改查

    一对一和多对一配置一样,这里就放到一起. 1.配置文件跟上一章一样,这里就不多写了,主要是Mapper映射文件 多 接口 public interface NewsMapper { public vo ...

  4. java实现注册的短信验证码

    最近在做只能净化器的后台用户管理系统,需要使用手机号进行注册,找了许久才大致了解了手机验证码实现流程,今天在此和大家分享一下. 我们使用的是榛子云短信平台, 官网地址:http://smsow.zhe ...

  5. js-day01-js语言基础

    JavaScript简介:JavaScript一种直译式脚本语言,是一种动态类型.弱类型.基于原型的语言,内置支持类型.它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本 ...

  6. python—文件处理

    一.文件处理流程 1.打开文件,得到文件句柄并赋值 2.通过句柄对文件进行操作 3.关闭文件 二.文件打开模式 1.r,只读,默认模式 2.w,只写 3.a,追加 4. r+.w+.x+.a+ ,可读 ...

  7. [lua][openresty]代码覆盖率检测的解决方式

    废话在前 什么是代码覆盖率 来自百度百科 代码覆盖(Code coverage)是软件测试中的一种度量,描述程式中源代码被测试的比例和程度,所得比例称为代码覆盖率. 开发人员为何关注? 在我们的开发过 ...

  8. 连接池c3p0 ,Proxool ,Druid ,Tomcat Jdbc Pool对比测试

    这次所要做的测试是比较几种我们常用的数据库连接池的性能,他们分别是:c3p0 ,Proxool ,Druid ,Tomcat Jdbc Pool这四种,测试将采用统一的参数配置力求比较“公平”的体现统 ...

  9. js查重去重性能优化心得

    概述 今天产品反映有个5000条数据的页面的保存按钮很慢,查看代码看到是因为点击保存按钮之后,进行了查重操作,而查重操作是用2个for循环完成了,时间复杂度是O(n^2).没办法,只能想办法优化一下了 ...

  10. [Swift]LeetCode231. 2的幂 | Power of Two

    Given an integer, write a function to determine if it is a power of two. Credits:Special thanks to @ ...