首页 > 其他分享 >用 300 行代码手写提炼 Spring 核心原理 [2]

用 300 行代码手写提炼 Spring 核心原理 [2]

时间:2024-07-21 23:51:28浏览次数:12  
标签:String 300 Spring beanName example url 手写 IoC method

系列文章

上文 中我们实现了 mini-spring 的 1.0 版本,接下来我们在此基础上进行优化,将 init() 方法中的代码进行封装。按照之前的思路,先搭建基础框架,再 “填肉注血”。

初始化阶段

init

将 init() 方法中冗长的代码进行模块化拆分:

@Override
public void init(ServletConfig config) throws ServletException {
    // 1. 加载配置文件
    doLoadConfig(config.getInitParameter("contextConfigLocation"));

    // 2. 扫描相关类
    doScanner(contextConfig.getProperty("scanPackage"));

    // 3. 初始化扫描到的类,并将它们放到IoC容器中
    doInstance();

    // 4. 依赖注入
    doAutowired();

    // 5. 初始化handlerMapping
    initHandlerMapping();
}

声明全局成员变量,其中 IoC 容器就是注册时单例的具体实例:

// 保存application.properties配置文件中的内容
private Properties contextConfig = new Properties();

// 保存扫描到所有的类名
private List<String> classNames = new ArrayList<>();

// 传说中的IoC容器,我们来揭开它的神秘面纱
// 为了简化程序,暂时不考虑ConcurrentHashMap,主要关注原理和设计思想
private Map<String, Object> IoC = new HashMap<>();

// 保存url -> method的映射关系
private Map<String, Method> handlerMapping = new HashMap<>();

加载配置文件

实现 doLoadConfig() 方法:

/**
 * 加载配置文件,本例中配置文件configFileName为application.properties
 */
