[Spring] 30个类手写 Spring Mini 版本系列(二)

2023-12-22 01:32

本文主要是介绍[Spring] 30个类手写 Spring Mini 版本系列(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

[Spring] 30个类手写 Spring Mini 版本系列(二)

简介

为了更深入的了解 Spring 的实现原理和设计思想,一直打算出个系列文章,从零开始重新学习 Spring。在[Spring] 30个类手写 Spring Mini 版本系列(一)中,我们直接通过 Servlet API 初步实现了 Spring 的简易版。针对V1.1.0版,今天我们来做下优化,主要针对主要流程节点进行初步的方法封装,便于后续模块设计。


目录

  • [Spring] 30个类手写 Spring Mini 版本系列(二)
    • V1 版本
      • V1.2.0 版本
        • 代码重构
        • 效果演示
        • 小结
    • 更多

手机用户请横屏获取最佳阅读体验,REFERENCES中是本文参考的链接,如需要链接和更多资源,可以加入『知识星球』获取长期知识分享服务。


正文


基本思路

  • 配置阶段
    • 配置 web.xml
      • DispatchSevlet
    • 设定 init-param
      • contextConfigLocation = classpath:application.xml
    • 设定url-pattern
      • /*
    • 定义Annotation
      • @Controller
      • @Service
      • @Autowried
      • @RequestMapping
  • 初始化阶段
    • 调用 init() 方法
      • 加载配置文件
    • IOC容器初始化
      • Map<String,Object>
    • 扫描相关的类
      • Scan-package=“com.yido”
    • 创建实例化并保存至容器
      • 通过反射机制将类实例化放入 IOC 容器
    • 进行 DI
      • 扫描 IOC 容器中的实例,给没有赋值的属性自动赋值
    • 初始化 HandlerMapping
      • 将 URL 和 Method 建立一对一的映射关系
  • 运行阶段
    • 调用 doPost() / doGet()
      • Web 容器调用 doPost() / doGet() ,获得 request / response 对象
    • 匹配 HandlerMapping
      • 从 request 对象中获取用户输入的 url , 找到对应的 Method
    • 反射调用 method.invoke()
      • 利用反射调用方法并返回结果
    • 返回结果
      • 利用 response.getWriter().write(), 将返回结果输出到浏览器

V1 版本

准备工作

此处不再赘述,基于[Spring] 30个类手写 Spring Mini 版本系列(一)做优化。

V1.2.0 版本

注解和 Controller 复用,主要针对 XDispatchServlet 进行重构,主要是方法流程的编排,便于后续实现 Spring 中的设计

  • 方法功能分割

  • 设计流程分割

代码重构
/** @ProjectName: 编程学习* @Copyright:   2019 HangZhou Ashe Dev, Ltd. All Right Reserved.* @address:     https://yiyuery.github.io/NoteBooks/* @date:        2020/4/12 5:30 下午* @description: 本内容仅限于编程技术学习使用,转发请注明出处.*/
package com.yido.mvcframework.v2.servlet;import com.yido.mvcframework.annotation.*;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.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;/*** <p>* 手写一个 请求转发器 DispatchServlet* - 代码重构,流程清晰化* </p>** @author Helios* @date 2020/4/12 5:30 下午*/
public class XDispatchServlet extends HttpServlet {/*** 配置*/private Properties contextConfig = new Properties();//享元模式,缓存private List<String> classNames = new ArrayList<String>();//IoC容器,key默认是类名首字母小写,value就是对应的实例对象private Map<String, Object> ioc = new HashMap<String, Object>();//URL 和 Method 映射关系private Map<String, Method> handlerMapping = new HashMap<String, Method>();/*** Get 请求处理转发** @param req* @param resp* @throws ServletException* @throws IOException*/@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);}/*** Post请求处理转发** @param req* @param resp* @throws ServletException* @throws IOException*/@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {try {//【运行阶段】//6. 委派:根据 URL 找到一个对应的 Method ,并通过 response 返回doDispatch(req, resp);} catch (Exception e) {e.printStackTrace();//出现异常,返回堆栈信息resp.getWriter().write("500 Exception, Detail:  " + Arrays.toString(e.getStackTrace()));}}/*** 根据 URL 找到一个对应的 Method ,并通过 response 返回** @param req* @param resp*/private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException, InvocationTargetException, IllegalAccessException {//1. 获取参数和请求路径String url = req.getRequestURI();String contextPath = req.getContextPath();//替换请求上下文url = url.replace(contextPath, "")//替换多余 '/'.replaceAll("/+", "/");// 2. 获取请求处理器if (!this.handlerMapping.containsKey(url)) {resp.getWriter().write("404 Not Found!");return;}Map<String, String[]> params = req.getParameterMap();Method method = this.handlerMapping.get(url);// 3. 解析参数//3.1 获取形参列表Class<?>[] parameterTypes = method.getParameterTypes();Object[] paramValues = new Object[parameterTypes.length];boolean directReturn = true;for (int i = 0; i < parameterTypes.length; i++) {Class<?> parameterType = parameterTypes[i];if (parameterType == HttpServletRequest.class) {paramValues[i] = req;} else if (parameterType == HttpServletResponse.class) {paramValues[i] = resp;directReturn = false;} else if (parameterType == String.class) {//通过运行时状态去获取Annotation[][] parameterAnnotations = method.getParameterAnnotations();for (int j = 0; j < parameterAnnotations.length; j++) {for (Annotation annotation : parameterAnnotations[j]) {if (annotation instanceof XRequestParam) {String paramName = ((XRequestParam) annotation).value();if (!"".equals(paramName.trim())) {paramValues[i] = Arrays.toString(params.get(paramName)).replaceAll("\\[|\\]", "").replaceAll("\\s+", ",");}}}}}}//暂时硬编码String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());//赋值实参列表Object result = method.invoke(ioc.get(beanName), paramValues);//方法入参没有 resp 时 & 方法返回值部不为 void 直接返回if (!"void".equals(method.getReturnType().getName()) && directReturn) {resp.getWriter().write(Arrays.toString(new Object[]{result}).replaceAll("\\[|\\]", ""));}}/*** 加载配置并缓存 Controller 实例和 初始化请求对应的 Method映射** @param config* @throws ServletException*/@Overridepublic void init(ServletConfig config) throws ServletException {//【初始化阶段】//====== 初始化加载 =====//1. 读取配置文件doLoadConfig(config.getInitParameter("contextConfigLocation"));//2. 扫描doScanner(contextConfig.getProperty("scanPackage"));//====== IOC 容器 =====//3. 初始化 IOC 容器,将扫描到的相关类实例化,保存到 IOC 容器doInstance();//AOP 生成新的代理对象//====== DI =====//4. 完成依赖注入doAutoWired();//====== MVC =====//5. 初始化 HandlerMappingdoInitHandlerMapping();System.out.println("XSpring MVC Framework has been initialed");}/*** 读取配置文件** @param contextConfigLocation*/private void doLoadConfig(String contextConfigLocation) {try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation)) {contextConfig.load(is);} catch (IOException e) {e.printStackTrace();}}private void doScanner(String scanPackage) {URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));File rootDir = new File(url.getFile());for (File file : rootDir.listFiles()) {if (file.isDirectory()) {doScanner(scanPackage + "." + file.getName());} else {if (!file.getName().endsWith(".class")) {continue;}String clazzName = scanPackage + "." + file.getName().replace(".class", "");classNames.add(clazzName);}}}/*** 完成依赖注入*/private void doAutoWired() {if (ioc.isEmpty()) {return;}Collection<Object> values = ioc.values();for (Object value : values) {if (null == value) {continue;}Class clazz = value.getClass();if (clazz.isAnnotationPresent(XController.class)) {Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {if (!field.isAnnotationPresent(XAutowired.class)) {continue;}XAutowired autowired = field.getAnnotation(XAutowired.class);String beanName = autowired.value();if ("".equals(beanName)) {beanName = toLowerFirstCase(field.getType().getSimpleName());}//注入依赖实例field.setAccessible(true);try {field.set(value, ioc.get(beanName));} catch (IllegalAccessException e) {e.printStackTrace();}}}}//Spring 实例平铺后只需要再次扫描进行一次 DI 操作即可解决依赖注入的问题,此处演示未涉及,暂不做此处理}/*** 初始化 IOC 容器,将扫描到的相关类实例化,保存到 IOC 容器*/private void doInstance() {if (classNames.isEmpty()) {return;}try {for (String clazzName : classNames) {if (!clazzName.contains(".")) {continue;}Class<?> clazz = Class.forName(clazzName);// 1. 处理 XControllerif (clazz.isAnnotationPresent(XController.class)) {ioc.put(toLowerFirstCase(clazz.getSimpleName()), clazz.newInstance());// 2. 处理 XService} else if (clazz.isAnnotationPresent(XService.class)) {//2.1 在多个包下出现相同的类名,只能(自己)起一个全局唯一的名字//自定义命名XService service = clazz.getAnnotation(XService.class);String beanName = service.value();//2.2 默认的类名首字母小写if ("".equals(beanName)) {beanName = toLowerFirstCase(clazz.getSimpleName());}//2.3 如果是接口//判断有多少个实现类,如果有多个重名实例,只能抛异常Object instance = clazz.newInstance();ioc.put(beanName, instance);for (Class<?> i : clazz.getInterfaces()) {if (ioc.containsKey(toLowerFirstCase(i.getSimpleName()))) {//不允许一个接口映射多个实例,默认取第一个throw new Exception("The " + i.getName() + " is exists!!");}ioc.put(toLowerFirstCase(i.getSimpleName()), instance);}}}} catch (Exception e) {e.printStackTrace();}}/*** 首字母小写** @param name* @return*/private String toLowerFirstCase(String name) {char[] chars = name.toCharArray();chars[0] += 32;return String.valueOf(chars);}/*** 初始化 HandlerMapping*/private void doInitHandlerMapping() {if (classNames.isEmpty()) {return;}try {for (Map.Entry<String, Object> entry : ioc.entrySet()) {Class<?> clazz = entry.getValue().getClass();if (!clazz.isAnnotationPresent(XController.class)) {continue;}String baseUrl = "";// 1. 处理 XController 中方法映射if (clazz.isAnnotationPresent(XController.class)) {// 1.1 解析请求路径前缀if (clazz.isAnnotationPresent(XRequestMapping.class)) {XRequestMapping requestMapping = clazz.getAnnotation(XRequestMapping.class);baseUrl = requestMapping.value();}// 1.2 解析public方法Method[] methods = clazz.getMethods();for (Method method : methods) {if (method.isAnnotationPresent(XRequestMapping.class)) {XRequestMapping annotation = method.getAnnotation(XRequestMapping.class);//替换多/为单/String url = ("/" + baseUrl + "/" + annotation.value()).replaceAll("/+", "/");handlerMapping.put(url, method);System.out.println("> Mapped--------->url: " + url + "," + method.getName());}}}}} catch (Exception e) {e.printStackTrace();}}}
效果演示
   /*** support:* spring-v1* spring-v2** @param name* @return*/@XRequestMapping("/v2/welcome")public String welcome2(@XRequestParam(value = "name") String name) {return helloService.welcome(name);}/*** support:* spring-v1* spring-v2** @param name* @return*/@XRequestMapping("/v3/welcome")public String welcome3(@XRequestParam(value = "name") String name, @XRequestParam("nick") String nick) {return helloService.welcome(nick + " " + name);}

为了便于区分,对接口添加版本标识

.
.

小结
  • 增加 request \ response 和其他参数的自动注入(目前仅支持 String)
  • 支持方法返回结果的直接渲染
  • 流程更加清晰
    • 初始化加载配置
    • 实例化并放入容器,控制反转:IOC
    • 依赖注入:DI
    • 前端请求处理方式映射表 HandlerMapping
    • 运行状态下请求处理:doDispatch
  • 但是 HandleMapping 管理过程中暴露了太多细节,还有些小问题需要优化
  • 没有明确划分 AOP、IOC、MVC 模块
  • To do Continue...

更多

扫码关注架构探险之道,回复『源码』,获取本文相关源码和资源链接

.

知识星球(扫码加入,获取珍贵笔记、视频、电子书的等资源)

.

这篇关于[Spring] 30个类手写 Spring Mini 版本系列(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/522180

相关文章

springboot健康检查监控全过程

《springboot健康检查监控全过程》文章介绍了SpringBoot如何使用Actuator和Micrometer进行健康检查和监控,通过配置和自定义健康指示器,开发者可以实时监控应用组件的状态,... 目录1. 引言重要性2. 配置Spring Boot ActuatorSpring Boot Act

使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)

《使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)》在现代软件开发中,处理JSON数据是一项非常常见的任务,无论是从API接口获取数据,还是将数据存储为JSON格式,解析... 目录1. 背景介绍1.1 jsON简介1.2 实际案例2. 准备工作2.1 环境搭建2.1.1 添加

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

java如何分布式锁实现和选型

《java如何分布式锁实现和选型》文章介绍了分布式锁的重要性以及在分布式系统中常见的问题和需求,它详细阐述了如何使用分布式锁来确保数据的一致性和系统的高可用性,文章还提供了基于数据库、Redis和Zo... 目录引言:分布式锁的重要性与分布式系统中的常见问题和需求分布式锁的重要性分布式系统中常见的问题和需求

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python