手写SpringMvc

一、简介

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

二、项目搭建

在IDEA上用MAVEN创建一个webApp项目:
手写SpringMvc_第1张图片
原来的springMVC中,最重要的一个类就是DispatchServlet即前端请求控制器,我们自定义自己的DispatchServlet,继承HttpServlet。
因为要继承HttpServlet,利用pom引入servlet-api的jar包:

    **
      javax.servlet
      servlet-api
      3.0-alpha-1
    **

继承HttpServelet新建DispatchServlet类,重写doGet、doPost、和init方法。在init方法中实现包扫描、IOC容器初始化等一系列操作。这里面操作会在tomcat加载项目后初始化完成。
手写SpringMvc_第2张图片
同时在web.xml配置DispatchServlet:

  
    DispatchServlet
    com.zjx.myspringmvc.servelet.DispatchServlet
    0
  

  DispatchServlet
  /

三、流程解读

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容器中。
    关键代码如下:
    手写SpringMvc_第3张图片

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

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

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
    关键代码:
    手写SpringMvc_第6张图片
  • HttpServletResponsetParamResolver
    和上面的一样,判断参数是否是HttpServletResponse类型,看是否实现了ServletResponse接口,也是原参数返回。
    关键代码:
    手写SpringMvc_第7张图片
  • MyRequestParamResolver
    这里判断和上面就不同了,因为这是加在参数上的 @MyRequestParam注解,可能有多个,所以每次先获得方法注解的参数类型,由方法 method.getParameterAnnotations() 获得,该方法获得的是一个二维数组,再根据传进来的参数下标,可以取得当前参数的注解类型,判断是否是 @MyRequestParam的注解类型。是该注解,取出注解的值,执行 request.getParameter(value) 可以获得对应参数的值。
    关键代码:
    手写SpringMvc_第8张图片
    手写SpringMvc_第9张图片
    最后定义一个处理器类,来执行这些策略,返回方法执行的参数值数组。流程如下:
  • 拿到当前待执行的方法有哪些参数
  • 拿到所有的解析器类
  • 遍历所有参数应用对应解析器,得到参数值。
  • 返回最终结果数组
    关键代码:
    手写SpringMvc_第10张图片
    手写SpringMvc_第11张图片

四、总结和一些坑

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

  • 浏览器会发起这么一个请求:/favicon.ico,浏览器会请求网站根目录的这个图标,如果网站根目录也没有这图标会产生404,因为我们没有对应的controller类来进行处理,这里遇到这个请求就直接return了。
  • 目前只参数类型只能是String,因为通过 request.getParameter(value) 获得的就是String,还没有做到再根据参数类型强转。
    这只是小小的实现了一把,很多细节还不完善,希望大家多多指正。
    附上源码:mySpringMvc

你可能感兴趣的:(spring,java,springMvc,手写springMvc,java)