private void doLoadConfig(String configFileName) {
    // 找到Spring主配置文件所在路径
    // 读取出来保存到Properties文件中
    // 本例中配置文件中的内容只有一行: scanPackage=org.example.minispring
    InputStream is = this.getClass().getClassLoader().getResourceAsStream(configFileName);
    try {
        contextConfig.load(is);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

扫描相关的类

实现 doScanner() 方法:

/**
 * 扫描相关的类,本例中scanPackage=org.example.minispring
 */
private void doScanner(String scanPackage) {
    // 转换为文件路径,手机上就是把"."替换为"/"
    URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
    File classDir = new File(url.getFile());
    for (File file : classDir.listFiles()) {
        if (file.isDirectory()) {
            // 递归扫描子文件夹
            doScanner(scanPackage + "." + file.getName());
        } else {
            // 只需要扫描.class文件
            if (!file.getName().endsWith(".class"))
                continue;
            String clazzName = scanPackage + "." + file.getName().replace(".class", "");
            classNames.add(clazzName);
        }
    }
}

类实例化

实现 doInstance() 方法,原理是通过反射机制将类实例化,保存至 IoC 容器中:

/**
 * 初始化扫描到的类,并将它们放到IoC容器中
 * 
 * 本例中IoC容器中的内容如下:
 * org.example.minispring.service.IDemoService = org.example.minispring.service.impl.DemoService@c97ae21
 * org.example.minispring.service.impl.DemoService = org.example.minispring.service.impl.DemoService@c97ae21
 * org.example.minispring.action.DemoAction = org.example.minispring.action.DemoAction@25051c3
 */
private void doInstance() {
    if (classNames.isEmpty())
        return;
    try {
        for (String className : classNames) {
            Class<?> clazz = Class.forName(className);
            // 什么类需要实例化呢?
            // 加了注解的类需要实例化, 本例中需要实例化@MyController, @MyService注解的类
            if (clazz.isAnnotationPresent(MyController.class)) {
                Object instance = clazz.newInstance();
                String beanName = clazz.getName();
                IoC.put(beanName, instance);
            } else if (clazz.isAnnotationPresent(MyService.class)) {
                // 自定义的beanName
                MyService service = clazz.getAnnotation(MyService.class);
                String beanName = service.value();
                if ("".equals(beanName.trim())) {
                    beanName = clazz.getName();
                }

                Object instance = clazz.newInstance();
                IoC.put(beanName, instance);
                // 根据接口类型自动赋值
                for (Class<?> i : clazz.getInterfaces()) {
                    if (IoC.containsKey(i.getName())) {
                        throw new Exception("The '" + i.getName() + "' already exists!");
                    }
                    // 把接口的类型直接当做key
                    IoC.put(i.getName(), instance);
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

依赖注入

进行依赖注入,实现 doAutowired() 方法:

/**
 * 完成依赖注入
 */
private void doAutowired() {
    if (IoC.isEmpty())
        return;
    for (Entry<String, Object> entry : IoC.entrySet()) {
        // 获取所有的字段,包括public, private, protected, default类型的
        Field[] fields = entry.getValue().getClass().getDeclaredFields();
        for (Field field : fields) {
            if (!field.isAnnotationPresent(MyAutowired.class))
                continue;
            MyAutowired autoWired = field.getAnnotation(MyAutowired.class);

            // 如果用户没有定义beanName,默认就根据类型注入
            String beanName = autoWired.value().trim();
            if ("".equals(beanName)) {
                beanName = field.getType().getName();
            }

            // 如果是public之外的类型,只要加了@MyAutowired注解都要强制赋值
            // 反射中叫做暴力访问
            field.setAccessible(true);

            try {
                // 用反射机制动态给字段赋值
                // 赋值后DemoAction.demoService = org.example.minispring.service.impl.DemoService@c97ae21
                // 也即DemoService实例被注入到了DemoAction对象中,此谓之依赖注入
                field.set(entry.getValue(), IoC.get(beanName));
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

初始化 handlerMapping

实现 initHandlerMapping() 方法,这里应用到了策略模式:

/**
 * 初始化HandlerMapping
 * 
 * 本例中handlerMapping中的内容是:
 * /demo/query = org.example.minispring.action.DemoAction.query(HttpServletRequest, HttpServletResponse, String)
 */
private void initHandlerMapping() {
    if (IoC.isEmpty())
        return;

    for (Entry<String, Object> entry : IoC.entrySet()) {
        Class<?> clazz = entry.getValue().getClass();
        if (!clazz.isAnnotationPresent(MyController.class))
            continue;

        String baseUrl = "";
        if (clazz.isAnnotationPresent(MyRequestMapping.class)) {
            MyRequestMapping requestMapping = clazz.getAnnotation(MyRequestMapping.class);
            baseUrl = requestMapping.value();
        }

        // 解析@MyController中方法上的@MyRequestMapping注解
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (!method.isAnnotationPresent(MyRequestMapping.class)) {
                continue;
            }
            MyRequestMapping requestMapping = method.getAnnotation(MyRequestMapping.class);
            // 组合方法签名上的完整url,正则替换是为防止路径中出现多个连续多个"/"的不规范写法
            String url = (baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
            // 保存url -> method的对应关系
            handlerMapping.put(url, method);
            System.out.println("Mapped " + url + " -> " + method);
        }
    }
}

到这里初始化工作完成,接下来实现运行的逻辑。

运行阶段

首先看 doPost() 和 doGet() 方法的代码:

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

@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    try {
        doDispatch(req, resp);
    } catch (Exception e) {
        e.printStackTrace();
        resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
    }
}

核心逻辑在 doDispatch() 方法中实现,其中用到了委派模式的思想:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replace(contextPath, "").replaceAll("/+", "/");
    if (!this.handlerMapping.containsKey(url)) {
        resp.getWriter().write("404 Not Found");
        return;
    }
    // 根据url找到对应的方法
    // 例如url(/demo/query)对应的method为
    // org.example.minispring.action.DemoAction.query(HttpServletRequest, HttpServletResponse, String)
    Method method = (Method) this.handlerMapping.get(url);
    // 获取请求参数, 此处为: name = [watermark]
    Map<String, String[]> parameterMap = req.getParameterMap();

    // 1. method.getDeclaringClass().getName(): beanName, 此处为org.example.minispring.action.DemoAction
    // 2. IoC.get(beanName): 根据beanName获取到对应的bean实例:org.example.minispring.action.DemoAction@51e3ce14
    // 3. method.invoke调用的就是[email protected](req, resp, name)
    String beanName = method.getDeclaringClass().getName();
    method.invoke(IoC.get(beanName), new Object[] { req, resp, parameterMap.get("name")[0] });
}

在以上代码中,doDispatch() 虽然完成了动态委派并进行了反射调用,但是对 url 参数的处理还是静态的,即只能对 "name" 参数做解析。要实现 url 参数的动态获取,还有些工作要做,我们继续优化 doDispatch() 方法的实现:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replace(contextPath, "").replaceAll("/+", "/");
    if (!this.handlerMapping.containsKey(url)) {
        resp.getWriter().write("404 Not Found");
        return;
    }
    // 根据url找到对应的方法
    // 例如url(/demo/query)对应的method为
    // org.example.minispring.action.DemoAction.query(HttpServletRequest, HttpServletResponse, String)
    Method method = (Method) this.handlerMapping.get(url);
    // 获取请求参数, 此处为: name = [watermark]
    Map<String, String[]> parameterMap = req.getParameterMap();

    // 1. method.getDeclaringClass().getName(): beanName, 此处为org.example.minispring.action.DemoAction
    // 2. IoC.get(beanName): 根据beanName获取到对应的bean实例:org.example.minispring.action.DemoAction@51e3ce14
    // 3. method.invoke调用的就是[email protected](req, resp, name)
    // String beanName = method.getDeclaringClass().getName();
    // method.invoke(IoC.get(beanName), new Object[] { req, resp, parameterMap.get("name")[0] });

    // url参数的动态处理
    // 获取method的形参列表
    Class<?>[] parameterTypes = method.getParameterTypes();
    // 保存赋值参数的位置
    Object[] paramValues = new Object[parameterTypes.length];
    // 根据参数的位置动态赋值
    for (int i = 0; i < parameterTypes.length; i++) {
        Class<?> parameterType = parameterTypes[i];
        if (parameterType == HttpServletRequest.class) {
            paramValues[i] = req;
            continue;
        } else if (parameterType == HttpServletResponse.class) {
            paramValues[i] = resp;
            continue;
        } else if (parameterType == String.class) {
            // 提取方法中加了注解的参数
            Annotation[][] pa = method.getParameterAnnotations();
            for (int j = 0; j < pa.length; j++) {
                for (Annotation a : pa[j]) {
                    if (a instanceof MyRequestParam) {
                        String paramName = ((MyRequestParam) a).value().trim();
                        if (!"".equals(paramName)) {
                            String paramValue = Arrays.toString(parameterMap.get(paramName))
                                .replaceAll("\\[|\\]", "")
                                .replaceAll("\\s", ",");
                            paramValues[i] = paramValue;
                        }
                    }
                }
            }
        }
    }
    String beanName = method.getDeclaringClass().getName();
    // 根据动态获取的参数去invoke method
    method.invoke(IoC.get(beanName), paramValues);
}

运行演示

到此为止我们就实现了 mini-spring 的 2.0 版本。

2.0 版本在 1.0 基础上做了优化,但是还有很多工作要做,例如 HandlerMapping 还不能像 SpringMVC 一样支持正则,url 参数还不支持类型转换,在 3.0 版本中我们将继续优化,请看下篇 用 300 行代码手写提炼 Spring 核心原理 [3]

参考

[1] 《Spring 5 核心原理与 30 个类手写实战》,谭勇德著。

标签:String,300,Spring,beanName,example,url,手写,IoC,method
From: https://www.cnblogs.com/myownswordsman/p/-/mini-spring-2

相关文章

  • 解决spring后端传前端数值为空的问题
    问题:在开发当中,由于我的数据传输从DTO在某些场景下,其中的部分字段并不需求进行值的传递,但在其他功能当中需要;(比如开发题目模块时,查询题目采用同一接口,根据题目id不同,后台判断其为多选还是单选进行回传给dto给前端)。导致出现了如下情况的诸多null值,而这些是没有作用但又不可删除的......
  • 用 300 行代码手写提炼 Spring 核心原理 [1]
    手写一个mini版本的Spring框架是一个很好的实践项目,可以让你对框架的核心概念和实现有更深刻的理解。接下来我们从0-1逐层深入,一步一步揭开Spring的神秘面纱。自定义配置配置application.properties为了解析方便,我们用application.properties来代替application.......
  • 用 300 行代码手写提炼 Spring 核心原理 [1]
    手写一个mini版本的Spring框架是一个很好的实践项目,可以让你对框架的核心概念和实现有更深刻的理解。接下来我们从0-1逐层深入,一步一步揭开Spring的神秘面纱。自定义配置配置application.properties为了解析方便,我们用application.properties来代替application.......
  • Elastic Search基于Spring Boot实现复杂查询和对复杂查询结果的映射银行账户对象并获
    packagecom.alatus.search;importcom.alatus.search.config.MallElasticSearchConfig;importcom.alibaba.fastjson.JSON;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importlombok.ToString;importorg.elasticsearch.......
  • 【深度学习入门项目】多层感知器(MLP)实现手写数字识别
    多层感知器(MLP)实现手写数字识别导入必要的包获得软件包的版本信息下载并可视化数据查看一个batch的数据查看图片细节信息设置随机种子定义模型架构Buildmodel_1Buildmodel_2TraintheNetwork(30marks)Trainmodel_1Trainmodel_1Visualizethetrainingprocess......
  • 毕业设计&毕业项目:基于springboot+vue实现的在线音乐平台
    一、前言        在当今数字化时代,音乐已经成为人们生活中不可或缺的一部分。随着技术的飞速发展,构建一个用户友好、功能丰富的在线音乐平台成为了许多开发者和创业者的目标。本文将介绍如何使用SpringBoot作为后端框架,结合Vue.js作为前端框架,共同实现一个高效、可扩展的......
  • 计算机Java项目|基于SpringBoot的高校办公室行政事务管理系统
    作者主页:编程指南针作者简介:Java领域优质创作者、CSDN博客专家、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验,被多个学校常年聘为校外企业导师,指导学生毕业设计并参与学生毕业答辩指导,有较为丰富的相关经验。期待与......
  • A144-基于SpringBoot的大学生心理健康咨询系统(源码+数据库+文档+包运行)
    项目简介这是一个基于SpringBoot框架开发的在线心理测评管理系统,主要分为两个角色:管理员和用户。系统提供了一系列功能,旨在方便管理员和用户进行相关操作。管理员角色功能登录:管理员可以通过登录功能进入系统。首页展示:展示系统的概要信息或重要通知。文章管理:管理系统内的......
  • 基于springboot+vue的治安管理系统
    博主主页:猫头鹰源码博主简介:Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万+、专注Java技术领域和毕业设计项目实战,欢迎高校老师\讲师\同行交流合作​主要内容:毕业设计(Javaweb项目|小程序|Python|HTML|数据可视化|SSM|SpringBoot|Vue|Jsp|PHP......
  • 基于协同过滤推荐算法+springboot+vue的校园二手商城(前后端分离)
    博主主页:猫头鹰源码博主简介:Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万+、专注Java技术领域和毕业设计项目实战主要内容:毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询文末联系获取项目介绍: 本系统为原创项目,采用前后端分......