手写SpringMvc

一、简介

空闲之余手撕一把springMVC,以加深对spring的理解,尽可能写的全面,源码中注释也会很详细。话不多说开搞!

二、项目搭建

在IDEA上用MAVEN创建一个webApp项目:

项目构建.png

原来的springMVC中,最重要的一个类就是DispatchServlet即前端请求控制器,我们自定义自己的DispatchServlet,继承HttpServlet。
因为要继承HttpServlet,利用pom引入servlet-api的jar包:
...
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>3.0-alpha-1</version>
</dependency>

...
继承HttpServelet新建DispatchServlet类,重写doGet、doPost、和init方法。在init方法中实现包扫描、IOC容器初始化等一系列操作。这里面操作会在tomcat加载项目后初始化完成。
DispatchServlet.png

同时在web.xml配置DispatchServlet:
...
<servlet>
<servlet-name>DispatchServlet</servlet-name>
<servlet-class>com.zjx.myspringmvc.servelet.DispatchServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatchServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

...

三、流程解读

3.1.自定义注解

常用的@Controller、@Service、@RequestMapping、@RequestParam、@Qualifier等。我们对应定义自己的注解@MyController、@MyService、@MyRequestMapping、@MyRequestParam、@MyQualifier
自定义注解首先了解四种元注解: @Retention @Target @Document @Inherited

@Retention:注解的保留位置         
@Retention(RetentionPolicy.SOURCE)//注解仅存在于源码中,在class字节码文件中不包含
@Retention(RetentionPolicy.CLASS)//默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
@Retention(RetentionPolicy.RUNTIME)//注解会在class字节码文件中存在,在运行时可以通过反射获取到

@Target:注解的作用目标
@Target(ElementType.TYPE)//接口、类、枚举、注解
@Target(ElementType.FIELD)//字段、枚举的常量
@Target(ElementType.METHOD)//方法

*@Target(ElementType.PARAMETER)//方法参数*
*@Target(ElementType.CONSTRUCTOR)//构造函数*
*@Target(ElementType.LOCAL_VARIABLE)//局部变量*
*@Target(ElementType.ANNOTATION_TYPE)//注解*
*@Target(ElementType.PACKAGE)//包*  

@Document:说明该注解将被包含在javadoc中*

@Inherited:说明子类可以继承父类中的该注解*

我们自定义的注解用到前面三个,其中@Retention(RetentionPolicy.RUNTIME)、@Document是相同的。使用@Target不同的注解类各不相同。具体定义代码如下:
...
@Target({ElementType.TYPE})//可以用在接口、类、枚举
@Retention(RetentionPolicy.RUNTIME)//可以通过反射得到
@Documented//该注解将被包含在javadoc中
public @interface MyController {
String value() default " ";//
}

...
@Target({ElementType.FIELD})//用于字段、枚举的常量
@Retention(RetentionPolicy.RUNTIME)//可以通过反射得到
@Documented//该注解将被包含在javadoc中
public @interface MyQualifier {
String value() default " ";
}

...
@Target({ElementType.METHOD,ElementType.TYPE})//用于方法,类
@Retention(RetentionPolicy.RUNTIME)//可以通过反射得到
@Documented//该注解将被包含在javadoc中
public @interface MyRequestMapping {
String value() default " ";
}

...
@Target({ElementType.PARAMETER})//用于方法参数
@Retention(RetentionPolicy.RUNTIME)//可以通过反射得到
@Documented//该注解将被包含在javadoc中
public @interface MyRequestParam {
String value() default " ";
}

...
@Target({ElementType.TYPE})//可以用在接口、类、枚举
@Retention(RetentionPolicy.RUNTIME)//可以通过反射得到
@Documented//该注解将被包含在javadoc中
public @interface MyService {
String value() default " ";
}

...

3.2.init方法

其实这里就是整个项目的核心,首先重写init方法。在这个方法里按序实现包扫描、实例化(即初始化IOC容器)、依赖注入以及建立url与方法的映射关系,init方法如下。
...
@Override
public void init(ServletConfig config) throws ServletException {
//1、扫描哪些需要被实例化 class(包及包以下的class)
scanPackage("com.zjx");
//2、扫描出来的类 进实例化
instance();
//依赖注入 这里项目简单 只有service层注入controller 后续读者可以自实现dao->service
iocInject();
// 4、建立一个path与method的映射关系
HandlerMapping();
}

