欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

手写SpringMVC 框架

程序员文章站 2022-06-05 18:57:56
手写SpringMVC框架 细嗅蔷薇 心有猛虎 背景:Spring 想必大家都听说过,可能现在更多流行的是Spring Boot 和Spring Cloud 框架;但是SpringMVC 作为一款实现了MVC 设计模式的web (表现层) 层框架,其高开发效率和高性能也是现在很多公司仍在采用的框架; ......

手写springmvc框架

 

细嗅蔷薇 心有猛虎

背景:spring 想必大家都听说过,可能现在更多流行的是spring boot 和spring cloud 框架;但是springmvc 作为一款实现了mvc 设计模式的web (表现层) 层框架,其高开发效率和高性能也是现在很多公司仍在采用的框架;除此之外,spring 源码大师级的代码规范和设计思想都十分值得学习;退一步说,spring boot 框架底层也有很多spring 的东西,而且面试的时候还会经常被问到springmvc 原理,一般人可能也就是只能把springmvc 的运行原理背出来罢了,至于问到有没有了解其底层实现(代码层面),那很可能就歇菜了,但您要是可以手写springmvc 框架就肯定可以令面试官刮目相看,所以手写springmvc 值得一试。

在设计自己的springmvc 框架之前,需要了解下其运行流程。

一、springmvc 运行流程

图1. springmvc 运行流程

手写SpringMVC 框架

1、用户向服务器发送请求,请求被spring 前端控制器dispatcherservlet 捕获;

2、dispatcherservlet 收到请求后调用handlermapping 处理器映射器;

3、处理器映射器对请求url 进行解析,得到请求资源标识符(uri);然后根据该uri,调用handlermapping 获得该handler 配置的所有相关的对象(包括handler 对象以及handler 对象对应的拦截器),再以handlerexecutionchain 对象的形式返回给dispatcherservlet

4、dispatcherservlet 根据获得的handler通过handleradapter 处理器适配器选择一个合适的handleradapter;(附注:如果成功获得handleradapter 后,此时将开始执行拦截器的prehandler(...)方法);

5、提取request 中的模型数据,填充handler 入参,开始执行handler(即controller);【在填充handler的入参过程中,根据你的配置,spring 将帮你做一些额外的工作如:httpmessageconveter:将请求消息(如jsonxml等数据)转换成一个对象,将对象转换为指定的响应信息数据转换:对请求消息进行数据转换,如string转换成integerdouble等;数据格式化:对请求消息进行数据格式化,如将字符串转换成格式化数字或格式化日期等;数据验证:验证数据的有效性(长度、格式等),验证结果存储到bindingresulterror

6、controller 执行完成返回modelandview 对象;

7、handleradapter 将controller 执行结果modelandview 对象返回给dispatcherservlet;

8、dispatcherservlet 将modelandview 对象传给viewreslover 视图解析器;

9、viewreslover 根据返回的modelandview,选择一个适合的viewresolver (必须是已经注册到spring容器中的viewresolver)返回给dispatcherservlet;

10、dispatcherservlet 对view 进行渲染视图(即将模型数据填充至视图中);

11、dispatcherservlet 将渲染结果响应用户(客户端)。

二、springmvc 框架设计思路

1、读取配置阶段

图2. springmvc 继承关系

手写SpringMVC 框架

      第一步就是配置web.xml,加载自定义的dispatcherservlet。而从图中可以看出,springmvc 本质上是一个servlet,这个servlet 继承自httpservlet,此外,frameworkservlet 负责初始springmvc的容器,并将spring 容器设置为父容器;为了读取web.xml 中的配置,需要用到servletconfig 这个类,它代表当前servlet 在web.xml 中的配置信息,然后通过web.xml 中加载我们自己写的mydispatcherservlet 和读取配置文件。

2、初始化阶段

初始化阶段会在dispatcherservlet 类中,按顺序实现下面几个步骤:

1、加载配置文件;

