当前位置: 首页 > news >正文

Spring系列之手写一个SpringMVC

目录

  • Spring系列之IOC的原理及手动实现
  • Spring系列之DI的原理及手动实现
  • Spring系列之AOP的原理及手动实现
  • Spring系列之手写注解与配置文件的解析

引言

在前面的几个章节中我们已经简单的完成了一个简易版的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源码的过程中不至于完全就是一头雾水。因为技术水平的原因,文章中可能还存在着一些错误,欢迎大家指出。

相关文章:

  • SharePoint中List的大Version和小Version的区别
  • 20名香港大学生结束湖南广电实习 回味“湘遇”之旅
  • 已附件的形式发送测试报告
  • 我与51CTO博客园的第一次
  • PCA降维
  • PHP 全局变量
  • 朋友圈继续扩大!科蓝软件联合蚂蚁金服发布“移动金融逸平台”
  • MySQL面试题之如何优化一条有问题的SQL语句?
  • UOJ131 [NOI2015] 品酒大会
  • uoj#349. 【WC2018】即时战略(动态点分治)
  • 未来科技展亮相杭州 七大5G应用领域打开全新想象
  • 在Linux下创建文件,文件名中包含当前时间
  • Java源码解析 - ThreadPoolExecutor 线程池
  • 通过sys.objects查询SQL SERVER数据库改动内容
  • 共轭分布
  • 230. Kth Smallest Element in a BST
  • Consul Config 使用Git做版本控制的实现
  • Docker 1.12实践:Docker Service、Stack与分布式应用捆绑包
  • Java 多线程编程之:notify 和 wait 用法
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • Java小白进阶笔记(3)-初级面向对象
  • JS进阶 - JS 、JS-Web-API与DOM、BOM
  • Markdown 语法简单说明
  • Mithril.js 入门介绍
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • Python 反序列化安全问题(二)
  • Redis在Web项目中的应用与实践
  • RxJS 实现摩斯密码(Morse) 【内附脑图】
  • yii2中session跨域名的问题
  • Zsh 开发指南(第十四篇 文件读写)
  • 工作中总结前端开发流程--vue项目
  • 关于extract.autodesk.io的一些说明
  • 全栈开发——Linux
  • 如何编写一个可升级的智能合约
  • 硬币翻转问题,区间操作
  • 远离DoS攻击 Windows Server 2016发布DNS政策
  • C# - 为值类型重定义相等性
  • 基于django的视频点播网站开发-step3-注册登录功能 ...
  • ​Z时代时尚SUV新宠:起亚赛图斯值不值得年轻人买?
  • #每日一题合集#牛客JZ23-JZ33
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (8)STL算法之替换
  • (分享)自己整理的一些简单awk实用语句
  • (一) storm的集群安装与配置
  • (一)appium-desktop定位元素原理
  • (转)eclipse内存溢出设置 -Xms212m -Xmx804m -XX:PermSize=250M -XX:MaxPermSize=356m
  • (转)拼包函数及网络封包的异常处理(含代码)
  • .bat批处理(五):遍历指定目录下资源文件并更新
  • .NET 依赖注入和配置系统
  • .NET/MSBuild 中的发布路径在哪里呢?如何在扩展编译的时候修改发布路径中的文件呢?
  • .net2005怎么读string形的xml,不是xml文件。
  • .NET3.5下用Lambda简化跨线程访问窗体控件,避免繁复的delegate,Invoke(转)
  • .NET面试题(二)
  • .NET设计模式(2):单件模式(Singleton Pattern)
  • .Net中ListT 泛型转成DataTable、DataSet