手写 MVC 框架

回顾SpringMVC执行的大致原理,后续根据这个模仿收写自己的mvc框架

流程图

Spring MVC自定义框架 - 图5image.gif

写MVC框架之注解开发

1、创建项目

Spring MVC自定义框架 - 图7image.gifSpring MVC自定义框架 - 图9image.gifSpring MVC自定义框架 - 图11image.gifSpring MVC自定义框架 - 图13image.gifSpring MVC自定义框架 - 图15image.gif

2、删除build里面的内容,换为tomcat

  1. <properties>
  2. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  3. <maven.compiler.source>11</maven.compiler.source><!--jdk是11版本-->
  4. <maven.compiler.target>11</maven.compiler.target>
  5. </properties>
  6. <build>
  7. <plugins>
  8. <plugin>
  9. <groupId>org.apache.tomcat.maven</groupId>
  10. <artifactId>tomcat7-maven-plugin</artifactId>
  11. <version>2.2</version>
  12. <configuration>
  13. <port>8080</port>
  14. <path>/</path>
  15. </configuration>
  16. </plugin>
  17. </plugins>
  18. </build>

image.gif

3、添加文件夹并设置

Spring MVC自定义框架 - 图18image.gif

4、因为要写web项目,需要用到原生的api

<dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
    </dependency>

image.gif

5、创建框架类

Spring MVC自定义框架 - 图21image.gif

package com.shanglin.edu.mvcframework.servlet;
import javax.servlet.http.HttpServlet;
/**
 * @Description:
 * @Author: shanglin
 * @Date: 2020-08-13 19:58
 */
public class SlDispatcherServlet  extends HttpServlet {

}

image.gif

6、创建了SlDispatcherServlet类,那么在web.xml配置文件中就可以对它进行调用

<!--刚刚已经写了servlet,然后这里就可以进行调用了
  SlDispatcherServlet会去加载配置文件,配置文件会去扫描注解-->
  <servlet>
    <servlet-name>SlMVC</servlet-name>
    <servlet-class>com.shanglin.edu.mvcframework.SlDispatcherServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>SlMVC</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

image.gif

7、创建注解(需要写四个注解)

// 注解的使用范围
@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)——包



a、创建controller注解
Spring MVC自定义框架 - 图25image.gif

package com.shanglin.edu.mvcframework.annotation;
import java.lang.annotation.*;
@Documented  // 可以被java文档的编辑工具所读取
@Target(ElementType.TYPE) // 因为Controller是添加到类上的,所以是ElementType.TYPE
@Retention(RetentionPolicy.RUNTIME) // 指定它的生存周期为运行时(jvm加载到内存中也是有效的)
public @interface SlController {
    String value() default "";
}

image.gif
b、创建service注解
Spring MVC自定义框架 - 图28image.gif

package com.shanglin.edu.mvcframework.annotations;
import java.lang.annotation.*;
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SlService {
    String value() default "";
}

image.gif
c、创建RequestMapping注解
Spring MVC自定义框架 - 图31image.gif

package com.shanglin.edu.mvcframework.annotations;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE,ElementType.METHOD}) // 不仅可以添加到类上,还能添加到方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface SlRequestMapping {
    String value() default "";
}

image.gif

d、创建autowired注解
**
Spring MVC自定义框架 - 图34image.gif

package com.shanglin.edu.mvcframework.annotations;
import java.lang.annotation.*;
@Documented
@Target(ElementType.FIELD) // 添加到字段上
@Retention(RetentionPolicy.RUNTIME)
public @interface SlAutowired {
    String value() default  "";
}

image.gif

8、SlDispatcherServlet 要重新Servlet方法,并且初始化很多方法

package com.shanglin.edu.mvcframework.servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * @Description: 重写一下Servlet里面的方法,因为创建了注解,还需要对注解进行扫描
 *               所以还是有很多方法要进行初始化的,所以要init方法
 * @Author: shanglin
 * @Date: 2020-08-13 19:58
 */
