[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

相关文章

Java通过驱动包(jar包)连接MySQL数据库的步骤总结及验证方式

《Java通过驱动包(jar包)连接MySQL数据库的步骤总结及验证方式》本文详细介绍如何使用Java通过JDBC连接MySQL数据库,包括下载驱动、配置Eclipse环境、检测数据库连接等关键步骤,... 目录一、下载驱动包二、放jar包三、检测数据库连接JavaJava 如何使用 JDBC 连接 mys

SpringBoot线程池配置使用示例详解

《SpringBoot线程池配置使用示例详解》SpringBoot集成@Async注解,支持线程池参数配置(核心数、队列容量、拒绝策略等)及生命周期管理,结合监控与任务装饰器,提升异步处理效率与系统... 目录一、核心特性二、添加依赖三、参数详解四、配置线程池五、应用实践代码说明拒绝策略(Rejected

一文详解SpringBoot中控制器的动态注册与卸载

《一文详解SpringBoot中控制器的动态注册与卸载》在项目开发中,通过动态注册和卸载控制器功能,可以根据业务场景和项目需要实现功能的动态增加、删除,提高系统的灵活性和可扩展性,下面我们就来看看Sp... 目录项目结构1. 创建 Spring Boot 启动类2. 创建一个测试控制器3. 创建动态控制器注

Java操作Word文档的全面指南

《Java操作Word文档的全面指南》在Java开发中,操作Word文档是常见的业务需求,广泛应用于合同生成、报表输出、通知发布、法律文书生成、病历模板填写等场景,本文将全面介绍Java操作Word文... 目录简介段落页头与页脚页码表格图片批注文本框目录图表简介Word编程最重要的类是org.apach

Spring Boot中WebSocket常用使用方法详解

《SpringBoot中WebSocket常用使用方法详解》本文从WebSocket的基础概念出发,详细介绍了SpringBoot集成WebSocket的步骤,并重点讲解了常用的使用方法,包括简单消... 目录一、WebSocket基础概念1.1 什么是WebSocket1.2 WebSocket与HTTP

SpringBoot+Docker+Graylog 如何让错误自动报警

《SpringBoot+Docker+Graylog如何让错误自动报警》SpringBoot默认使用SLF4J与Logback,支持多日志级别和配置方式,可输出到控制台、文件及远程服务器,集成ELK... 目录01 Spring Boot 默认日志框架解析02 Spring Boot 日志级别详解03 Sp

java中反射Reflection的4个作用详解

《java中反射Reflection的4个作用详解》反射Reflection是Java等编程语言中的一个重要特性,它允许程序在运行时进行自我检查和对内部成员(如字段、方法、类等)的操作,本文将详细介绍... 目录作用1、在运行时判断任意一个对象所属的类作用2、在运行时构造任意一个类的对象作用3、在运行时判断

java如何解压zip压缩包

《java如何解压zip压缩包》:本文主要介绍java如何解压zip压缩包问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java解压zip压缩包实例代码结果如下总结java解压zip压缩包坐在旁边的小伙伴问我怎么用 java 将服务器上的压缩文件解压出来,

SpringBoot中SM2公钥加密、私钥解密的实现示例详解

《SpringBoot中SM2公钥加密、私钥解密的实现示例详解》本文介绍了如何在SpringBoot项目中实现SM2公钥加密和私钥解密的功能,通过使用Hutool库和BouncyCastle依赖,简化... 目录一、前言1、加密信息(示例)2、加密结果(示例)二、实现代码1、yml文件配置2、创建SM2工具

Spring WebFlux 与 WebClient 使用指南及最佳实践

《SpringWebFlux与WebClient使用指南及最佳实践》WebClient是SpringWebFlux模块提供的非阻塞、响应式HTTP客户端,基于ProjectReactor实现,... 目录Spring WebFlux 与 WebClient 使用指南1. WebClient 概述2. 核心依