SpringMVC 底层机制的简易实现

news2024/11/16 12:07:17

SpringMVC 底层机制的简易实现

      • 项目基础
        • 配置 xml 文件
      • 开发指南
      • 开发步骤
        • 1.初始化数据
        • 2.中央控制器 - 分发请求
        • 3.开发者角度
        • 4.视图解析器
      • 开发总结

项目基础

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>

    <!--原生 Servlet-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <!--作用范围-->
        <!--provided:表示该项目在打包放在生产环境的时候不用带上 servlet-api.jar 包-->
        <!--目的: 防止和 tomcat 中该jar冲突-->
        <scope>provided</scope>
    </dependency>

    <!--引入 xml 解析工具-->
    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>

    <!--引入常用工具-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.5</version>
    </dependency>
</dependencies>

配置 xml 文件

image-20221015222559526

mySpringMVC.xml 文件:用来指定扫描包路径

<?xml version="1.0" encoding="utf-8" ?>
<beans>
    <component-scan base-package="com.al_tair"></component-scan>
</beans>

web.xml 文件

  • 指定要操作的 spring 容器配置文件名
  • 配置自动加载
  • 路径全部访问中央控制器
<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <servlet>
    <servlet-name>MyDispatcherServlet</servlet-name>
    <servlet-class>com.al_tair.servlet.MyDispatcherServlet</servlet-class>
    <!--指定要操作的 spring 容器配置文件-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:mySpringMVC.xml</param-value>
    </init-param>
    <!--自动加载-->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>MyDispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

开发指南

image-20221015221516593

开发步骤

MyDispatcherServlet 本质就是一个 Servlet ,那么我们就知道他的基本特性,刚启动服务的时候,我们会进行初始化,然后每进行 http 请求,就会访问 Service 方法(这里交给 Servet 的父类来实现,我们只需要实现 doPost 和 doGet 的方法)

1.初始化数据

