Spring系列之手写一个SpringMVC

目录

引言

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

关于MVC/SpringMVC

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

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

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

SpringMVC

SpringMVC处理请求流程

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

Spring系列之手写一个SpringMVC

//图片来源于网络

从上图中我们可以总结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接口,接口中提供一个处理方法。

Spring系列之手写一个SpringMVC

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

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

Spring系列之手写一个SpringMVC

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

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

Spring系列之手写一个SpringMVC

方法级别的映射

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

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

Spring系列之手写一个SpringMVC

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

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

请求分发

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

Spring系列之手写一个SpringMVC

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

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

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

HandlerMapping

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

Spring系列之手写一个SpringMVC

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

HandlerAdapter

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

Spring系列之手写一个SpringMVC

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

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

Spring系列之手写一个SpringMVC

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

扫描注册

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

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

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

Spring系列之手写一个SpringMVC

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()完成关闭后的处理。

Spring系列之手写一个SpringMVC

View

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

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

Spring系列之手写一个SpringMVC

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

ViewResolver

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

Spring系列之手写一个SpringMVC

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

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

Spring系列之手写一个SpringMVC

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

小结

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

总结

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

上一篇:lua模块demo(redis,http,mysql,cjson,本地缓存)


下一篇:Java8简单的本地缓存实现