...
下面分别说明一下每个方法的流程,具体实现代码就不贴出来了,因为这几个方法在实现的过程中有许多相似的地方,都是基于反射得到实例,再根据不同的注解进行处理,具体可以参考最后的源码。

  • scanPackage("com.zjx")
    根据初始传入的包名进行扫描,这里会用到递归,扫描已经编译好的项目下所有的类,最后把所有的类名存放到一个List集合中。

  • instance()
    遍历扫描到的class文件,利用反射Class.forName()方法得到class对象,这里注意上一步得到的类名会有.class后缀,需要去掉。得到class类后先判断是否有注解,这里为了简单我们只关注了@MyService 、@MyController类注解,如果有该类注解,则继续利用反射方法创建实例,该实例作为值,同时得到对应注解上的参数值作为key(springMVC是将类名首字母小写作为key,这里我们简单起见),保存到一个Map容器中。
    关键代码如下:

    image.png

  • iocInject()
    进行依赖注入,上一步中IOC容器中已经存放所有我们所关心的实例。遍历容器,首先得到当前遍历到的类的实例和类class,根据类class来获取注解信息,如果当前类有@MyController注解,先获取类里面的所有属性,遍历所有属性,判断类属性上是否有自动装配(依赖注入)的注解@Autowired或Qualifier,如果有获得该注解的value,即IOC容器中的key,根据该key获得对应实例,最后给该属性设值(即注入)。注意点:类中属性一般是private,所以设值前需要field.setAccessible(true)获得许可,不然无法设值。
    关键代码如下:

    image.png

  • HandlerMapping
    请求路径url与方法的映射关系,主要是获得Controller上注解的值和方法上注解的值。同样还是遍历IOC容器,得到类class,判断是否有@MyController注解,如果有得到注解上的值,同时获取该类下所有的方法,遍历判断哪些方法有@MyRequestMapping注解,得到该注解上的值,和@MyController注解的值拼接成了请求路径url,最后将拼接成的url作为key,方法作为值存放到一个Map中。
    关键代码:

    image.png

3.3.处理请求

DispatchServlet处理请求的两个方法doGet、doPost,这里我们只需实现doPostdoGet直接在方法里面调用doPost方法。
处理请求的实现的基本流程是:

  • 获取到请求路径
  • 根据请求路径获得对应要执行的方法(请求路径和方法的映射我们在init方法中已经得到)
  • 取得控制类(controller)的实例
  • 获得方法执行的参数值
  • 调用方法的invoke,方法执行完成

这里最重要的一步就是解析执行方法上的参数,因为参数类型可能很多,直接if..else依次判断的话,会很繁琐,且代码不够优雅。这里我们采用策略模式,每一种类型的参数,都对应一种解析器,然后通过处理器执行得到我们想要测参数。关于什么是策略模式,可以参考:策略模式
这里我们定义了三种参数类型的解析器类:HttpServletRequestParamResolver、HttpServletResponsetParamResolver、 MyRequestParamResolver,由类名可以知道对应那种参数类型,这里不再赘述。这三个处理器都实现同一个解析器接口ParamResolver。解析器中都实现了两个方法:

  • boolean support(Class<?> type, int paramIndex, Method method)
    该方法是判断当前传进来的方法参数是否是该解析器对应的类型。
  • Object paramResolver(HttpServletRequest request, HttpServletResponse response, Class<?> type, int paramIndex, Method method)
    返回对应方法参数的值。

下面具体介绍一下这三个解析器类:

  • HttpServletRequestParamResolver
    判断参数是否是HttpServletRequest类型,就看是否实现了ServletRequest接口。paramResolver方法返回的还是原来的HttpServletRequest
    关键代码:
    image.png
  • HttpServletResponsetParamResolver
    和上面的一样,判断参数是否是HttpServletResponse类型,看是否实现了ServletResponse接口,也是原参数返回。
    关键代码:
    image.png
  • MyRequestParamResolver
    这里判断和上面就不同了,因为这是加在参数上的@MyRequestParam注解,可能有多个,所以每次先获得方法注解的参数类型,由方法method.getParameterAnnotations()获得,该方法获得的是一个二维数组,再根据传进来的参数下标,可以取得当前参数的注解类型,判断是否是@MyRequestParam的注解类型。是该注解,取出注解的值,执行request.getParameter(value)可以获得对应参数的值。
    关键代码:
    image.png

    image.png

    最后定义一个处理器类,来执行这些策略,返回方法执行的参数值数组。流程如下:
  • 拿到当前待执行的方法有哪些参数
  • 拿到所有的解析器类
  • 遍历所有参数应用对应解析器,得到参数值。
  • 返回最终结果数组
    关键代码:


    image.png

    image.png

四、总结和一些坑

具体的流程就是上面所写的,关键代码也都贴出来了,另外还有一些坑和不完善的地方做一下说明:

  • 浏览器会发起这么一个请求:/favicon.ico,浏览器会请求网站根目录的这个图标,如果网站根目录也没有这图标会产生404,因为我们没有对应的controller类来进行处理,这里遇到这个请求就直接return了。
  • 目前只参数类型只能是String,因为通过request.getParameter(value)获得的就是String,还没有做到再根据参数类型强转。
    这只是小小的实现了一把,很多细节还不完善,希望大家多多指正。
    附上源码:mySpringMvc
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容