@Override
public void init(ServletConfig config) throws ServletException {
    // 需要将 config 传给父类,才可以获取工程路径
    super.init(config);
    // 上述 web.xml 文件中指定了该参数为 Spring 容器的文件名 mySpringMVC.xml
    String contextConfigLocation = config.getInitParameter("contextConfigLocation");
    // 分成两步骤进行理解
    // 1.通过 xml 解析获取到该 mySpringMVC.xml 的包路径
    // 2.将该路径进行扫描,生成 ioc 容器
    ioc = new SpringIocApplicationContext(XmlParser.getBasePackage(contextConfigLocation.split(":")[1]));
    // 初始化映射器
    initHandlerMapper();
}
  • XmlParser 解析器

    public class XmlParser {
        public static String getBasePackage(String fileName){
            // 解析器
            SAXReader saxReader = new SAXReader();
            // 获取到资源路径
            InputStream in = XmlParser.class.getClassLoader().getResourceAsStream(fileName);
    
            String basePackage = null;
            try {
                Document read = saxReader.read(in);
                // 获取到 component-scan 元素
                Element element = read.getRootElement().element("component-scan");
                // 扫描包路径
                basePackage = element.attribute("base-package").getText();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return basePackage;
        }
    }
    
  • 初始化 ioc 容器(我主要介绍实现过程)

    1. 获取扫描包的全路径
    2. 通过类加载器获取到对应包的资源路径
    3. 扫描资源路径,将 java 文件生成的 .class 文件路径保存到 List 集合中
    4. 遍历每个 java 文件路径,并进行类加载,通过判断是否有注解(MyController 、MyService、MyMapper、MyComponent)
    5. 然后通过注解上的值来获取对应 id(默认方法名首字母小写),并划分单例还是多例(用于判断是否当下就创建对象并放入 ioc 容器中)
    6. ioc 容器用于存储 id <=> BeanDefinition对象(属性有 scope、Class对象)
    7. singletonObject 单例池用于存储 id <=> 单例对象
  • 初始化映射器

    private void initHandlerMapper() {
        // 默认所有映射都是非懒加载的单例池中
        ConcurrentHashMap<String, Object> singletonObject = ioc.getSingletonObject();
        if (singletonObject.isEmpty()) {
            return;
        } else {
            for (Map.Entry<String, Object> entry : singletonObject.entrySet()) {
                // 拿到加载的类对应的 Class 类
                Class<?> aClass = entry.getValue().getClass();
                // 判断是否是控制类
                if (aClass.isAnnotationPresent(MyController.class)) {
                    // 1.获取类注解值
                    String classValue = "";
                    // 2.获取类上路径地址
                    if (aClass.isAnnotationPresent(MyRequestMapping.class)) {
                        MyRequestMapping annotation = aClass.getAnnotation(MyRequestMapping.class);
                        classValue = annotation.value();
                    }
    
                    // 3.获取到控制类中的所有方法
                    Method[] declaredMethods = aClass.getDeclaredMethods();
                    for (Method declaredMethod : declaredMethods) {
                        if (declaredMethod.isAnnotationPresent(MyRequestMapping.class)) {
                            MyRequestMapping MyRequestMappingAnnotation =
                                declaredMethod.getAnnotation(MyRequestMapping.class);
                            // getServletContext().getContextPath()  获取工程路径
                            String url = getServletContext().getContextPath() + classValue
                                + MyRequestMappingAnnotation.value();
                            // 创建 HandlerMapper 对象(用于存储 访问的 url、controller 对象、对应的方法)
                            HandlerMapper handlerMapper =
                                new HandlerMapper(url, entry.getValue(), declaredMethod);
                            // HandlerList 集合用来保存每个能够访问到的对应类的对应方法信息
                            HandlerList.add(handlerMapper);
                        }
                    }
                }
            }
        }
    }
    
    

2.中央控制器 - 分发请求

刚开始讲到了中央控制器本质就是 Servlet,每次请求都会访问 Service,然后分发到对应的请求类型比如 get、post等等

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 防止乱码
    req.setCharacterEncoding("utf-8");
    resp.setCharacterEncoding("utf-8");
    // 分发请求
    executeDispatch(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doGet(req, resp);
}

分发请求(同时也会接收返回的结果,这个后续讲解)

<!-- 用于获取请求参数名(默认为 args0、args1等)-->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.7.0</version>
  <configuration>
    <source>1.8</source>
    <target>1.8</target>
    <compilerArgs>
      <arg>-parameters</arg>
    </compilerArgs>
    <encoding>utf-8</encoding>
  </configuration>
</plugin>
private void executeDispatch(HttpServletRequest req, HttpServletResponse resp) {
    HandlerMapper handler = getHandler(req);
    // 判断路径是否匹配成功
    if (handler == null) {
        try {
            resp.getWriter().print("<h1>404 not found</h1>");
        } catch (IOException e) {
            log.print(e.getMessage());
        }
    } else {
        // 1.获取请求的方法
        Method method = handler.getMethod();
        // 2.获取请求方法的参数类型列表
        Class<?>[] parameterTypes = method.getParameterTypes();
        // 用于匹配对应方法的参数,可以自动化装配
        Object[] params = new Object[parameterTypes.length];
        // 3.遍历每个参数,并填充数据
        for (int i = 0; i < parameterTypes.length; i++) {
            Class<?> parameterType = parameterTypes[i];
            // 4.获取类型的简单名称,就是类名(原来名称全包名)
            String simpleName = parameterType.getSimpleName();
            if ("HttpServletRequest".equals(simpleName)) {
                params[i] = req;
            } else if ("HttpServletResponse".equals(simpleName)) {
                params[i] = resp;
            }
        }
        // 5.传入的参数的 key - value
        Map<String, String[]> parameterMap = req.getParameterMap();
        // 6.对应方法参数上的信息
        Parameter[] parameters = method.getParameters();
        // 7.遍历方法的每个参数并根据参数名或者注解值来填入数据
        for (int i = 0; i < params.length; i++) {
            if(params[i] == null){
                boolean isMyRequestParam = parameters[i].isAnnotationPresent(MyRequestParam.class);
                // 参数名
                String paramName = "";
                if(isMyRequestParam){
                    paramName = parameters[i].getDeclaredAnnotation(MyRequestParam.class).value();
                }else{
                    // 注意获取参数名必须添加脚本文件在 pom.xml 中
                    paramName = parameters[i].getName();
                }
                for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                    if(paramName.equals(entry.getKey())){
                        // 根据类型来判断是否是数组或者集合等等
                        if("List".equals(parameterTypes[i].getSimpleName())){
                            params[i] = Arrays.asList(entry.getValue());
                        }else if(parameterTypes[i].getSimpleName().contains("[]")){
                            params[i] = entry.getValue();
                        }else{
                            params[i] = entry.getValue()[0];
                        }
                        break;
                    }
                }
            }
        }
        try {
            // 匹配成功,通过反射来调用对应的类(返回结果暂时就是默认 String 类型)
            Object result = method.invoke(handler.getController(), params);
            // 重定向或者请求转发(视图解析操作)
            viewParser(req,resp,method,result);

        } catch (Exception e) {
            log.print(e.getMessage());
        }
    }
}

3.开发者角度

开发控制层、服务层、Dao层,用于接口的开发等等…

4.视图解析器