2、扫描当前项目下的所有文件;

3、拿到扫描到的类,通过反射机制将其实例化,并且放到ioc 容器中(map的键值对  beanname-bean) beanname默认是首字母小写;

4、初始化path 与方法的映射;

5、获取请求传入的参数并处理参数通过初始化好的handlermapping 中拿出url 对应的方法名,反射调用。

3、运行阶段

      运行阶段,每一次请求将会调用doget 或dopost 方法,它会根据url 请求去handlermapping 中匹配到对应的method,然后利用反射机制调用controller 中的url 对应的方法,并得到结果返回。

三、实现springmvc 框架

      首先,小老弟springmvc 框架只实现自己的@controller 和@requestmapping 注解,其它注解功能实现方式类似,实现注解较少所以项目比较简单,可以看到如下工程文件及目录截图。

图3. 工程文件及目录

手写SpringMVC 框架

 1、创建java web 工程

创建java web 工程,勾选javaee 下方的web application 选项,next。

图4. 创建java web 工程

手写SpringMVC 框架

 2、在工程web-inf 下的web.xml 中加入下方配置

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
 3          xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
 4          xsi:schemalocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
 5          version="4.0">
 6 
 7     <servlet>
 8         <servlet-name>dispatcherservlet</servlet-name>
 9         <servlet-class>com.tjt.springmvc.dispatcherservlet</servlet-class>
10     </servlet>
11     <servlet-mapping>
12         <servlet-name>dispatcherservlet</servlet-name>
13         <url-pattern>/</url-pattern>
14     </servlet-mapping>
15 
16 </web-app>

3、创建自定义controller 注解

 1 package com.tjt.springmvc;
 2 
 3 
 4 import java.lang.annotation.*;
 5 
 6 
 7 /**
 8  * @mycontroller 自定义注解类
 9  *
10  * @@target(elementtype.type)
11  * 表示该注解可以作用在类上;
12  *
13  * @retention(retentionpolicy.runtime)
14  * 表示该注解会在class 字节码文件中存在,在运行时可以通过反射获取到
15  *
16  * @documented
17  * 标记注解,表示可以生成文档
18  */
19 @target(elementtype.type)
20 @retention(retentionpolicy.runtime)
21 @documented
22 public @interface mycontroller {
23 
24     /**
25      * public class mycontroller
26      * 把 class 替换成 @interface 该类即成为注解类
27      */
28 
29     /**
30      * 为controller 注册别名
31      * @return
32      */
33     string value() default "";
34     
35 }

4、创建自定义requestmapping 注解

 1 package com.tjt.springmvc;
 2 
 3 
 4 import java.lang.annotation.*;
 5 
 6 
 7 /**
 8  * @myrequestmapping 自定义注解类
 9  *
10  * @target({elementtype.method,elementtype.type})
11  * 表示该注解可以作用在方法、类上;
12  *
13  * @retention(retentionpolicy.runtime)
14  * 表示该注解会在class 字节码文件中存在,在运行时可以通过反射获取到
15  *
16  * @documented
17  * 标记注解,表示可以生成文档
18  */
19 @target({elementtype.method, elementtype.type})
20 @retention(retentionpolicy.runtime)
21 @documented
22 public @interface myrequestmapping {
23 
24     /**
25      * public @interface myrequestmapping
26      * 把 class 替换成 @interface 该类即成为注解类
27      */
28 
29     /**
30      * 表示访问该方法的url
31      * @return
32      */
33     string value() default "";
34 
35 }

