系列文章

手写一个 mini 版本的 Spring 框架是一个很好的实践项目,可以让你对框架的核心概念和实现有更深刻的理解。接下来我们从 0-1 逐层深入,一步一步揭开 Spring 的神秘面纱。

自定义配置

配置 application.properties

为了解析方便,我们用 application.properties 来代替 application.xml 文件,具体的配置内容如下:

scanPackage=org.example.minispring

配置 web.xml

所有依赖于 Web 容器的项目都是从读取 web.xml 文件开始的,我们先配置好 web.xml 中的内容:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<display-name>MiniSpring</display-name>
<servlet>
<servlet-name>dispatcher-servlet</servlet-name>
<servlet-class>org.example.minispring.framework.v1.MyDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher-servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>

其中 MyDispatcherServlet 是模拟 Spring 实现的核心功能类。

自定义注解

  • @MyService 注解
package org.example.minispring.framework.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyService {
String value() default "";
}
  • @MyAutowired 注解
package org.example.minispring.framework.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAutowired {
String value() default "";
}
  • @MyController 注解
package org.example.minispring.framework.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyController {
String value() default "";
}
  • @MyRequestMapping 注解
package org.example.minispring.framework.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface MyRequestMapping {
String value() default "";
}
  • @MyRequestParam 注解
package org.example.minispring.framework.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface MyRequestParam {
String value() default "";
}

配置注解

  • 配置业务实现类 DemoService

按照规范,先定义接口类 IDemoService

package org.example.minispring.service;

public interface IDemoService {
public String get(String name);
}

再定义实现类:

package org.example.minispring.service.impl;

import org.example.minispring.framework.annotation.MyService;
import org.example.minispring.service.IDemoService; @MyService
public class DemoService implements IDemoService { @Override
public String get(String name) {
return "Hello " + name + "!";
}
}
  • 配置请求入口类 DemoAction
package org.example.minispring.action;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.example.minispring.framework.annotation.MyAutowired;
import org.example.minispring.framework.annotation.MyController;
import org.example.minispring.framework.annotation.MyRequestMapping;
import org.example.minispring.framework.annotation.MyRequestParam;
import org.example.minispring.service.IDemoService; @MyController
@MyRequestMapping("/demo")
public class DemoAction { @MyAutowired
private IDemoService demoService; @MyRequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp,
@MyRequestParam("name") String name) {
String result = demoService.get(name);
try {
resp.getWriter().write("<html><h2>" + result + "</h2></html>");
} catch (IOException e) {
e.printStackTrace();
}
}
}

容器初始化

所有核心逻辑全部写在 MyDispatcherServlet 的 init() 方法中:

