[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
    • 扫描相关的类
      • 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.*;

/**
 * 

* 手写一个 请求转发器 DispatchServlet * - 代码重构,流程清晰化 *

* * @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 */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } /** * Post请求处理转发 * * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected 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 */ @Override public 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. 初始化 HandlerMapping doInitHandlerMapping(); 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. 处理 XController if (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)