5、设计用于获取项目工程下所有的class 文件的封装工具类

  1 package com.tjt.springmvc;
  2 
  3 
  4 import java.io.file;
  5 import java.io.filefilter;
  6 import java.net.jarurlconnection;
  7 import java.net.url;
  8 import java.net.urldecoder;
  9 import java.util.arraylist;
 10 import java.util.enumeration;
 11 import java.util.list;
 12 import java.util.jar.jarentry;
 13 import java.util.jar.jarfile;
 14 
 15 /**
 16  * 从项目工程包package 中获取所有的class 工具类
 17  */
 18 public class classutils {
 19 
 20     /**
 21      * 静态常量
 22      */
 23     private static string file_constant = "file";
 24     private static string utf8_constant = "utf-8";
 25     private static string jar_constant = "jar";
 26     private static string point_class_constant = ".class";
 27     private static char point_constant = '.';
 28     private static char left_line_constant = '/';
 29 
 30 
 31     /**
 32      * 定义私有构造函数来屏蔽隐式公有构造函数
 33      */
 34     private classutils() {
 35     }
 36 
 37 
 38     /**
 39      * 从项目工程包package 中获取所有的class
 40      * getclasses
 41      *
 42      * @param packagename
 43      * @return
 44      */
 45     public static list<class<?>> getclasses(string packagename) throws exception {
 46 
 47 
 48         list<class<?>> classes = new arraylist<class<?>>();  // 定义一个class 类的泛型集合
 49         boolean recursive = true;  // recursive 是否循环迭代
 50         string packagedirname = packagename.replace(point_constant, left_line_constant);  // 获取包的名字 并进行替换
 51         enumeration<url> dirs;  // 定义一个枚举的集合 分别保存该目录下的所有java 类文件及jar 包等内容
 52         dirs = thread.currentthread().getcontextclassloader().getresources(packagedirname);
 53         /**
 54          * 循环迭代 处理这个目录下的things
 55          */
 56         while (dirs.hasmoreelements()) {
 57             url url = dirs.nextelement();  // 获取下一个元素
 58             string protocol = url.getprotocol();  // 得到协议的名称 protocol
 59             // 如果是
 60             /**
 61              * 若protocol 是文件形式
 62              */
 63             if (file_constant.equals(protocol)) {
 64                 string filepath = urldecoder.decode(url.getfile(), utf8_constant); // 获取包的物理路径
 65                 findandaddclassesinpackagebyfile(packagename, filepath, recursive, classes); // 以文件的方式扫描整个包下的文件 并添加到集合中
 66                 /**
 67                  * 若protocol 是jar 包文件
 68                  */
 69             } else if (jar_constant.equals(protocol)) {
 70                 jarfile jar;  // 定义一个jarfile
 71                 jar = ((jarurlconnection) url.openconnection()).getjarfile();  // 获取jar
 72                 enumeration<jarentry> entries = jar.entries();  // 从jar 包中获取枚举类
 73                 /**
 74                  * 循环迭代从jar 包中获得的枚举类
 75                  */
 76                 while (entries.hasmoreelements()) {
 77                     jarentry entry = entries.nextelement();  // 获取jar里的一个实体,如目录、meta-inf等文件
 78                     string name = entry.getname();
 79                     /**
 80                      * 若实体名是以 / 开头
 81                      */
 82                     if (name.charat(0) == left_line_constant) {
 83                         name = name.substring(1);  // 获取后面的字符串
 84                     }
 85                     // 如果
 86                     /**
 87                      * 若实体名前半部分和定义的包名相同
 88                      */
 89                     if (name.startswith(packagedirname)) {
 90                         int idx = name.lastindexof(left_line_constant);
 91                         /**
 92                          * 并且实体名以为'/' 结尾
 93                          * 若其以'/' 结尾则是一个包
 94                          */
 95                         if (idx != -1) {
 96                             packagename = name.substring(0, idx).replace(left_line_constant, point_constant);  // 获取包名 并把'/' 替换成'.'
 97                         }
 98                         /**
 99                          * 若实体是一个包 且可以继续迭代
100                          */
101                         if ((idx != -1) || recursive) {
102                             if (name.endswith(point_class_constant) && !entry.isdirectory()) {  // 若为.class 文件 且不是目录
103                                 string classname = name.substring(packagename.length() + 1, name.length() - 6);  // 则去掉.class 后缀并获取真正的类名
104                                 classes.add(class.forname(packagename + '.' + classname)); // 把获得到的类名添加到classes
105                             }
106                         }
107                     }
108                 }
109             }
110         }
111 
112         return classes;
113     }
114 
115 
116     /**
117      * 以文件的形式来获取包下的所有class
118      * findandaddclassesinpackagebyfile
119      *
120      * @param packagename
121      * @param packagepath
122      * @param recursive
123      * @param classes
124      */
125     public static void findandaddclassesinpackagebyfile(
126             string packagename, string packagepath,
127             final boolean recursive,
128             list<class<?>> classes) throws exception {
129 
130 
131         file dir = new file(packagepath);  // 获取此包的目录并建立一个file
132 
133         if (!dir.exists() || !dir.isdirectory()) {  // 若dir 不存在或者 也不是目录就直接返回
134             return;
135         }
136 
137         file[] dirfiles = dir.listfiles(new filefilter() {  // 若dir 存在 则获取包下的所有文件、目录
138 
139             /**
140              * 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class 结尾的文件(编译好的java 字节码文件)
141              * @param file
142              * @return
143              */
144             @override
145             public boolean accept(file file) {
146                 return (recursive && file.isdirectory()) || (file.getname().endswith(point_class_constant));
147             }
148         });
149 
150         /**
151          * 循环所有文件获取java 类文件并添加到集合中
152          */
153         for (file file : dirfiles) {
154             if (file.isdirectory()) {  // 若file 为目录 则继续扫描
155                 findandaddclassesinpackagebyfile(packagename + "." + file.getname(), file.getabsolutepath(), recursive,
156                         classes);
157             } else {  // 若file 为java 类文件 则去掉后面的.class 只留下类名
158                 string classname = file.getname().substring(0, file.getname().length() - 6);
159                 classes.add(class.forname(packagename + '.' + classname));  // 把classname 添加到集合中去
160 
161             }
162         }
163     }
164 }

