【学习笔记】手写一个简单的 Spring MVC

目录

[一、什么是Spring MVC ?](#一、什么是Spring MVC ?)

[Spring 和 Spring MVC 的区别?](#Spring 和 Spring MVC 的区别?)

[Spring MVC 的运行流程?](#Spring MVC 的运行流程?)

二、实现步骤

[1. DispatcherServlet](#1. DispatcherServlet)

[1. 创建一个中央分发器](#1. 创建一个中央分发器)

拦截所有请求

测试

[2. 接管 IOC 容器](#2. 接管 IOC 容器)

[1. 创建配置文件](#1. 创建配置文件)

[2. 修改 web.xml 配置文件](#2. 修改 web.xml 配置文件)

[3. 启动 IOC 容器](#3. 启动 IOC 容器)

[2. HandlerMapping](#2. HandlerMapping)

[1. 添加映射](#1. 添加映射)

[1. 创建 RequestMapping 注解](#1. 创建 RequestMapping 注解)

[2. 创建映射Bean](#2. 创建映射Bean)

[3. 使用 RequestMapping 注解](#3. 使用 RequestMapping 注解)

[4. 添加映射](#4. 添加映射)

[2. 处理映射](#2. 处理映射)

[1. 创建 ResponseBody 注解](#1. 创建 ResponseBody 注解)

[2. 创建一个 HTML 文件](#2. 创建一个 HTML 文件)

[3. 使用 ResponseBody 注解](#3. 使用 ResponseBody 注解)

[4. 处理映射](#4. 处理映射)

[3. 测试](#3. 测试)


一、什么是Spring MVC ?

Spring MVC 是 Spring 的模块之一,Spring MVC 实现了MVC 的设计思想,并继承了 Servlet API 的WEB 框架。当用户在游览器地址栏上输入 url 后,Spring MVC就可以处理用户的请求

Spring 和 Spring MVC 的区别?

Spring 是一个框架,这个框架由不同的模块组成,其中一个模块 就是Spring MVC,Spring 核心是IOC 控制反转,IOC 容器负责对象的创建和依赖注入。

Spring MVC 是基于 MVC 设计来开发web 应用,Spring MVC 将前端发送的请求分发给适当的控制器 Controller,然后根据结果选择合适的视图进行渲染返回

Spring MVC 的运行流程?

  1. 用户发送HTTP请求
  2. 请求到达服务器后,Spring MVC 的中央分发器拦截请求
  3. 中央分发器根据 请求的路径找到对应的 HandlerMapping,确定由哪个 Controller 控制器处理
  4. HandlerMaping 根据请求信息映射到对应的 Controller ,然后返回给中央分发器
  5. 中央分发器把请求交给对应的Controller 控制器,Controller 是Spring MVC的一个组件,它负责处理请求以及响应结果
  6. Controller 控制器调用合适的业务层或 Mapper 层获取数据
  7. Controller 把数据封装成一个ModelAndView对象,然后返回给中央分发器
  8. 中央分发器把ModelAndView对象传给 ViewResolver
  9. ViewResolver 根据视图名称解析出一个 View 对象,然后返回给中央分发器
  10. 中央分发器把 view 对象渲染出来返回给客户端

在 手写 Spring IOC 的基础上再进行扩展,手写一个 Spring MVC

二、实现步骤

1. DispatcherServlet

1. 创建一个中央分发器

java 复制代码
package com.shao.MVC;

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;


public class DispatcherServlet extends HttpServlet {

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("dispatcherServlet 初始化");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("dispatcherServlet 开始执行任务了");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("dispatcherServlet doPost");
    }

}
拦截所有请求

在 Tomcat 的 web.xml 配置文件中配置自定义的中央分发器,拦截所有请求

XML 复制代码
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
    <display-name>Archetype Created Web Application</display-name>
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>com.shao.MVC.DispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>
测试

在地址栏随便输入一个请求,中央分发器会进行拦截,然后执行初始化和相关方法,初始化只会执行一次

2. 接管 IOC 容器

在 手写 spring IOC 的时候,为了方便测试是在 Servlet 的 doGet 方法中初始化 IOC 容器,现在改为在中央分发器初始化的时候启动 IOC 容器

1. 创建配置文件

在配置文件中配置扫描包路径,然后启动中央分发器的时候把配置文件传过去

2. 修改 web.xml 配置文件
3. 启动 IOC 容器

在中央分发器的初始化方法中启动 IOC 容器

java 复制代码
package com.shao.MVC;

import com.shao.IOC.ApplicationContext;

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;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Properties;


public class DispatcherServlet extends HttpServlet {

    // 存储 IOC 容器
    private ApplicationContext applicationContext;
    private Properties Prop = new Properties();

    /**
     * 初始化
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("dispatcherServlet 初始化");
        // 获取传过来的配置文件
        String configLocation = config.getInitParameter("contextConfigLocation");
        String fileName = configLocation.replace("classpath:", "");
        // 调用 loadConfig 方法,传入配置文件名,返回扫描包路径
        String packagePath = loadConfig(fileName);

        try {
            // 启动 IOC 容器
            applicationContext = new ApplicationContext(packagePath);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("dispatcherServlet 开始执行任务了");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("dispatcherServlet doPost");
    }

    /**
     * 加载配置文件,解析配置文件,返回扫描包路径
     */
    public String loadConfig(String path) {

        // 以流的方式加载配置文件
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(path);

        String basePackage = "";
        try {
            // 解析配置文件中的属性,以键值对的方式存储到 Prop 中
            Prop.load(resourceAsStream);
            basePackage = Prop.getProperty("basePackage");

        } catch (IOException e) {
            e.printStackTrace();
        }
        return basePackage;
    }

}

2. HandlerMapping

HandlerMapping 根据注解的值映射到对应的 Controller 和方法

将 url 和 controller 里面的方法进行映射,存到 HashMap 中,key 是 url ,value 是一个对象,这个对象有 url 对应的 controller 对象和对应的方法

1. 添加映射

1. 创建 RequestMapping 注解
2. 创建映射Bean
java 复制代码
package com.shao.MVC;


import java.lang.reflect.Method;

public class RequestMappingBean {
    /**
     * controller 对象
     */
    private Object controller;
    /**
     * controller 的方法
     */
    private Method method;

    public RequestMappingBean(Object controller, Method method) {
        this.controller = controller;
        this.method = method;
    }

    /**
     * 获取
     *
     * @return controller
     */
    public Object getController() {
        return controller;
    }

    /**
     * 设置
     *
     * @param controller
     */
    public void setController(Object controller) {
        this.controller = controller;
    }

    /**
     * 获取
     *
     * @return method
     */
    public Method getMethod() {
        return method;
    }

    /**
     * 设置
     *
     * @param method
     */
    public void setMethod(Method method) {
        this.method = method;
    }

    public String toString() {
        return "RequestMappingBean{controller = " + controller + ", method = " + method + "}";
    }
}
3. 使用 RequestMapping 注解
4. 添加映射
java 复制代码
package com.shao.MVC;

import com.shao.Annotation.Controller;
import com.shao.Annotation.RequestMapping;
import com.shao.IOC.ApplicationContext;

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;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Properties;


public class DispatcherServlet extends HttpServlet {

    // 存储 IOC 容器
    private ApplicationContext applicationContext;
    private Properties Prop = new Properties();

    private HashMap<String, RequestMappingBean> mappingMap = new HashMap<>();


    /**
     * 初始化
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("dispatcherServlet 初始化");
        // 获取传过来的配置文件
        String configLocation = config.getInitParameter("contextConfigLocation");
        String fileName = configLocation.replace("classpath:", "");
        // 调用 loadConfig 方法,传入配置文件名,返回扫描包路径
        String packagePath = loadConfig(fileName);

        try {
            // 启动 IOC 容器
            applicationContext = new ApplicationContext(packagePath);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        // 添加映射
        AddRequestMapping();
        
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("dispatcherServlet 开始执行任务了");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("dispatcherServlet doPost");
    }

    /**
     * 加载配置文件,解析配置文件,返回扫描包路径
     */
    public String loadConfig(String path) {

        // 以流的方式加载配置文件
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(path);

        String basePackage = "";
        try {
            // 解析配置文件中的属性,以键值对的方式存储到 Prop 中
            Prop.load(resourceAsStream);
            basePackage = Prop.getProperty("basePackage");

        } catch (IOException e) {
            e.printStackTrace();
        }
        return basePackage;
    }

    /**
     * 添加映射
     * 1. 从 IOC 容器中获取所有带 RequestMapping 注解的 Controller 对象
     * 2. 获取 Controller 对象中带 RequestMapping 注解的方法
     * 3. 将 Controller 对象和方法封装为 RequestMappingBean 对象
     * 4. 构建映射关系,key 是 url,value 是 映射Bean 对象,包括 Controller 对象和方法
     */

    public void AddRequestMapping() {
        // 获取 IOC 容器的 Bean Map
        HashMap<String, Object> beanMap = applicationContext.getBeanMap();

        for (Object bean : beanMap.values()) {
            // 获取 bean 的 Class 对象
            Class<?> aClass = bean.getClass();

            // 判断是否有 @Controller 注解
            if (!aClass.isAnnotationPresent(Controller.class)) {
                continue;
            }

            // 判断是否有 @RequestMapping 注解
            if (!aClass.isAnnotationPresent(RequestMapping.class)) {
                continue;
            }

            // 获取类的 @RequestMapping 注解的值
            String basePath = aClass.getAnnotation(RequestMapping.class).value();

            // 获取 Controller 对象中的所有方法
            Method[] methods = aClass.getDeclaredMethods();
            for (Method method : methods) {

                // 判断方法上有没有带 @RequestMapping 注解
                if (!method.isAnnotationPresent(RequestMapping.class)) {
                    continue;
                }
                String path = method.getAnnotation(RequestMapping.class).value();

                // 封装为 映射Bean 对象
                RequestMappingBean mappingBean = new RequestMappingBean(bean, method);

                // 构建映射,添加到 Map 中
                mappingMap.put(basePath + path, mappingBean);
            }
        }

        System.out.println("映射添加完成");
        System.out.println(mappingMap);

    }

}

2. 处理映射

1. 创建 ResponseBody 注解
2. 创建一个 HTML 文件
3. 使用 ResponseBody 注解
4. 处理映射

为了方便测试,只处理了 GET 方法的映射

java 复制代码
package com.shao.MVC;

import com.alibaba.fastjson2.JSON;
import com.shao.Annotation.Controller;
import com.shao.Annotation.RequestMapping;
import com.shao.Annotation.ResponseBody;
import com.shao.IOC.ApplicationContext;

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;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Properties;


public class DispatcherServlet extends HttpServlet {

    // 存储 IOC 容器
    private ApplicationContext applicationContext;
    private Properties Prop = new Properties();

    private HashMap<String, RequestMappingBean> mappingMap = new HashMap<>();


    /**
     * 初始化
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("dispatcherServlet 初始化");
        // 获取传过来的配置文件
        String configLocation = config.getInitParameter("contextConfigLocation");
        String fileName = configLocation.replace("classpath:", "");
        // 调用 loadConfig 方法,传入配置文件名,返回扫描包路径
        String packagePath = loadConfig(fileName);

        try {
            // 启动 IOC 容器
            applicationContext = new ApplicationContext(packagePath);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        // 添加映射
        AddRequestMapping();

    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("dispatcherServlet 开始执行任务了");

        // 处理请求
        HandlerMapping(req, resp);

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("dispatcherServlet doPost");
    }

    /**
     * 加载配置文件,解析配置文件,返回扫描包路径
     */
    public String loadConfig(String path) {

        // 以流的方式加载配置文件
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(path);

        String basePackage = "";
        try {
            // 解析配置文件中的属性,以键值对的方式存储到 Prop 中
            Prop.load(resourceAsStream);
            basePackage = Prop.getProperty("basePackage");

        } catch (IOException e) {
            e.printStackTrace();
        }
        return basePackage;
    }

    /**
     * 添加映射
     * 1. 从 IOC 容器中获取所有带 RequestMapping 注解的 Controller 对象
     * 2. 获取 Controller 对象中带 RequestMapping 注解的方法
     * 3. 将 Controller 对象和方法封装为 RequestMappingBean 对象
     * 4. 构建映射关系,key 是 url,value 是 映射Bean 对象,包括 Controller 对象和方法
     */

    public void AddRequestMapping() {
        // 获取 IOC 容器的 Bean Map
        HashMap<String, Object> beanMap = applicationContext.getBeanMap();

        for (Object bean : beanMap.values()) {
            // 获取 bean 的 Class 对象
            Class<?> aClass = bean.getClass();

            // 判断是否有 @Controller 注解
            if (!aClass.isAnnotationPresent(Controller.class)) {
                continue;
            }

            // 判断是否有 @RequestMapping 注解
            if (!aClass.isAnnotationPresent(RequestMapping.class)) {
                continue;
            }

            // 获取类的 @RequestMapping 注解的值
            String basePath = aClass.getAnnotation(RequestMapping.class).value();

            // 获取 Controller 对象中的所有方法
            Method[] methods = aClass.getDeclaredMethods();
            for (Method method : methods) {

                // 判断方法上有没有带 @RequestMapping 注解
                if (!method.isAnnotationPresent(RequestMapping.class)) {
                    continue;
                }
                String path = method.getAnnotation(RequestMapping.class).value();

                // 封装为 映射Bean 对象
                RequestMappingBean mappingBean = new RequestMappingBean(bean, method);

                // 构建映射,添加到 Map 中
                mappingMap.put(basePath + path, mappingBean);
            }
        }

        System.out.println("映射添加完成");
        System.out.println(mappingMap);

    }

    /**
     * 处理请求,根据 url 找到对应的映射对象,调用对应的方法,返回结果
     */
    public void HandlerMapping(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        // 获取请求的路径,这个请求路径中有项目名称
        String requestURI = req.getRequestURI();
        // 获取项目名
        String contextPath = req.getContextPath();
        // 去掉项目名
        String url = requestURI.replace(contextPath, "");

        // 获取 url 对应的映射对象
        RequestMappingBean mappingBean = mappingMap.get(url);
        Object controller = mappingBean.getController();
        Method method = mappingBean.getMethod();
        Object res = null;
        try {
            // 调用映射对象中的方法
            res = method.invoke(controller);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 判断方法是否有返回内容
        if (res == null) {
            return;
        }

        // 判断方法是否有 @ResponseBody 注解
        if (method.isAnnotationPresent(ResponseBody.class)) {
            resp.setContentType("application/json;charset=utf-8");

            // 响应数据
            resp.getWriter().write(JSON.toJSONString(res));

        } else {

            // 获取编译后的项目根目录
            String path = this.getClass().getClassLoader().getResource("../../").getPath();

            // 路径前面有一个 /,比如: /D:/xxx,需要去掉,然后拼接静态资源名称
            String filePath = path.substring(1) + res;

            try {
                // 解码,如果路径有空格或者中文,会出现 16 进制的字符
                filePath = URLDecoder.decode(filePath, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }

            // 获取静态资源内容
            byte[] fileContents = StaticResourceHandler.getFileContents(filePath);
            // 获取文件媒体类型
            String mimeType = StaticResourceHandler.getFileMimeType(filePath);
            resp.setContentType(mimeType + ";charset=utf-8");

            // 响应内容
            resp.getWriter().write(new String(fileContents));
        }
    }
}

3. 测试

如果显示乱码可以添加以下命令试一下

java 复制代码
-Dfile.encoding=UTF-8
相关推荐
北极无雪11 分钟前
Spring源码学习(拓展篇):SpringMVC中的异常处理
java·开发语言·数据库·学习·spring·servlet
小码狐38 分钟前
并查集【数据结构与算法】【C语言版-笔记】
数据结构·笔记·并查集·c语言版
问道飞鱼1 小时前
每日学习一个数据结构-默克尔树(Merkle Tree)
数据结构·学习·默克尔树
Amagi.1 小时前
Spring中Bean的作用域
java·后端·spring
William_Edmund1 小时前
Python 语言学习——应用1.2 数字图像处理(第二节,变换)
人工智能·学习·计算机视觉
J老熊1 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
weixin_514548892 小时前
机器学习课程学习周报十五
人工智能·学习·机器学习
Themberfue2 小时前
基础算法之双指针--Java实现(下)--LeetCode题解:有效三角形的个数-查找总价格为目标值的两个商品-三数之和-四数之和
java·开发语言·学习·算法·leetcode·双指针
AIGC破防黑吗喽2 小时前
Midjourney零基础学习
人工智能·gpt·学习·ai·stable diffusion·midjourney·ai绘画
TheManba2 小时前
04. maven 三种项目打包方式 pom、jar、war 的区别(记一次 Spring 项目启动报错)
spring·maven·jar