手写迷你SpringMVC框架
前言
学习如何使用spring,springmvc是很快的,但是在往后使用的过程中难免会想探究一下框架背后的原理是什么,本文将通过讲解如何手写一个简单版的springmvc框架,直接从代码上看框架中请求分发,控制反转和依赖注入是如何实现的。
建议配合示例源码阅读,github地址如下:
https://github.com/liuyj24/mini-spring
项目搭建
项目搭建可以参考github中的项目,先选好jar包管理工具,maven和gradle都行,本项目使用的是gradle。
然后在项目下建两个模块,一个是framework,用于编写框架;另外一个是test,用于应用并测试框架(注意test模块要依赖framework模块)。
接着在framework模块下按照spring创建好beans,core,context,web等模块对应的包,完成后便可以进入框架的编写了。
请求分发
在讲请求分发之前先来梳理一下整个web模型:
- 首先用户在客户端发送一个请求到服务器,经操作系统的tcp/ip栈解析后会交到在某个端口监听的web服务器。
- web服务器程序监听到请求后便会把请求分发给对应的程序进行处理。比如tomcat就会将请求分发给对应的java程序(servlet)进行处理,web服务器本身是不进行请求处理的。
本项目的web服务器选择tomcat,而且为了能让项目直接跑起来,选择了在项目中内嵌tomcat,这样框架在做测试的时候就能像spring boot一样一键启动,方便测试。
servlet
既然选择了使用java编写服务端程序,那就不得不提到servlet接口了。为了规范服务器与java程序之间的通信方式,java官方制定了servlet规范,服务端的java应用程序必须实现该接口,把java作为处理语言的服务器也必须要根据servlet规范进行对接。
在还没有spring之前,人们是这么开发web程序的:一个业务逻辑对应一个servlet,所以一个大项目中会有多个servlet,这大量的servlet会被配置到一个叫web.xml的配置文件中,当服务器运行的时候,tomcat会根据请求的uri到web.xml文件中寻找对应的servlet业务类处理请求。
但是你想,每来一个请求就创建一个servlet,而且一个servlet实现类中我们通常只重写一个service方法,另外四个方法都只是给个空实现,这太浪费资源了。而且编起程序来创建很多servlet还很难管理。能不能改进一下?
spring的dispatcherservlet
方法确实有:
从上图可以看到,我们原来是经过web服务器把请求分发到不同的servlet;我们可以换个思路,让web服务器把请求都发送到一个servlet,再由这个servlet把请求按照uri分发给不同的方法进行处理。
这样一来,不管收到什么请求,web服务器都会分发到同一个servlet(dispatcherservlet),避免了多个servlet所带来的问题,有以下好处:
- 把分发请求这一步从web服务器移动到框架内,这样更容易控制,也方便扩展。
- 可以把同一个业务的处理方法集中到同一个类里,把这种类起名为controller,一个controller中有多个处理方法,这样配置分散不杂乱。
- 配置uri映射路径的时候可以不使用配置文件,直接在处理方法上用注解配置即可,解决了配置集中,大而杂的问题。
实操
建议配合文章开头给出的源码进行参考
- 首先在web.mvc包中创建三个注解:controller,requestmapping,requestparam,有了注解我们才能在框架启动时动态获得配置信息。
- 由于处理方法都是被注解的,要想解析被注解的类,首先得获得项目中相关的所有类,对应是源码中core包下的classscanner类
public class classscanner { public static list<class<?>> scanclass(string packagename) throws ioexception, classnotfoundexception { //用于保存结果的容器 list<class<?>> classlist = new arraylist<>(); //把文件名改为文件路径 string path = packagename.replace(".", "/"); //获取默认的类加载器 classloader classloader = thread.currentthread().getcontextclassloader(); //通过文件路径获取该文件夹下所有资源的url enumeration<url> resources = classloader.getresources(path); int index = 0;//测试 while(resources.hasmoreelements()){ //拿到下一个资源 url resource = resources.nextelement(); //先判断是否是jar包,因为默认.class文件会被打包为jar包 if(resource.getprotocol().contains("jar")){ //把url强转为jar包链接 jarurlconnection jarurlconnection = (jarurlconnection)resource.openconnection(); //根据jar包获取jar包的路径名 string jarfilepath = jarurlconnection.getjarfile().getname(); //把jar包下所有的类添加的保存结果的容器中 classlist.addall(getclassfromjar(jarfilepath, path)); }else{//也有可能不是jar文件,先放下 //todo } } return classlist; } /** * 获取jar包中所有路径符合的类文件 * @param jarfilepath * @param path * @return */ private static list<class<?>> getclassfromjar(string jarfilepath, string path) throws ioexception, classnotfoundexception { list<class<?>> classes = new arraylist<>();//保存结果的集合 jarfile jarfile = new jarfile(jarfilepath);//创建对应jar包的句柄 enumeration<jarentry> jarentries = jarfile.entries();//拿到jar包中所有的文件 while(jarentries.hasmoreelements()){ jarentry jarentry = jarentries.nextelement();//拿到一个文件 string entryname = jarentry.getname();//拿到文件名,大概是这样:com/shenghao/test/test.class if (entryname.startswith(path) && entryname.endswith(".class")){//判断是否是类文件 string classfullname = entryname.replace("/", ".") .substring(0, entryname.length() - 6); classes.add(class.forname(classfullname)); } } return classes; } }
- 然后在handler包创建mappinghandler类,在将来框架运行的过程中,一个mappinghandler就对应一个业务逻辑,比如说增加一个用户。所以一个mappinghandler中要有“请求uri,处理方法,方法的参数,方法所处的类”这四个字段,其中请求uri用于匹配请求uri,后面三个参数用于运行时通过反射调用该处理方法
public class mappinghandler { private string uri; private method method; private class<?> controller; private string[] args; mappinghandler(string uri, method method, class<?> cls, string[] args){ this.uri = uri; this.method = method; this.controller = cls; this.args = args; } public boolean handle(servletrequest req, servletresponse res) throws illegalaccessexception, instantiationexception, invocationtargetexception, ioexception { //拿到请求的uri string requesturi = ((httpservletrequest)req).getrequesturi(); if(!uri.equals(requesturi)){//如果和自身uri不同就跳过 return false; } object[] parameters = new object[args.length]; for(int i = 0; i < args.length; i++){ parameters[i] = req.getparameter(args[i]); } object ctl = beanfactory.getbean(controller); object response = method.invoke(ctl, parameters); res.getwriter().println(response.tostring()); return true; } }
- 接下来在handler包创建handlermanager类,这个类拥有一个静态的mappinghandler集合,这个类的作用是从获得的所有类中,找到被@controller注解的类,并将controller类中每个被@reqeustmapping注解的方法封装成一个mappinghandler,然后把mappinghandler放入静态集合中
public class handlermanager { public static list<mappinghandler> mappinghandlerlist = new arraylist<>(); /** * 处理类文件集合,挑出mappinghandler * @param classlist */ public static void resolvemappinghandler(list<class<?>> classlist){ for(class<?> cls : classlist){ if(cls.isannotationpresent(controller.class)){//mappinghandler会在controller里面 parsehandlerfromcontroller(cls);//继续从controller中分离出一个个mappinghandler } } } private static void parsehandlerfromcontroller(class<?> cls) { //先获取该controller中所有的方法 method[] methods = cls.getdeclaredmethods(); //从中挑选出被requestmapping注解的方法进行封装 for(method method : methods){ if(!method.isannotationpresent(requestmapping.class)){ continue; } string uri = method.getdeclaredannotation(requestmapping.class).value();//拿到requestmapping定义的uri list<string> paramnamelist = new arraylist<>();//保存方法参数的集合 for(parameter parameter : method.getparameters()){ if(parameter.isannotationpresent(requestparam.class)){//把有被requestparam注解的参数添加入集合 paramnamelist.add(parameter.getdeclaredannotation(requestparam.class).value()); } } string[] params = paramnamelist.toarray(new string[paramnamelist.size()]);//把参数集合转为数组,用于反射 mappinghandler mappinghandler = new mappinghandler(uri, method, cls, params);//反射生成mappinghandler mappinghandlerlist.add(mappinghandler);//把mappinghandler装入集合中 } } }
- 完成上面四步后,我们在框架启动的时候就获得了一个mappinghandler集合,当请求来到时,我们只要根据请求的uri从集合中找到对应的mappinghandler,就可以通过反射调用对应的处理方法,到此也就完成了框架请求分发的功能。
控制反转和依赖注入
完成了请求分发功能后,进一步想这么一个问题:
假设现在处理一个请求需要创建a,b,c三个对象,而
a 有个字段 d
b 有个字段 d
c 有个字段 b
如果按照顺序创建abc的话,
首先要创建一个d,然后创建一个a;
接着先创建一个d,然后创建一个b;
接着先创建一个d,然后创建一个b,才能创建出一个c
总共创建了一个a,两个b,一个c,三个d。
上述是我们编写程序的一方创建对象的方式,可以看到由于对象不能被重复引用,导致创建了大量重复对象。
为了解决这个问题,spring提出了bean这么个概念,你可以把一个bean理解为一个对象,但是他对比普通的对象有如下特点:
- 不像普通对象一样朝生暮死,声明周期较长
- 在整个虚拟机内可见,不像普通对象只在某个代码块中可见
- 维护成本高,以单例形式存在
为了制作出上述的bean,我们得有个bean工厂,bean工厂的原理也很简单:在框架初始化的时候创建相关的bean(也可以在用到的时候创建),当需要使用bean的时候直接从工厂中拿。也就是我们把创建对象的权力交给框架,这就是控制反转
有了bean工厂后按顺序创建abc的过程如下:
首先创建一个d,把d放入工厂,然后创建一个a,把a放入工厂;
接着从工厂拿出一个d,创建一个b,把b也放入工厂;
接着从工厂拿出一个b,创建一个c,把c也放入工厂;
总共创建了一个a,一个b,一个c,一个d
达到了对象重复利用的目的
至于创建出一个d,然后把d设置为a的一个字段这么个过程,叫做依赖注入
所以控制反转和依赖注入的概念其实很好理解,控制反转是一种思想,而依赖注入是控制反转的一种具体实现。
实操
- 首先在bean包下创建@bean和@autowired两个注解,同样是用于框架解析类的。
- 接着在bean包下创建beanfactory,beanfactory要能提供一个根据类获取实例的功能,这就要求他要有一个静态的getbean()方法,和一个保存bean的映射集合。
- 为了初始化bean,要有一个根据类文件集合解析出bean的方法。该方法会遍历集合中所有的类,把有注解的,属于bean的类提取出来,创建该类的对象并放到静态集合中。
- 在这里有个有意思的点——按什么顺序创建bean?在本文给出的源码中,用了一个循环来创建bean,如果该bean没有依赖其他的bean就直接创建,如果有依赖其他bean就看其他bean有没被创建出来,如果没有就跳过当前的bean,如果有就创建当前的bean。
- 在循环创建bean的过程中可能出现一种bean之间相互依赖的现象,源码中暂时对这种现象抛出异常,没作处理。
public class beanfactory { //保存bean实例的映射集合 private static map<class<?>, object> classtobean = new concurrenthashmap<>(); /** * 根据class类型获取bean * @param cls * @return */ public static object getbean(class<?> cls){ return classtobean.get(cls); } /** * 初始化bean工厂 * @param classlist 需要一个.class文件集合 * @throws exception */ public static void initbean(list<class<?>> classlist) throws exception { //先创建一个.class文件集合的副本 list<class<?>> tocreate = new arraylist<>(classlist); //循环创建bean实例 while(tocreate.size() != 0){ int remainsize = tocreate.size();//记录开始时集合大小,如果一轮结束后大小没有变证明有相互依赖 for(int i = 0; i < tocreate.size(); i++){//遍历创建bean,如果失败就先跳过,等下一轮再创建 if(finishcreate(tocreate.get(i))){ tocreate.remove(i); } } if(tocreate.size() == remainsize){//有相互依赖的情况先抛出异常 throw new exception("cycle dependency!"); } } } private static boolean finishcreate(class<?> cls) throws illegalaccessexception, instantiationexception { //创建的bean实例仅包括bean和controller注释的类 if(!cls.isannotationpresent(bean.class) && !cls.isannotationpresent(controller.class)){ return true; } //先创建实例对象 object bean = cls.newinstance(); //看看实例对象是否需要执行依赖注入,注入其他bean for(field field : cls.getdeclaredfields()){ if(field.isannotationpresent(autowired.class)){ class<?> fieldtype = field.gettype(); object reliantbean = beanfactory.getbean(fieldtype); if(reliantbean == null){//如果要注入的bean还未被创建就先跳过 return false; } field.setaccessible(true); field.set(bean, reliantbean); } } classtobean.put(cls, bean); return true; } }
- 有了bean工厂之后,凡是用到bean的地方都能直接通过bean工厂拿了
- 最后我们可以写一个小demo测试一下自己的框架是否能正确地处理请求完成响应。相信整个迷你框架撸下来,spring的核心功能,以及控制反转,依赖控制等名词在你脑海中不再只是概念,而是一行行清晰的代码了。
上一篇: Java考题知识点
下一篇: 基础篇-1.5Java的数组
推荐阅读
-
Python实现手写一个类似django的web框架示例
-
【java框架】SpringMVC(1)--SpringMVC入门
-
Java SSM框架(Spring+SpringMVC+MyBatis)搭建过程
-
详解手把手Maven搭建SpringMVC+Spring+MyBatis框架(超级详细版)
-
如何从零开始手写Koa2框架
-
使用maven整合Spring+SpringMVC+Mybatis框架详细步骤(图文)
-
基于vue框架手写一个notify插件实现通知功能的方法
-
SpringMVC框架实现图片上传与下载
-
Python(TensorFlow框架)实现手写数字识别系统的方法
-
使用spring拦截器手写权限认证框架