6、访问跳转页面index.jsp

 1 <%--
 2   created by intellij idea.
 3   user: apple
 4   date: 2019-11-07
 5   time: 13:28
 6   to change this template use file | settings | file templates.
 7 --%>
 8 <%--
 9 <%@ page contenttype="text/html;charset=utf-8" language="java" %>
10 --%>
11 <html>
12   <head>
13     <title>my fucking springmvc</title>
14   </head>
15   <body>
16   <h2>the lie we live!</h2>
17   <h2>my fucking springmvc</h2>
18   </body>
19 </html>

7、自定义dispatcherservlet 设计,继承httpservlet,重写init 方法、doget、dopost 等方法,以及自定义注解要实现的功能。

  1 package com.tjt.springmvc;
  2 
  3 
  4 import javax.servlet.servletconfig;
  5 import javax.servlet.servletexception;
  6 import javax.servlet.http.httpservlet;
  7 import javax.servlet.http.httpservletrequest;
  8 import javax.servlet.http.httpservletresponse;
  9 import java.io.ioexception;
 10 import java.lang.reflect.invocationtargetexception;
 11 import java.lang.reflect.method;
 12 import java.util.list;
 13 import java.util.map;
 14 import java.util.objects;
 15 import java.util.concurrent.concurrenthashmap;
 16 
 17 
 18 
 19 /**
 20  * dispatcherservlet 处理springmvc 框架流程
 21  * 主要流程:
 22  * 1、包扫描获取包下面所有的类
 23  * 2、初始化包下面所有的类
 24  * 3、初始化handlermapping 方法,将url 和方法对应上
 25  * 4、实现httpservlet 重写dopost 方法
 26  *
 27  */
 28 public class dispatcherservlet extends httpservlet {
 29 
 30     /**
 31      * 部分静态常量
 32      */
 33     private static string package_class_null_ex = "包扫描后的classes为null";
 34     private static string http_not_exist = "sorry http is not exit 404";
 35     private static string method_not_exist = "sorry method is not exit 404";
 36     private static string point_jsp = ".jsp";
 37     private static string left_line = "/";
 38 
 39     /**
 40      * 用于存放springmvc bean 的容器
 41      */
 42     private concurrenthashmap<string, object> mvcbeans = new concurrenthashmap<>();
 43     private concurrenthashmap<string, object> mvcbeanurl = new concurrenthashmap<>();
 44     private concurrenthashmap<string, string> mvcmethodurl = new concurrenthashmap<>();
 45     private static string project_package_path = "com.tjt.springmvc";
 46 
 47 
 48     /**
 49      * 按顺序初始化组件
 50      * @param config
 51      */
 52     @override
 53     public void init(servletconfig config) {
 54         string packagepath = project_package_path;
 55         try {
 56             //1.进行报扫描获取当前包下面所有的类
 57             list<class<?>> classes = comscanpackage(packagepath);
 58             //2.初始化springmvcbean
 59             initspringmvcbean(classes);
 60         } catch (exception e) {
 61             e.printstacktrace();
 62         }
 63         //3.将请求地址和方法进行映射
 64         inithandmapping(mvcbeans);
 65     }
 66 
 67 
 68     /**
 69      * 调用classutils 工具类获取工程中所有的class
 70      * @param packagepath
 71      * @return
 72      * @throws exception
 73      */
 74     public list<class<?>> comscanpackage(string packagepath) throws exception {
 75         list<class<?>> classes = classutils.getclasses(packagepath);
 76         return classes;
 77     }
 78 
 79     /**
 80      * 初始化springmvc bean
 81      *
 82      * @param classes
 83      * @throws exception
 84      */
 85     public void initspringmvcbean(list<class<?>> classes) throws exception {
 86         /**
 87          * 若包扫描出的classes 为空则直接抛异常
 88          */
 89         if (classes.isempty()) {
 90             throw new exception(package_class_null_ex);
 91         }
 92 
 93         /**
 94          * 遍历所有classes 获取@mycontroller 注解
 95          */
 96         for (class<?> aclass : classes) {
 97             //获取被自定义注解的controller 将其初始化到自定义springmvc 容器中
 98             mycontroller declaredannotation = aclass.getdeclaredannotation(mycontroller.class);
 99             if (declaredannotation != null) {
100                 //获取类的名字
101                 string beanid = lowerfirstcapse(aclass.getsimplename());
102                 //获取对象
103                 object beanobj = aclass.newinstance();
104                 //放入spring 容器
105                 mvcbeans.put(beanid, beanobj);
106             }
107         }
108 
109     }
110 
111     /**
112      * 初始化handlermapping 方法
113      *
114      * @param mvcbeans
115      */
116     public void inithandmapping(concurrenthashmap<string, object> mvcbeans) {
117         /**
118          * 遍历springmvc 获取注入的对象值
119          */
120         for (map.entry<string, object> entry : mvcbeans.entryset()) {
121             object objvalue = entry.getvalue();
122             class<?> aclass = objvalue.getclass();
123             //获取当前类 判断其是否有自定义的requestmapping 注解
124             string mappingurl = null;
125             myrequestmapping anrequestmapping = aclass.getdeclaredannotation(myrequestmapping.class);
126             if (anrequestmapping != null) {
127                 mappingurl = anrequestmapping.value();
128             }
129             //获取当前类所有方法,判断方法上是否有注解
130             method[] declaredmethods = aclass.getdeclaredmethods();
131             /**
132              * 遍历注解
133              */
134             for (method method : declaredmethods) {
135                 myrequestmapping methoddeclaredannotation = method.getdeclaredannotation(myrequestmapping.class);
136                 if (methoddeclaredannotation != null) {
137                     string methodurl = methoddeclaredannotation.value();
138                     mvcbeanurl.put(mappingurl + methodurl, objvalue);
139                     mvcmethodurl.put(mappingurl + methodurl, method.getname());
140                 }
141             }
142 
143         }
144 
145     }
146 
147     /**
148      * @param str
149      * @return 类名首字母小写
150      */
151     public static string lowerfirstcapse(string str) {
152         char[] chars = str.tochararray();
153         chars[0] += 32;
154         return string.valueof(chars);
155 
156     }
157 
158     /**
159      * dopost 请求
160      * @param req
161      * @param resp
162      * @throws servletexception
163      * @throws ioexception
164      */
165     @override
166     protected void dopost(httpservletrequest req, httpservletresponse resp) throws servletexception, ioexception {
167         try {
168             /**
169              * 处理请求
170              */
171             doservelt(req, resp);
172         } catch (nosuchmethodexception e) {
173             e.printstacktrace();
174         } catch (invocationtargetexception e) {
175             e.printstacktrace();
176         } catch (illegalaccessexception e) {
177             e.printstacktrace();
178         }
179     }
180 
181     /**
182      * doservelt 处理请求
183      * @param req
184      * @param resp
185      * @throws ioexception
186      * @throws nosuchmethodexception
187      * @throws invocationtargetexception
188      * @throws illegalaccessexception
189      * @throws servletexception
190      */
191     private void doservelt(httpservletrequest req, httpservletresponse resp) throws ioexception, nosuchmethodexception, invocationtargetexception, illegalaccessexception, servletexception {
192         //获取请求地址
193         string requesturl = req.getrequesturi();
194         //查找地址所对应bean
195         object object = mvcbeanurl.get(requesturl);
196         if (objects.isnull(object)) {
197             resp.getwriter().println(http_not_exist);
198             return;
199         }
200         //获取请求的方法
201         string methodname = mvcmethodurl.get(requesturl);
202         if (methodname == null) {
203             resp.getwriter().println(method_not_exist);
204             return;
205         }
206 
207 
208         //通过构反射执行方法
209         class<?> aclass = object.getclass();
210         method method = aclass.getmethod(methodname);
211 
212         string invoke = (string) method.invoke(object);
213         // 获取后缀信息
214         string suffix = point_jsp;
215         // 页面目录地址
216         string prefix = left_line;
217         req.getrequestdispatcher(prefix + invoke + suffix).forward(req, resp);
218 
219 
220 
221 
222     }
223 
224     /**
225      * doget 请求
226      * @param req
227      * @param resp
228      * @throws servletexception
229      * @throws ioexception
230      */
231     @override
232     protected void doget(httpservletrequest req, httpservletresponse resp) throws servletexception, ioexception {
233         this.dopost(req, resp);
234     }
235 
236 
237 }