主要用于方法调用完之后接下来的操作(重定向、请求转发、或者返回 json 格式数据等等操作)

    <!--引入jackson 使用他的工具类可以进行json操作 -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.4</version>
    </dependency>
public void viewParser(HttpServletRequest request,HttpServletResponse response,Method method,Object result){
    if(result instanceof String){
        // 返回结果: forward:/200_ok.jsp(例子)
        String res = (String)result;
        if(res.contains(":")){
            String[] split = res.split(":");
            if("forward".equals(split[0])){
                try {
                    request.getRequestDispatcher(split[1]).forward(request,response);
                } catch (Exception e) {
                    System.out.println("请求转发异常" + e.getMessage());
                }
            }else if("redirect".equals(split[0])){
                try {
                    // 重定向需要获取工程路径
                    response.sendRedirect(getServletContext().getContextPath() + split[1]);
                } catch (IOException e) {
                    System.out.println("重定向异常" + e.getMessage());
                }
            }
            // 默认请求转发
        }else{
            try {
                request.getRequestDispatcher(res).forward(request,response);
            } catch (Exception e) {
                System.out.println("请求转发异常" + e.getMessage());
            }
        }
    }else {
        // 判断是否需要解析成 json 格式的数据
        if(method.isAnnotationPresent(MyResponseBody.class)){
            if(result instanceof List){
                // jackson 工具类
                ObjectMapper objectMapper = new ObjectMapper();
                try {
                    String res = objectMapper.writeValueAsString(result);
                    PrintWriter writer = null;
                    try {
                        writer = response.getWriter();
                        writer.write(res);
                    } catch (IOException e) {
                        System.out.println(e.getMessage());
                    }finally {
                        writer.flush();
                        writer.close();
                    }
                } catch (JsonProcessingException e) {
                    System.out.println("json 解析错误" + e.getMessage());
                }
            }
        }
    }
}

开发总结

我简易实现 SpringMVC 底层机制,主要是为了熟悉该框架的流程,加深编程的思想深度,可能存在很多问题,希望大家能够帮忙提出,感谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/134273.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python基础】常用数据结构及处理

1. KeyValue dict.clear() 删除字典内所有元素dict.copy()返回一个字典的浅复制[dict.fromkeys(seq, val])创建一个新字典&#xff0c;以序列 seq 中元素做字典的键&#xff0c;val 为字典所有键对应的初始值 dict.get(key, defaultNone) 返回指定键的值&#xff0c;如果值不在…

prompt模型详解之文本生成

prompt在生成方面的应用从两个方面进行介绍&#xff1a; 评估手段 具体任务 评估手段 生成任务的评估手段主要分为四种类型&#xff1a; 1). 基于N-gram匹配 2). 基于编辑距离 3). 基于词向量 4). 基于可学习方式。 本小节主要介绍BARTSCORE&#xff0c;其使用prompt方…

Python杂题

目录 一、前言 二、例题1——修剪灌木 三、例题2—— 付账问题 四、例题3——最少砝码 五、例题四——矩形拼接 六、例题五——蜂巢 一、前言 竞赛题有很多不需要什么算法的题目&#xff0c;只要学过编程语言就能做&#xff0c;其考核思维、逻辑、编码能力。而这种题有“…

【算法题解】 8. K 个一组翻转链表

文章目录题目解题思路代码实现复杂度分析题目 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&#xff0c;它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍&#xff0c;那么请将最后剩余的节点保持…

Python中编码(encode)解码(decode)讲解

嗨害大家好鸭&#xff01;我是小熊猫~ 这次也是给大家带来一点干货~ 所用素材:点击此处跳转文末名片获取 一、python3中str与unicode 在python3中&#xff0c;字符串有两种形式&#xff1a;str和bytes&#xff0c;两者区别如下&#xff1a; unicode string(str类型)&#xf…

【经验】关于区分cin、getline、cin.getline三种字符串输入的区别

cin 既可以输入char[]数组&#xff0c;也可以输入string类型&#xff0c;输入会被空格打断 cin对char进行输入 #include<bits/stdc.h> using namespace std; int main(){char ch[50];cin>>ch;cout<<strlen(ch)<<endl;for(int i0;i<strlen(ch);i){…

1.移动机器人发展现状

移动机器人主要应用场景&#xff1a; 场景1.仓储机器人(AGV自动导引运输车)&#xff1a;电商企业用户下单后机器人可以实现自动分拣和发货。需要多个传感器配合 2.自动驾驶领域(AMR自主移动机器人):车辆避让行人、导航等 热点研究领域&#xff1a; 环境感知和建模、人机交互…

2022简要总结和2023行动指南

