学写一个 Java Web MVC 框架
Posted sp42a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学写一个 Java Web MVC 框架相关的知识,希望对你有一定的参考价值。
当前我们介绍的是一个简单的MVC,用8个类即实现完整Spring MVC核心功能,外加其他实用的小功能。它是怎么实现的呢?让我们来一探究竟!
源码在:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-base/src/main/java/com/ajaxjs/web/mvc。
基本原理
首先声明AJAXJS MVC遵循JavaEE Servlet规范。为何要提这点?当今某些Web框架如Play Framework并没有遵循Servlet规范,它们觉得Servlet是一个旧包袱,倒不如不用。但本框架依然承认Servlet是Java Web开发的标准和规范,他还是较好地对HTTP进行了抽象,为MVC提供了基础的范式。AJAXJS MVC在Servlet之上做了浅层包装而做出来的,它做的事情简单说是:接收请求->封装参数->将请求交给开发者这编写的逻辑处理->返回处理结果。接收所有的请求作出合适分发的类是MvcDispatcher,它是一个标准Servlet Filter过滤器,接收了所有的HTTP请求然后分析URL,使得URL与合适的控制器Controller分发关联起来,从而完成对应的业务逻辑。从Servlet宏观角度来看它有且只有一个控制器,就是MvcDispatcher,MvcDispatcher是整个Servlet应用程序的控制器;但从微观来看,用户定义的各个控制器才是针对每个HTTP请求的控制器。
MVC框架中控制器是重点,理解好控制器的角色的话其余View和Model则非常好办。下面再逐点说说MVC处理流程的概要。
- 首先浏览器或其其他客户端发起一次请求到HTTP服务器(例如Tomcat),分配给前端控制器MvcDispatcher。
- MvcDispatcher根据URL路由规则,找到匹配的控制器及其方法,此过程是统一分发的过程。
- 得到执行具体的控制器方法其对象引用,先不执行。前期先解析方法对象的参数,将HTTP请求参数转化为适合方法执行的参数
- 对指定控制器方法的逻辑传入类型吻合的参数并执行。控制器方法可以调用相关业务方法,最后得到给客户端的响应内容。
- 一旦控制器执行完此,返回ModelAndView和视图给MvcDispatcher前端控制器。ModelAndView是视图所需要的数据模型。
- MvcDispatcher得到所需要渲染的视图,可以是html/JSP/JSON/XML或其他响应类型,最终将渲染结果返回给客户端。
上述六点勾勒出一个MVC请求到响应的概要过程,实际完善中还有许多功能点的,例如异常的处理、内置的过滤器等。如果我们希望疱丁解牛般深入一个MVC系统如何打造的话,还需要将实现的过程逐步分解,细化到每一个关键的方法函数阐述,把来龙去脉说清楚。
MvcDispatcher控制器
接着在WEB-INF/web.xml
配置文件中新建一个标准的过滤器(Filter,和SpringMVC不同的是,它采用Servlet监听URL路径而AJAXJS使用Filter过滤器)。和其它多数Web程序那样,此处的配置相当于应用的总体入口,定义了请求URL路径映射着某个Filter类。如下面第一个粗体部分,所定义的MvcDispatcher虽然是过滤器,但其真实角色是分发器或者转发器(Dispatcher)(我们下面会详细分析之)。该项是固定不能修改的。它统一接收了所有的请求(如下代码第二个粗体部分,配置为/*
的过滤器拦截了所有请求),再根据请求映射规则分发给相应的控制器进行处理。
<filter>
<description>MVC 过滤器</description>
<filter-name>MVC_GlobalFilter</filter-name>
**<filter-class>com.ajaxjs.mvc.controller.MvcDispatcher</filter-class>**
<init-param>
<description>指定搜索的包名,多个可以用 逗号 分开</description>
<param-name>controller</param-name>
<param-value>com.mysoft.app</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>MVC_GlobalFilter</filter-name>
**<url-pattern>/*</url-pattern>**
</filter-mapping>
此处开发者可修改的参数只有一个:controller
的值,表示要扫描的包名,且为必填的,它是在<init-param>
初始化节点内的<param-value>
元素。该参数说明了控制器类都保存在哪个包名下,或者说有哪些包是包含了控制器类,要让框架知道并收集起来处理。有同学问是否可以全局扫描,这虽然不是聪明的做法,但值得称赞的是起码这位同学脑海中已经存在有全局Classpath的概念。全局扫描就是不管什么Java类都要进行扫描一遍,那样的话可以连controller配置都不用写。但是缺点是明显的,就是效率太慢,包括JDK里面的类都要扫一遍,那样启动Tomcat都要等半天。
controller可以设置多个包,用逗号分隔开。搜索查找的过程是递归的,也就是说指定了一个包名那么其所有子包名都会被扫描,例如输入com.mysoft.app
,表示所有的控制器类必须在com.mysoft.app
包中,或者在com.mysoft.app.*
的任何一个子包中。
解析配置
Web服务器接受到请求,把请求对象Request
、响应对象Response
交给MvcDispatcher
处理。MvcDispatcher功能是将负责将请求分发,它可以认为是MVC前端控制器(不得不说MvcDispatcher是前端控制器(Front Controller Pattern)设计模式的实现。该模式有点像电话分机,用户先打总机号码,接听后话务员按照分机号分发到具体的部门,具体的人。前端控制器模式主要用于集中统一化对外的请求接口,便于更好的封装内部逻辑),所有的请求都要经过该控制器来进行统一的分发。所谓分发(Dispatch)过程就是请求的是什么路径,被派到(分发)到哪里,搞清楚具体执行的是哪个Controller实例/哪个方法之问题。当然,一个前提就是把这些相关的问题都要事先决定好。故所以在客户端请求之前须完成初始化工作。通常这工作在Servlet初始化的时候比较适合,也就是Tomcat这些Web容器启动的时候执行的。MVC使用Servlet侦听器拦截请求,所以具体的事机是ServletContextListener
的contextInitialized
事件。
另外一个问题是,MvcDispatcher充其量是个分发器,接着执行什么流程呢?我们说紧接着就是找到真正的控制器对象并执行,此时我们已经知道调用哪个控制器实例/哪个方法了。不过在此之前,分发器必须晓得控制器有哪些。我们通过扫描控制器来完成,满足扫描条件的有两个:一、设定一个范围扫描,也就是要求定义一个包名;二、必须为IController接口之实例。首先指定包名,在原web.xml
的基础上加入<init-param>
初始化参数的元素。
前面已经说过,在web.xml
定义了控制器的入口,名为MVC_GlobalFilter
的过滤器具体指向了类MvcDispatcher,如下代码所示。这与最简单的过滤器用法差不多。MvcDispatcher本身就是一个标准的过滤器,实现了Servlet Filter标准接口javax.servlet.Filter
。
<filter>
<description>MVC 过滤器</description>
<filter-name>MVC_GlobalFilter</filter-name>
<filter-class>com.ajaxjs.mvc.controller.MvcDispatcher</filter-class>
<init-param>
<description>依赖注射</description>
<param-name>doIoc</param-name>
<param-value>com.ajaxjs.cms, cn.nike</param-value>
</init-param>
<init-param>
<description>指定搜索的包名,多个可以用 逗号 分开</description>
<param-name>controller</param-name>
<param-value>com.ajaxjs.cms</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>MVC_GlobalFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
浏览器作为客户端向服务端发起一个HTTP请求,Servlet最先接受到请求的是Filter过滤器,然后是 HttpServlet,最后跳转视图呈现。对于控制器角色的选择,要么过滤器,要么HttpServlet 。这里MVC选择了过滤器,优先级比HttpServlet高,故有可能后面的HttpServlet并不会访问,因为MvcDispatcher充当控制器拦截了。Servlet Filter这里发挥了请求入口的作用。
我们看看MvcDispatcher类的声明,如插图 4.5所示,它实现了Servlet Filter标准接口javax.servlet.Filter
,故应要重写这个方法写出具体的实现 ,即init()
和 doFilter()
方法。
初始化阶段,MvcDispatcher会读取<init-param>/controller
的配置,令到凡是IController接口的那些控制器类都将收集起来“听候发落”。过滤器本身是个链式调用,到MvcDispatcher这里来的时候就是根据URL、参数、HTTP方法等等的不同分发来决定把这次请求分发到那个应该要得到请求的控制器中。这就是Dispatcher分发器的作用。如上例代码所示,我们定义了过滤器监听的URL路径<url-pattern>/*</url-pattern>
为全局的/*
,表示用户不论访问的什么URL都会访问到MvcDispatcher
类身上。实质上这里就是要接管所有的HTTP请求。如果符合控制器规则的,就执行处理,不符合的返回到标准Servlet过滤器中。
全局路径的Servlet和非Servlet的访问都要被MvcDispatcher接管控制,包括了*.jsp
甚至*.html/*.js/*.css/*.jpg
这些静态资源——听上去接管的权限有点过大,但实际上不必担心,MvcDispatcher已有对应的分支逻辑来应付,我们下文讨论到的时候才分析说。
以上是关于学写一个 Java Web MVC 框架的主要内容,如果未能解决你的问题,请参考以下文章