8、测试手写springmvc 框架效果类testmyspringmvc 。

 1 package com.tjt.springmvc;
 2 
 3 
 4 /**
 5  * 手写springmvc 测试类
 6  * testmyspringmvc
 7  */
 8 @mycontroller
 9 @myrequestmapping(value = "/tjt")
10 public class testmyspringmvc {
11 
12 
13     /**
14      * 测试手写springmvc 框架效果 testmymvc1
15      * @return
16      */
17     @myrequestmapping("/mvc")
18     public string testmymvc1() {
19         system.out.println("he lie we live!");
20         return "index";
21     }
22 
23 
24 }

9、配置tomcat 用于运行web 项目。

图5. 配置tomcat

手写SpringMVC 框架

10、运行项目,访问测试。

1、输入正常路径 http://localhost:8080/tjt/mvc 访问测试效果如下:

图6. 正常路径测试效果

手写SpringMVC 框架

2、输入非法(不存在)路径 http://localhost:8080/tjt/mvc8 访问测试效果如下:

图7. 非法路径测试效果

手写SpringMVC 框架

3、控制台打印“the lie we live”如下:

图8. 控制台打印

手写SpringMVC 框架

测试效果如上则证明成功手写springmvc 框架,恭喜。

 

 

细嗅蔷薇 心有猛虎