public class SlDispatcherServlet  extends HttpServlet {
    @Override
    public void init(ServletConfig config) throws ServletException {
        // 加载配置文件
    }
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req,resp);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 处理请求
    }
}

image.gif

初始化里面具体的步骤

@Override
    public void init(ServletConfig config) throws ServletException {
        // 1、加载配置文件 springmvc.properties
        // 2、扫描相关的类、扫描注解
        // 3、初始化bean对象(实现ioc容器,基于注解)
        // 4、实现依赖注入
        // 5、构造一个HandlerMapping处理器映射器,将配置好的url和Method建立映射关系
        System.out.println("mvc 初始化完成");
        // 等待请求进入,处理请求
    }

image.gif

9、根据第八步的1,需要创建配置文件

Spring MVC自定义框架 - 图39image.gif

scanPackage=com.shanglin.demo

image.gif

10、在web.xml中指定SlDispatcherServlet类的初始化参数,为了将配置文件的值传入

Spring MVC自定义框架 - 图42image.gif

11、先将SlDispatcherServlet初始化需要的方法都创建出来

疑问一:scanPackage如何来。

@Override
    public void init(ServletConfig config) throws ServletException {
        // 1、加载配置文件 springmvc.properties
        String contextConfigLocation = config.getInitParameter("contextConfigLocation");
        doLoadConfig(contextConfigLocation);
        // 2、扫描相关的类、扫描注解
        doScan(properties.getProperty("scanPackage"));
        // 3、初始化bean对象(实现ioc容器,基于注解)
        doInstance();
        // 4、实现依赖注入
        doAutoWired();
        // 5、构造一个HandlerMapping处理器映射器,将配置好的url和Method建立映射关系
        initHandlerMapping();
        System.out.println("mvc 初始化完成");
        // 等待请求进入,处理请求
    }
    // 构造一个HandlerMapping处理器映射器
    private void initHandlerMapping() {
    }
    // 实现依赖注入
    private void doAutoWired() {
    }
    // ioc容器
    private void doInstance() {
    }
    // 扫描类
    private void doScan(String scanPackage) {
    }
    // 加载配置文件
    private void doLoadConfig(String contextConfigLocation) {
    }

image.gif

12、创建对应的包以及类,方便后续的解析

创建Controller类
Spring MVC自定义框架 - 图45
image.gif

@SlController
public class DemoController {
}

image.gif
创建Service接口
Spring MVC自定义框架 - 图48image.gif

public interface IDemoService {
    String get(String name);
}

image.gif创建Service接口的实现类
Spring MVC自定义框架 - 图51image.gif

@SlService
public class DemoServiceImpl implements IDemoService {
    @Override
    public String get(String name) {
        System.out.println("service 实现类中的name参数"+name);
        return name;
    }
}

image.gif

Controller需要使用service层,需要添加接口,通过注解注入到Controller层

@SlController
@SlRequestMapping("/demo")
public class DemoController {
    // 在controller层完成service层的调用,通过注解SlAutowired将对象注入进去
    @SlAutowired
    private IDemoService demoService;
    // 添加一个查询的方法,name等都传进来
    @SlRequestMapping("/query")
    public  String query(HttpServletRequest request, HttpServletResponse response,String name){
        return demoService.get(name);
    }
}

image.gif

13、初始化各个步骤的方法的实现如下

加载方法

