学写一个 Java Web MVC 框架
Posted sp42a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学写一个 Java Web MVC 框架相关的知识,希望对你有一定的参考价值。
解析控制器
之所以要解析控制器,是因为Controller类定义了各种URL映射的规则。在初始化阶段我们要获取这套规则具体如何,以便每次请求访问过来的时候可以执行具体的控制器方法。从某个角度讲这些控制器类相当于一个个配置文件,配置规则利用Java注解来参与完成。很多场合下注解发挥了配置的作用。Controller注解可以定义在类身上,可以定义在方法身上,开发者先定义Controller类和方法,然后按照规则需求分配不同的注解到具体的类或者方法身上,从而完成URL与控制器之间的映射关系,这正是MVC的所要完成的目标:当用户访问一个URL,最终对应的是一个Java方法,用户获得什么信息取决于该方法返回什么,那自然是一套规则,视乎Controller API能够提供什么能力给我们去设置这套规则。
如下是一个典型的Controller。
@Path("/MyTopPath_And_SubPath")
public class SubPathController implements IController {
@GET
public String showhtml() {
return "html::Hello World!";
}
@GET
@Path("subPath")
public String showHi(HttpServletRequest request, HttpServletResponse response) {
return "html::Hello " + request.getParameter("name");
}
}
Controller目标已经明确,就是映射URL与Controller的关系。为此必须使用一种数据结构来存储这种关系。到底应该如何设计合适的数据结构来描述映射关系呢?这将是我们MVC的重点所在。
首先观察Action结构。控制器与方法一起构成了一个Action对象,即action = controller + methods
。我们解析一个控制器所有的信息,将其保存在Action身上。一个Action有如下属性。
- 一个控制器对应着一个Action,如下所示Action类有
IController controller
属性; - 控制器注解
@Path
有URL路径,故如下所示Action类有String path
属性; - 控制器方法有注解
@GET
、@POST
等四种常见HTTP方法,故如下所示Action类有Method getMethod、postMethod、putMethod、deleteMethod
属性; - 控制器控制器的方法还可以有注解@Path子路径,故如下所示Action类有Map<String, Action> children属性;
Action通过children
属性扩展下一级路径,形成一颗Action树。children本身是一个Map,键是URL路径的各个目录,键值是路径对应的Action。如下面控制器 FooController对应树状图,如插图所示。
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
@Path("/foo")
public class FooController implements IController {
@GET
public String get();
@POST
public String post();
@PUT
public String put();
@DELETE
public String delete();
@Path("/bar")
@GET
public String barGet();
@Path("/bar")
@DELETE
public String barDELETE();
@Path("/xyz")
@GET
public String xyzGET();
@Path("/xyz/bar/foo")
@POST
public String xyzPOST();
}
控制器FooController一开始的根目录为foo
,有下级目录/foo/bar、/foo/xyz和/foo/xyz/bar/foo
。篇幅所限,最后的/xyz/bar/foo
没有在图中列出。如果要列出,还有树节点/foo/xyz/bar和/foo/xyz/bar/foo
这两个,其中/foo/xyz/bar节点有一Action实例,但是其属性getMethod、postMethod、putMethod、deleteMethod
皆为null
,仅承托下一级树节点之用;节点/foo/xyz/bar/foo有一属性 postMethod,因为它本身对应有POST的方法。
所有控制器路径与Action的关系,都定义在一个全局的静态成员urlMappingTree
身上,它是一个Action Map,如下插图所示。
控制器的分析工作在Servlet过滤器初始化阶段完成,最终结果就是形成这个urlMappingTree
(有读者可能会问,Tomcat的webapps
下面可以部署多个项目,在不同的目录。假设修改项目A的静态static
成员,会影响别的项目的static
成员吗?Tomcat对每一个项目使用一个类加载器。JVM中判断一个类是否相同是两个条件,全限定名相同,类加载器相同。因此这种情况是互相隔离的,不用担心)的Map结构。分析工作是怎么完成的呢?回到ControllerScanner
类的add()
方法,开始了对控制器的相关解析工作,返回一个Action并保存在urlMappingTree
,如插图示。
第一个重点方法是findTreeByPath()
,这里调用的是重载后的方法,其原型方法如下插图所示。
参数path
是URL路径转换为队列(Queen)。队列的特性是在列表的头端进行删除操作,而在列表的后端进行插入操作。一开始路径是String,通过字符串split()
分拆为数组后,转换为LinkedList返回。LinkedList也是一种Queen对象。
对于Map树要进行遍历查找目标Action,输入条件是path
。当前初始化工作是新建Action节点,也就是说找不到目标Action就会新建Action。这里应用了数据结构队列的知识,当path队列压出一元素,马上进行Map.get(path[0])
的匹配,若为null则表示接下来要新建Action,否则是已经存在的Action。然后检查这队列是否已经为空(path.isEmpty()
),若为空表示已经查找完毕,返回target
即可,否则仍要查找下一级目录。这时队列已经压出头端的元素,剩下未查找的目录元素,则再次调用本方法,即递归,直到找到匹配路径为止,或者找不到返回null
。在递归之前,要检查一下children
属性是否已有Map实例。
之所以要提供boolean createIfEmpty
参数,是为了后来MvcDispatcher命中路径时做的准备。那时候,Action已经初始化完毕了,每次请求过来分析路径查找Action,不再有任何新建的操作,故只是单纯的查找操作。
生成Action对象之后,add(
)接着执行的方法是创建控制器实例createControllerInstance()
和解析控制器方法parseMethod()
。创建实例应用到了反射和依赖注射的概念,下面相关章节会讲,这里先定懂点谈谈parseMethod()
,如插图所示。
通过反射可以获取控制器方法所有的方法(getMethods()
),但只带有@GET/@POST/@PUT/@DELETE
注解的方法才会被解析,表明是HTTP请求的方法。另外一种情形是:带@Path
组件的和不带的,前者表示当前路径下的方法;后者为子路径的方法,由控制器总路径加上子路径组成最终的目标路径,子路径一样可以有自己的GET/POST/PUT/DELETE
方法。
children
是子路径保存所在Map之引用,创建好的subAction会保存于此。subAction的controller指针会指向当前控制器实例。
methodSend()
的作用是根据方法当前的@GET/@POST/@PUT/@DELETE
注解分门别类地指向对应的Method方法,以便MVC有请求到达时执行对应的控制器方法。这一过程比较简单就不张贴代码了。
分析控制器的过程到这里基本完成了,整个过滤器的初始化init()
工作到这里也结束了。总的来说,Map结构的urlMappingTree
保存了所有控制器路由与执行方法的映射关系,它是一棵树,如果对其进行遍历查找匹配的路径成为了一个关键算法,这里简单地应用到队列的知识,是理解MVC路由匹配的关键。除此之外,对于相关注解的使用运用到了Java反射的知识。
以上是关于学写一个 Java Web MVC 框架的主要内容,如果未能解决你的问题,请参考以下文章