在这辞旧迎接之际&#xff0c;心存感恩&#xff0c;放眼未来。 祝宝妈妈宝&#xff0c;幸福快乐&#xff1b; 祝国泰民安&#xff0c;政通人和。 祝百融云创系&#xff0c;生意兴隆&#xff1b; 祝公司老板们&#xff0c;大展宏图&#xff1b; 祝同事同行er&#xff0c;身…

Java算法_LeetCode:旋转数组

旋转数组 给你一个数组&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,…

Fastsapi的小疑问

1. Fastapi中的get和post区别是什么&#xff1f; 答&#xff1a;get参数传输暴露在外&#xff0c;post隐式传输 GET参数获取&#xff1a;获取一个URL后面带?param11&param22这种形式。 特点&#xff1a;URL上直接编辑传输&#xff0c;方便快捷&#xff0c;但是信息暴露在…

【nowcoder】笔试强训Day16

目录 一、选择题 二、编程题 2.1扑克牌大小 2.2完全数计算 一、选择题 1.在关系型是数据库中&#xff0c;有两个不同的事务同时操作数据库中同一表的同一行&#xff0c;不会引起冲突的是&#xff1a; A. 其中一个DELETE操作&#xff0c;一个是SELECT操作 B. 其中两个都是…

植物大战僵尸:代码实现无限阳光

通过逆向分析植物阳光数量的动态地址找到阳光的基址与偏移&#xff0c;从而实现每次启动游戏都能够使用基址加偏移的方式定位阳光数据&#xff0c;最后我们将通过使用C语言编写通用辅助实现简单的无限阳光辅助&#xff0c;在教程开始之前我们先来说一下为什么会有动态地址与基址…

光缆单盘检测与光缆线路测试需使用双窗口吗?

1 引言 光缆线路和宽带接入工程中&#xff0c;通常会涉及光缆单盘检测与光缆线路的测试工作&#xff0c;光缆线路测试包括&#xff1a;中继段测试、用户光缆测试等。这些测试条目&#xff0c;有的只需采用测试仪表的1个波长进行测试&#xff0c;即单窗口测试&#xff0c;有的则…

代码随想录算法训练营第2天 977. 有序数组的平方、209. 长度最小的子数组

代码随想录算法训练营第2天| 977. 有序数组的平方、209. 长度最小的子数组 有序数组的平方 力扣题目链接(opens new window) 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 数组其实是有…

C 语法--编译相关

1&#xff0c; 单下划线和双下划线 #pragma #pragma 用于指示编译器完成一些特定的动作。#pragma 所定义的很多指示字是编译器特有的&#xff0c;在不同的编译器间是不可移植的 #pragma section APP_VERSION //__far const unsigned long version 0x01010101; __far const un…

redis缓存淘汰策略-基于LinkedHashMap实现LRU算法

redis缓存淘汰策略-LRU算法&#xff08;最近最少使用&#xff09; LRU是Least Recently Used的缩写&#xff0c;即最近最少使用&#xff0c;是一种常用的页面置换算法&#xff0c; 选择最近最久未使用的数据予以淘汰。 1&#xff0c;所谓缓存&#xff0c; 必须要有读写两个操作…

【叨叨与总结】2022年总结

如果我记得没错&#xff0c;这个记录时间的软件应该是在6月份或者7月份才开始用的&#xff0c;大概记录的时间有半年。个人觉得还是不错的&#xff0c;下面还是简单的总结一下。   首先睡眠时长是有一定保障的&#xff0c;甚至有好多时候还睡了9、10个小时&#xff0c;当然这…

【Python基础】模块化编程-包调用

datitle: Python Module&&Invoking date: 2020-05-12 00:16:58 img: https://gitee.com/github-25970295/blogImage/raw/master/img/woman-3219507__340.webp categories: 编程语言 reprintPolicy: cc_by cover: false tags: python ​ 无论我们选择用何种语言进行程序…

2022年度总结:凝神聚力 踔厉奋发

2021年底手里握了几份offer&#xff0c;有研究型大学&#xff0c;也有教学型大学。是选择去前者继续拼搏&#xff0c;还是选择去后者直接躺平&#xff1f;二者的权衡和取舍确实不太容易抉择。尽管也咨询了很多前辈&#xff0c;最后还是得自己做决定。尽管我还是很喜欢做研究工作…

spark理论

前言&#xff1a; 本文是之前19年学生时学习林子雨老师《Spark大数据 》网易公开课的中关于spark的理论部分的部分笔记。主要包括大数据产品与spark的一些概念与运行原理介绍。 目录 大数据产品与hadoop生态系统 Spark概念 MapReduce与spark的比较 Spark运行 Spark运行基本流程…