// 加载配置文件
    private void doLoadConfig(String contextConfigLocation) {
        // 先将读取进来配置文件转为字节输入流
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        // 将字节输入流加载到内存中,并且存储到一个properties容器中
        try {
            properties.load(resourceAsStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

image.gif扫描方法(将磁盘文件扫描出来)需要深入!!!

根据扫描的包名,通过当前线程的路径,得到扫描包的物理路径
根据物理路径,读取出物理路径下的文件到file中
如果是空则继续递归,如果非空,有类,则包名+文件名去后缀,就是全限定类名

   // 将扫描到的类的全限定类名,缓存起来
    private List<String> classNames = new ArrayList<>();
    // 扫描类 传入的参数  package——> 在磁盘上以文件夹展示(File) com/shanglin/demo    
    // 将.转为/才可以去访问文件夹里面的文件 scanPackage=com.shanglin.demo
    // 包下面可能有子包,子包下面还可能有子包,所以肯定有用到递归的
    private void doScan(String scanPackage) {
        // 因为包的开头是以classpath开头,所以要从classpath去拿到磁盘路径。
         // getResource是空的时候拿到的就是classpath的路径
        // 磁盘路径找到了,还需要找到文件,文件是com/shanglin/demo形式的,所以要将.替换成/。
        String scanPackagePath= Thread.currentThread().getContextClassLoader().getResource("").getPath() + scanPackage.replaceAll("\\.", "/");
        // 借助File,读取路径,转为文件
        File pack = new File(scanPackagePath);
        // 读出的是一个数组,将文件读取出来
        File[] files = pack.listFiles();
        // 遍历数组
        for (File file : files) {
            // 如果存在子包
            if(file.isDirectory()){
                // 则继续递归
                doScan(scanPackage+"."+file.getName()); // 如: com.shanglin.demo.controller
            }else if(file.getName().endsWith(".class")){
                 // 如果是.class结尾就是java类了,那么下面就是获取它的权限定类名  包名+类名  类名的后缀去掉
                String className = scanPackage + "." + file.getName().replaceAll(".class", "");
                classNames.add(className);
            }
        }
    }

image.gif

实例化对象,存入id和对象的映射关系

全限定类名,通过反射创建对象(返回一个类) ——》类的注解从类上判断
判断对应的注解,取出对应的value做id,如果没有则用类名小写做id
然后将id,反射回来的对象,一起存入容器中(什么样的beanName对应什么样的Object)
如果该类还存在接口,将其所有接口也存入容器中( 该类内注入的接口)

    // 需要一个容器,将他们保存下来
    private Map<String,Object> iocMap= new HashMap<String,Object>();
    // ioc容器
    // 基于classNames缓存的类的全限定类名,以及反射技术,完成对象创建和管理
    private void doInstance() {
        try {
            if(classNames.size() == 0) return;
            for (int i = 0; i < classNames.size(); i++) {
                // className好比com.shanglin.demo.controller.DemoController
                String className = classNames.get(i); 
                // 将获取到的全限定类名,通过反射技术,创建对象
                // JVM查找并加载指定的类,并且返回的是一个类
                Class<?> aClass = Class.forName(className);
                // 区分Controller,区分service
                if(aClass.isAnnotationPresent(SlController.class)){
                    // controller的id此处不做过多处理,不取value,只拿类的首字母小写作为id,保存到ioc中
                    //获取类的简写名称,也就是类名,如DemoController
                    String simpleName = aClass.getSimpleName(); 
                    // 将类名转为首字母小写
                    String lowerFirstSimpleName = lowerFirst(simpleName);
                    // 通过反射创建新的类,默认调用无参的构造函数,而且被调用的构造函数是可见的
                    // 使用类加载机制,通过newInstance调用无参构造函数初始化一个类,生成一个实例
                    Object o = aClass.newInstance();
                    // 把对象和简写后的名字放入ioc容器进行保存(beanName,Object)
                    iocMap.put(lowerFirstSimpleName,o);
                    // 到此完成了Controller对象的实例化和管理
                }else if(aClass.isAnnotationPresent(SlService.class)){ // 判断service层
                    // 先获取注解
                    SlService annotation = aClass.getAnnotation(SlService.class);
                    // 获取注解里面的value值,他的值就是id,也就是beanName
                    String beanName = annotation.value();
                    // 如果不为空就是指定了id,就以指定的id为主
                    if(!"".equals(beanName.trim())){
                        iocMap.put(beanName,aClass.newInstance()); // 实体化一个对象
                    }else{
                        // 如果没有指定,就以类名首字母小写
                        beanName = lowerFirst(aClass.getSimpleName());
                        iocMap.put(beanName,aClass.newInstance());
                    }

                    // service层往往是有接口的,面向接口开发,如果有接口,此时再以接口名为id,
                    // 放入一份对象到iocMap中,便于后期根据接口类型注入
                    Class<?>[] interfaces = aClass.getInterfaces();
                    // 遍历接口
                    for (int j = 0; j < interfaces.length; j++) {
                        Class<?> anInterface = interfaces[j];
                        // 以接口的全限定类名作为id放入iocMap,实例化一个对象
                        iocMap.put(anInterface.getName(),aClass.newInstance());
                    }
                }else{
                    // 如果还有其他的继续即可
                    continue;
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

image.gif

依赖注入(需要深入!!!)

遍历映射集合,通过对象获取其类型,通过类型获取其字段信息 ——》字段上的注解,从字段上判断
遍历其字段信息,看是否有对应的注解
如果有对应注解则取此value,如果无value,就取字段名
给相关对象,绑定实例

    // 实现(基于注解的)依赖注入
    // 先查看有没有SlAutowired这个注解,如果有指向的是哪个进行注入
    private void doAutoWired() {
        // 如果为空的则直接返回
        if(iocMap.isEmpty()){return;}
        // 如果不为空,则为有对象,则要进行依赖注入处理。不知道有没有注入,先要遍历容器
        // 遍历iocMap中所有对象,查看对象中的字段,是否有@SlAutowired注解,
        //如果有需要维护依赖注入关系
        for (Map.Entry<String,Object> entry: iocMap.entrySet()){
            // 获取bean对象中的字段信息
            // .getValue()拿到对象Object,.getClass()拿到类型,.getDeclaredFields()拿到字段信息
            Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();
            // 遍历判断处理
            for (int i = 0; i < declaredFields.length; i++) {
                // 遍历这个字段
                Field declaredField = declaredFields[i]; //@SlAutowired  private IDemoService demoService;
                // 判断有没有SlAutowired这个注解,如果不存在注解
                if(!declaredField.isAnnotationPresent(SlAutowired.class)){
                    continue;
                }
                // 如果有SlAutowired这个注解 // 就获取到这个注解
                SlAutowired annotation = declaredField.getAnnotation(SlAutowired.class);
                // 再获取注解的value ,这个value就是所依赖bean的id
                String beanName = annotation.value(); // 需要注入bean的id
                if("".equals(beanName.trim())){
                    // 没有配置具体的bean id,那就需要根据当前字段类型注入(接口注入) IDemoService
                    // declaredField.getType() 就是IDemoService
                    beanName = declaredField.getType().getName();
                }
                // 开启赋值
                declaredField.setAccessible(true);
                try {
                    // 给那个对象entry.getValue(),传递iocMap.get(beanName)
                    declaredField.set(entry.getValue(),iocMap.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
       // 到此,依赖注入维护完毕
    }

映射关系

从ioc实例化容器中取出全限定类名,通过全限定类名扫描所有注解或方法
判断有要处理的参数注解,取出其value值,
同样的方法,取出所有方法的上面对应注解的值
将类上面的值+方法的值就是=》要访问的方法路径

    // 添加一个handlerMapping来保存方法
    private Map<String,Object> handlerMapping=new HashMap<String ,Object>();
    // 构造一个HandlerMapping处理器映射器
    // 因为全限定类名有了, 通过全限定类名获取Class类型,通过Class类型获取其所有方法,
    //有了方法就可以扫描方法上面的注解。Controller类上面的注解也可以扫描到
    // 然后获取注解里面的参数,将两个参数拼接起来就OK了。
    // 目的:就是将Url和method建立关联
    private void initHandlerMapping() {
        // 先判断容器是否为空
        if(iocMap.isEmpty()){return;}
        // 如果不为空,扫描ioc容器当中对象的所有的方法,看看扫描到的方法上是否有注解RequestMapping,
        // 如果有再找一下它对应url,然后建立他们的映射
        for (Map.Entry<String,Object> entry: iocMap.entrySet()){
            // 获取iocMap中当前遍历的对象的class类型 。
            // entry.getValue()得到对象, .getClass()得到类型
            Class<?> aClass = entry.getValue().getClass();
            // 只处理Controller层。所以如果不包含SlController注解的,就不处理
            if(!aClass.isAnnotationPresent(SlController.class)){ continue; }
            // 如果Controller层包含SlController注解,则进入处理。
            //要判断类是否有第一个根RequestMapping。
            String beanUrl = "";
            if(aClass.isAnnotationPresent(SlRequestMapping.class)){
                // 取出这个注解
                SlRequestMapping annotation = aClass.getAnnotation(SlRequestMapping.class);
                // 取出注解里面的参数
                // 等同于取出了 Controller 层的第一个   /demo
                beanUrl = annotation.value(); 
            }
            // 前缀有了,还需要获取方法,获取方法上面的注解,获取注解里面的value
            Method[] methods = aClass.getMethods();
            // 遍历方法
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                // 对方法进行判断,如果method方法中没有标识SlRequestMapping就不处理
                if(!method.isAnnotationPresent(SlRequestMapping.class)){ continue; }
                // 如果标识有就处理
                SlRequestMapping annotation = method.getAnnotation(SlRequestMapping.class);
                String mothodUrl = annotation.value(); // 将方法的参数解析出来   如: /query
                // 然后将类的参数路径 + 方法的参数路径 =  才是访问的参数路径
                String Url= beanUrl+ mothodUrl;
                // 建立url和method直接的映射关系(map缓存起来)
                handlerMapping.put(Url,method);
            }
        }
    }

image.gif

14、需要进行两项配置

1、需要把web.xml中的classpath*: 去掉,因为读取的时候,并没有对它进行解析,而是直接读取文件了。
2、添加使用范围

<dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
      <!--添加使用范围,因为在服务器中会直接使用Tomcat,而不使用它
       如果不添加范围,会引起冲突的-->
        <scope>provided</scope>
    </dependency>

image.gif
到此,初始化开发基本完成
**

15、访问测试

Spring MVC自定义框架 - 图60image.gifSpring MVC自定义框架 - 图62image.gif

16、定义一个类来存储更多的东西

// 1、不仅仅要存储url和method之间的对应关系。而且需要存储更多的东西,所以定义一个类来存储
// 2、因为Controller下面的方法都叫handler,所以定义个handler实体
Spring MVC自定义框架 - 图64image.gif

package com.shanglin.demo.pojo;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
 * @Description:    封装handler方法相关的信息
 *                 1、要存储url和method之间的对应关系。而且还需要存储更多的东西,所以定义一个类来存储
 *                 2、因为Controller下面的方法都叫handler,所以定义个handler实体
 * @Author: shanglin
 * @Date: 2020-08-15 9:31
 */
public class Handler {
    // 因为method方法在controller类当中,controller对象当中。
     //调用是传入对象的。 method.invoke(obj,); 这里调用也需要对象
    private  Object controller;
    // 就是method方法
    private Method method;
    // spring中的url是支持正则的(和它匹配的url)
    private Pattern pattern;
    //存参数顺序,因为每个方法都要传参数并且有顺序,为了进行参数绑定,
    //key是参数名,value代表是第几个参数<name,2>
    private Map<String,Integer> parmIndexMapping;
    public Handler(Object controller, Method method, Pattern pattern) {
        this.controller = controller;
        this.method = method;
        this.pattern = pattern;
        this.parmIndexMapping = new HashMap<>();
    }
    public Object getController() {
        return controller;
    }
    public void setController(Object controller) {
        this.controller = controller;
    }
    public Method getMethod() {
        return method;
    }
    public void setMethod(Method method) {
        this.method = method;
    }
    public Pattern getPattern() {
        return pattern;
    }
    public void setPattern(Pattern pattern) {
        this.pattern = pattern;
    }
    public Map<String, Integer> getParmIndexMapping() {
        return parmIndexMapping;
    }
    public void setParmIndexMapping(Map<String, Integer> parmIndexMapping) {
        this.parmIndexMapping = parmIndexMapping;
    }
}

image.gif

17、在initHandlerMapping()中存的不仅仅是映射关系,还需整个handler方法

(需要深入)

private List<Handler> handlerMapping=new ArrayList<>();
    // 构造一个HandlerMapping处理器映射器
    // 因为全限定类名有了, 通过全限定类名获取Class类型,通过Class类型获取所有方法,有了方法就可以扫描方法上面的注解
    // 然后获取注解里面的参数,将两个参数拼接起来就OK了。
    // 目的:就是将Url和method建立关联
    private void initHandlerMapping() {
        // 先判断容器是否为空
        if(iocMap.isEmpty()){return;}
        // 如果不为空,扫描ioc容器当中对象的所有的方法,看看扫描到的方法上是否有注解RequestMapping,
        // 如果有再找一下它对应url,然后建立他们的映射
        for (Map.Entry<String,Object> entry: iocMap.entrySet()){
            // 获取iocMap中当前遍历的对象的class类型 。entry.getValue()得到对象,.getClass()得到类型
            Class<?> aClass = entry.getValue().getClass();
            // 只处理Controller层。所以如果不包含SlController注解的,就不处理
            if(!aClass.isAnnotationPresent(SlController.class)){ continue; }
            // 如果Controller层包含SlController注解,则进入处理。要判断类是否有第一个根RequestMapping。
            String beanUrl = "";
            if(aClass.isAnnotationPresent(SlRequestMapping.class)){
                // 取出这个注解
                SlRequestMapping annotation = aClass.getAnnotation(SlRequestMapping.class);
                // 取出注解里面的参数
                beanUrl = annotation.value(); // 等同于取出了 Controller 层的第一个   /demo
            }
            // 还需要获取方法
            Method[] methods = aClass.getMethods();
            // 遍历方法
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                // 对方法进行判断,如果method方法中没有标识SlRequestMapping就不处理
                if(!method.isAnnotationPresent(SlRequestMapping.class)){ continue; }

                // 如果标识有就处理
                SlRequestMapping annotation = method.getAnnotation(SlRequestMapping.class);
                String mothodUrl = annotation.value(); // 将方法的参数解析出来   如: /query

                // 然后将类的参数路径 + 方法的参数路径 =  才是访问的参数路径
                String Url= beanUrl+ mothodUrl;

                // 将method的所有信息以及url封装为一个handler,然后存到list里面
                Handler handler = new Handler(entry.getValue(), method, Pattern.compile(Url));

                // 计算方法的参数位置信息 query(HttpServletRequest request, HttpServletResponse response,String name)
                Parameter[] parameters = method.getParameters(); // 先要获取method方法所有参数
                for (int j = 0; j < parameters.length; j++) {
                    Parameter parameter = parameters[j]; // 将参数取出
                    if(parameter.getType() == HttpServletRequest.class|| parameter.getType() == HttpServletResponse.class){

                        // 如果是 Request和response对象,那么参数名称写HttpServletRequest或者HttpServletResponse
                        handler.getParmIndexMapping().put(parameter.getType().getSimpleName(),j);
                    }else{
                        handler.getParmIndexMapping().put(parameter.getName(),j);
                    }
                }
                // 建立url和method直接的映射关系(map缓存起来)
                handlerMapping.add(handler);
            }
        }
    }

image.gif

18、完善post请求

pop.xml中需要添加依赖

<dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.9</version>
    </dependency>

image.gif

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 处理请求:根据url,找到对应的Method方法,进行调用
        // 1、先获取uri
        //String requestURI = req.getRequestURI();
        // 2、获取到一个反射方法
        //Method method = (Method) handlerMapping.get(requestURI);
        // 3、反射调用、需要传入对象,需要传入参数,此处无法完成调用,因为没有把对象缓存起来,也没有参数
        //    所以还需要改造initHandlerMapping()。因为method的调用方法如:  method.invoke(对象,参数)
        //  method.invoke()
        // 根据uri获取到能够处理当前请求的handler(从handlerMapping中获取 它其实也就是一个list)
        Handler handler = getHandler(req);
        // 如果handler是空的反应404
        if(handler == null){
            resp.getWriter().write("404 not found");
            return;
        }
        // 如果不为空。需要绑定参数
        // 先要获取所有参数类型数组,这个数组长度就是后面要传入的args数组的长度
        Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();
        // 根据数组长度创建一个新数组,(参数数组,是要传入反射调用的)
        Object[] paraValues = new Object[parameterTypes.length];
        // 然后需要向参数数组中填值,而且还需要保证顺序和方法中的一致
        // 参数是前端传递过来的,所以需要获取所有的参数
        Map<String, String[]> parameterMap = req.getParameterMap();
        // 对所有的参数进行遍历(因为同一个参数名可能的值就有多个)
        for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
            // name=1&name=2  name[1,2] 将参数以指定的字符拼接起来
            String value = StringUtils.join(param.getValue(), ",");
            // 如果这些参数和方法的参数匹配上,就填充数据
            // 如果不包含某个参数,则继续循环
            if(!handler.getParmIndexMapping().containsKey(param.getKey())){ continue;}
            // 方法形参确实有该参数名,找到他的索引位置,对应的把参数值放入到paraValues
            // 因为parmIndexMapping存的就是key=参数名和value=索引位置,
            Integer index = handler.getParmIndexMapping().get(param.getKey());
            // 把前台传递过来的参数填充到对应的位置去
            paraValues[index]=value;
            // 如果是Request  response
            Integer reqIndex = handler.getParmIndexMapping().get(HttpServletRequest.class.getSimpleName());
            paraValues[reqIndex]=req;
            Integer respIndex = handler.getParmIndexMapping().get(HttpServletResponse.class.getSimpleName());
            paraValues[respIndex]=resp;
            // 到此paraValues参数的数组拼装完成
            // 最终调用handler的method属性
            try {
                handler.getMethod().invoke(handler.getController(),paraValues);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
    // 单独创建一个方法
    private Handler getHandler(HttpServletRequest request) {
        // 先判断请求是否为空
        if (handlerMapping.isEmpty()) {
            return null;
        }
        // 如果不为空,获取uri
        String url = request.getRequestURI();
        // 遍历handlerMapping集合,取出里面的url进行对比
        for (Handler handler : handlerMapping) {
            // 调用匹配方法看是否匹配url
            Matcher matcher = handler.getPattern().matcher(url);
            // 如果不匹配则继续循环下一个
            if (!matcher.matches()) {
                continue;
            }
            // 如果匹配就返回
            return handler;
        }
        return null;
    }

image.gif

19、修改编译的插件

<!--编译插件定义编译细节,当编译器编译的时候把真实的形参赋上去-->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.6.1</version>
        <configuration>
          <source>11</source>
          <target>11</target>
          <encoding>utf-8</encoding>
          <!--编译器编译的时候记录下形参的真实名称-->
          <compilerArgs>
            <arg>-parameters</arg>
          </compilerArgs>
        </configuration>
      </plugin>

image.gif

20、还需要重新编译一次,&测试

Spring MVC自定义框架 - 图71image.gif
Spring MVC自定义框架 - 图73image.gif

Spring MVC自定义框架 - 图75image.gif
参数能正常过去,而且没有异常。到此手写mvc已经OK