手写SpringMVC 框架
手写springmvc框架
细嗅蔷薇 心有猛虎
背景:spring 想必大家都听说过,可能现在更多流行的是spring boot 和spring cloud 框架;但是springmvc 作为一款实现了mvc 设计模式的web (表现层) 层框架,其高开发效率和高性能也是现在很多公司仍在采用的框架;除此之外,spring 源码大师级的代码规范和设计思想都十分值得学习;退一步说,spring boot 框架底层也有很多spring 的东西,而且面试的时候还会经常被问到springmvc 原理,一般人可能也就是只能把springmvc 的运行原理背出来罢了,至于问到有没有了解其底层实现(代码层面),那很可能就歇菜了,但您要是可以手写springmvc 框架就肯定可以令面试官刮目相看,所以手写springmvc 值得一试。
在设计自己的springmvc 框架之前,需要了解下其运行流程。
一、springmvc 运行流程
图1. 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:将请求消息(如json、xml等数据)转换成一个对象,将对象转换为指定的响应信息;数据转换:对请求消息进行数据转换,如string转换成integer、double等;数据格式化:对请求消息进行数据格式化,如将字符串转换成格式化数字或格式化日期等;数据验证:验证数据的有效性(长度、格式等),验证结果存储到bindingresult或error中 】
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 继承关系
第一步就是配置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. 工程文件及目录
1、创建java web 工程
创建java web 工程,勾选javaee 下方的web application 选项,next。
图4. 创建java web 工程
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
10、运行项目,访问测试。
1、输入正常路径 http://localhost:8080/tjt/mvc 访问测试效果如下:
图6. 正常路径测试效果
2、输入非法(不存在)路径 http://localhost:8080/tjt/mvc8 访问测试效果如下:
图7. 非法路径测试效果
3、控制台打印“the lie we live”如下:
图8. 控制台打印
测试效果如上则证明成功手写springmvc 框架,恭喜。
细嗅蔷薇 心有猛虎
推荐阅读
-
.netCore+Vue 搭建的简捷开发框架 (4)--NetCore 基础 -2
-
设计模式:与SpringMVC底层息息相关的适配器模式
-
PHP开发框架kohana3.3.1在nginx下的伪静态设置例子
-
YOYOW-WeCenter框架发布,建站上链YOYOW更便捷了
-
CodeIgniter框架中_remap()使用方法2例
-
TP3.2.3框架使用CKeditor编辑器在页面中上传图片的方法分析
-
Laravel5.1 框架Request请求操作常见用法实例分析
-
Laravel5.1 框架模型远层一对多关系实例分析
-
Laravel5.1框架路由分组用法实例分析
-
Laravel5.1 框架Middleware中间件基本用法实例分析