package org.example.minispring.framework.v1;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties; import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.example.minispring.framework.annotation.MyAutowired;
import org.example.minispring.framework.annotation.MyController;
import org.example.minispring.framework.annotation.MyRequestMapping;
import org.example.minispring.framework.annotation.MyService; public class MyDispatcherServlet extends HttpServlet { private static final long serialVersionUID = 1L; // 保存beanName -> bean的映射关系
private Map<String, Object> beanMapping = new HashMap<>();
// 保存url -> method的映射关系
private Map<String, Object> handlerMapping = new HashMap<>(); @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()));
}
} 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找到对应的方法
// 此处method为DemoAction.query(HttpServletRequest, HttpServletResponse, String)
Method method = (Method) this.handlerMapping.get(url);
// 获取请求参数, 此处为: name = [浏览器传来的值]
Map<String, String[]> params = req.getParameterMap(); // 1. method.getDeclaringClass().getName()
// 本例为org.example.minispring.action.DemoAction
// 2. beanMapping.get(beanName): 根据beanName获取到对应的bean实例,例如:
// org.example.minispring.action.DemoAction@51e3ce14
// 3. method.invoke调用的就是
// org.example.minispring.action.DemoAction@51e3ce14.query(req, resp, name)
String beanName = method.getDeclaringClass().getName();
method.invoke(this.beanMapping.get(beanName), new Object[] { req, resp, params.get("name")[0] });
} @Override
public void init(ServletConfig config) throws ServletException {
InputStream is = this.getClass().getClassLoader()
.getResourceAsStream(config.getInitParameter("contextConfigLocation"));
Properties configContext = new Properties();
try {
configContext.load(is);
String scanPackage = configContext.getProperty("scanPackage");
// 扫描相关的类,本例中scanPackage=org.example.minispring
doScanner(scanPackage); for (String className : beanMapping.keySet()) {
Class<?> clazz = Class.forName(className);
// 解析@MyController注解
if (clazz.isAnnotationPresent(MyController.class)) {
// 保存className和@MyController实例的对应关系
beanMapping.put(className, clazz.newInstance());
String baseUrl = "";
// 解析@MyController上的@MyRequestMapping注解,作为当前Controller的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);
}
}
// 解析@MyService注解
else if (clazz.isAnnotationPresent(MyService.class)) {
MyService service = clazz.getAnnotation(MyService.class);
String beanName = service.value();
if ("".equals(beanName)) {
beanName = clazz.getName();
}
Object instance = clazz.newInstance();
// 保存className和@MyService实例的对应关系
beanMapping.put(beanName, instance);
for (Class<?> i : clazz.getInterfaces()) {
beanMapping.put(i.getName(), instance);
}
}
} // 解析对象之间的依赖关系,依赖注入
for (Object object : beanMapping.values()) {
if (object == null) {
continue;
}
Class<?> clazz = object.getClass();
// 向MyController中注入MyService
if (clazz.isAnnotationPresent(MyController.class)) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(MyAutowired.class)) {
continue;
}
MyAutowired autowired = field.getAnnotation(MyAutowired.class);
String beanName = autowired.value();
if ("".equals(beanName)) {
beanName = field.getType().getName();
}
// 只要加了@MyAutowired注解都要强制赋值
// 反射中叫做暴力访问
field.setAccessible(true);
// 用反射机制动态给字段赋值
// 赋值后DemoAction.demoService = DemoService@c97ae21
// 也即DemoService实例被注入到了DemoAction对象中,这就是依赖注入
field.set(beanMapping.get(clazz.getName()), beanMapping.get(beanName));
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} /**
* 扫描相关的类,本例中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 {
String clazzName = scanPackage + "." + file.getName().replace(".class", "");
beanMapping.put(clazzName, null);
}
}
}
}

运行演示

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

1.0 版本具备了初步的功能,但是代码不够优雅,接下来我们在此基础上进行优化,采用常用的设计模式(工厂模式、单例模式、委派模式、策略模式)将 init() 方法中的代码进行封装,请看下篇 用 300 行代码手写提炼 Spring 核心原理 [2]

参考

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

用 300 行代码手写提炼 Spring 核心原理 [1]的更多相关文章

  1. 【面试题】手写async await核心原理,再也不怕面试官问我async await原理

    前言 async await 语法是 ES7出现的,是基于ES6的 promise和generator实现的 generator函数 在之前我专门讲个generator的使用与原理实现,大家没了解过的 ...

  2. 30个类手写Spring核心原理之环境准备(1)

    本文节选自<Spring 5核心原理> 1 IDEA集成Lombok插件 1.1 安装插件 IntelliJ IDEA是一款非常优秀的集成开发工具,功能强大,而且插件众多.Lombok是开 ...

  3. 自定义控件?试试300行代码实现QQ侧滑菜单

    Android自定义控件并没有什么捷径可走,需要不断得模仿练习才能出师.这其中进行模仿练习的demo的选择是至关重要的,最优选择莫过于官方的控件了,但是官方控件动辄就是几千行代码往往可能容易让人望而却 ...

  4. 带你手写基于 Spring 的可插拔式 RPC 框架(一)介绍

    概述 首先这篇文章是要带大家来实现一个框架,听到框架大家可能会觉得非常高大上,其实这和我们平时写业务员代码没什么区别,但是框架是要给别人使用的,所以我们要换位思考,怎么才能让别人用着舒服,怎么样才能让 ...

  5. 通过 Mesos、Docker 和 Go,使用 300 行代码创建一个分布式系统

    [摘要]虽然 Docker 和 Mesos 已成为不折不扣的 Buzzwords ,但是对于大部分人来说它们仍然是陌生的,下面我们就一起领略 Mesos .Docker 和 Go 配合带来的强大破坏力 ...

  6. 【Xamarin挖墙脚系列:代码手写UI,xib和StoryBoard间的博弈,以及Interface Builder的一些小技巧(转)】

    正愁如何选择构建项目中的视图呢,现在官方推荐画板 Storybord...但是好像 xib貌似更胜一筹.以前的老棒子总喜欢装吊,用代码写....用代码堆一个HTML页面不知道你们尝试过没有.等页面做出 ...

  7. Python:游戏:300行代码实现俄罗斯方块

    本文代码基于 python3.6 和 pygame1.9.4. 俄罗斯方块是儿时最经典的游戏之一,刚开始接触 pygame 的时候就想写一个俄罗斯方块.但是想到旋转,停靠,消除等操作,感觉好像很难啊, ...

  8. 代码手写UI,xib和StoryBoard间的博弈,以及Interface Builder的一些小技巧

    近期接触了几个刚入门的iOS学习者,他们之中存在一个普遍和困惑和疑问.就是应该怎样制作UI界面.iOS应用是非常重视用户体验的,能够说绝大多数的应用成功与否与交互设计以及UI是否美丽易用有着非常大的关 ...

  9. 通过Mesos、Docker和Go,使用300行代码创建一个分布式系统

    [摘要]虽然 Docker 和 Mesos 已成为不折不扣的 Buzzwords ,但是对于大部分人来说它们仍然是陌生的,下面我们就一起领略 Mesos .Docker 和 Go 配合带来的强大破坏力 ...

  10. 关于代码手写UI,xib和StoryBoard

    代码手写UI 这种方法经常被学院派的极客或者依赖多人合作的大型项目大规模使用.Geek们喜欢用代码构建UI,是因为代码是键盘敲出来的,这样可以做到不开IB,手不离开键盘就完成工作,可以专注于编码环境, ...

随机推荐

  1. C#/.NET/.NET Core定时任务调度的方法或者组件[转载]

    原文由Rector首发于 码友网 之 <C#/.NET/.NET Core应用程序编程中实现定时任务调度的方法或者组件有哪些,Timer,FluentScheduler,TaskSchedule ...

  2. csdn 下载券恶心之处

    今天在csdn碰到一个恶心事,啥事呢?下载券.详细的说,就是人家码友把下载积分都设置成0了,让大家自行下载.结果,却不行,非得搞个下载券,得去做任务,给它的广告爹爹们点点任务才能获取下载券的code. ...

  3. Android Camera2Video整合到自己项目里

    背景: Android项目里调用摄像头拍摄视频,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后来因项目需要,改成了camera2 1.Camera2Video 官方d ...

  4. CM3和ARM7的差异

    此文章由文心一言生成,引用请标注作者:文心一言CM3通常指的是Cortex-M3,它是ARM公司设计的一种基于ARMv7-M架构的32位处理器内核,主要用于嵌入式系统.而ARM7则是ARM公司早期设计 ...

  5. WebRTC 简单入门与实践

    一.前言 WebRTC 技术已经广泛在各个行业及场景中被应用,但对多数开发者来说,实时音视频及相关技术却是比较不常接触到的. 做为一名 Web 开发者,WebRTC 这块的概念着实花了不少时间才搞明白 ...

  6. 小tips:怎样实现简单的前端hash与history路由方式?

    前端路由实现方式,主要有两种,分别是history和hash模式. hash模式 不同路由对应的hash是不一样的,如何能够监听到URL中关于hash部分发生的变化?浏览器已经暴露给我们一个现成的方法 ...

  7. QT与JavaScript之QT6应用程序与JavaScript脚本集成开发:C++应用程序能与JavaScript实现相互调用吗?

    简介 QT6框架中提供了JavaScript引擎类型QJSEngine ,可用于实现 C++应用程序和JavaScript代码之间的相互调用. 目录 什么是 ECMAScript ? JavaScri ...

  8. 队列之ring_buffer优雅实现--附个人代码理解

    1. 下面张贴实现该队列仅需的两个头文件:ring_buffer_iterator.h 和 fifo_iterator.h ring_buffer_iterator.h 1 /* 2 * 3 * Th ...

  9. [TK] Tourist Attractions

    题目描述 给出一张有 \(n\) 个点 \(m\) 条边的无向图,每条边有边权. 你需要找一条从 \(1\) 到 \(n\) 的最短路径,并且这条路径在满足给出的 \(g\) 个限制的情况下可以在所有 ...

  10. Windows右下角时间显示具体星期

    事件起因: 有时候脑子不清楚,过着过着就会忘记今天是星期几,错过一些重要事情,于是乎就想看看Windows右下角能不能显示到具体星期,果然在查了资料之后这个需求可以达成 解决办法: 控制面板 - 日期 ...