Spring学习之——手写Mini版Spring源码
前言
Sping的生态圈已经非常大了,很多时候对Spring的理解都是在会用的阶段,想要理解其设计思想却无从下手。前些天看了某某学院的关于Spring学习的相关视频,有几篇讲到手写Spring源码,感觉有些地方还是说的挺好的,让博主对Spring的理解又多了一些,于是在业余时间也按照视频讲解实现一遍SpringIOC、DI、MVC的设计思想,加深巩固记忆,也便于初学者理解,我们不用重复造轮子,但得知道轮子怎么造。
开发工具
环境:jdk8 + IDEA + maven
jar包:javax.servlet-2.5
实现步骤

视频中这张图画得很好,我们按照这张图来概括一下实现步骤
1.配置阶段:即配置web.xml和application.properties,以及相关自定义注解;
2.初始化阶段:初始化Ioc容器,将扫描到的类实例化交给IoC容器管理,然后注入实例的各属性,接着将请求路径与执行方法的映射加载到HandlerMapping中;
3.运行阶段:由HandlerMapping根据请求路径将请求分发到指定方法,返回执行结果。
附上自己的项目结构供参考

具体实现
配置阶段
application.properties 配置扫描路径,这里为了方便,所以没有使用xml文件去解析
scanPackage=com.wqfrw
web.xml 配置Servlet和application文件的路径
<servlet>
<servlet-name>mymvc</servlet-name>
<servlet-class>com.framework.servlet.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>mymvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
pom.xml 引入servlet.jar
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
相关自定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAutowired {
String value() default "";
} @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyController {
String value() default "";
} @Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {
String value() default "";
} @Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestParam {
String value() default "";
} @Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyService {
String value() default "";
}
初始化阶段
这部分的操作才是最重要的,注释我都详细的写在代码中
MyDispatcherServlet
public class MyDispatcherServlet extends HttpServlet {
//IoC容器
private Map<String, Object> ioc = new HashMap<>();
//加载配置文件对象
private Properties contextConfig = new Properties();
//扫描到的类名带包路径
private List<String> classNameList = new ArrayList<>();
//url与方法的映射 也就是请求分发器
private Map<String, Method> handlerMapping = new HashMap<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req, resp);
} catch (Exception e) {
e.printStackTrace();
resp.getWriter().write("500 Exception Detail:" + Arrays.toString(e.getStackTrace()));
}
}
@Override
public void init(ServletConfig config) throws ServletException {
//加载配置文件 拿到需要扫描的包路径
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//实例化Bean到IoC容器
doInstance();
//依赖注入(DI)
doAutowired();
//初始化HandlerMapping url和对应方法的键值对
doInitHandlerMapping();
//初始化完成
System.out.println("MySpring framework is init!");
}
/**
* 功能描述: 接收到浏览器的请求,执行方法
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年07月28日 20:54:04
* @param req
* @param resp
* @return: void
**/
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath,"");
//根据请求路径如果没有找到对应的方法则抛出404错误
if (!handlerMapping.containsKey(url)) {
resp.getWriter().write("404 Not Found!");
return;
}
Map<String,String[]> params = req.getParameterMap();
Method method = handlerMapping.get(url);
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
//方法调用
method.invoke(ioc.get(beanName),new Object[]{req,resp,params.get("name")[0]});
}
/**
* 功能描述: 初始化HandlerMapping
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年07月28日 20:56:09
* @param
* @return: void
**/
private void doInitHandlerMapping() {
ioc.forEach((k, v) -> {
Class<?> clazz = v.getClass();
//加了MyController注解的类才操作
if (clazz.isAnnotationPresent(MyController.class)) {
String baseUrl = "";
//如果类上面加了MyRequestMapping注解,则需要拿到url进行拼接
if (clazz.isAnnotationPresent(MyRequestMapping.class)) {
MyRequestMapping annotation = clazz.getAnnotation(MyRequestMapping.class);
baseUrl = annotation.value();
}
//获取所有public修饰的方法
Method[] methods = clazz.getMethods();
//过滤拿到所有MyRequestMapping注解的方法,put到handlerMapping中
String finalBaseUrl = baseUrl;
Stream.of(methods)
.filter(m -> m.isAnnotationPresent(MyRequestMapping.class))
.forEach(m -> {
MyRequestMapping annotation = m.getAnnotation(MyRequestMapping.class);
String url = (finalBaseUrl + annotation.value()).replaceAll("/+", "/");
handlerMapping.put(url, m);
});
}
});
}
/**
* 功能描述: 依赖注入(DI)
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年07月28日 20:55:57
* @param
* @return: void
**/
private void doAutowired() {
//循环IoC容器中所管理的对象 注入属性
ioc.forEach((k, v) -> {
//拿到bean所有的字段 包括private、public、protected、default
Field[] Fields = v.getClass().getDeclaredFields();
//过滤拿到所有加了MyAutowired注解的字段并循环注入
Stream.of(Fields)
.filter(f -> f.isAnnotationPresent(MyAutowired.class))
.forEach(f -> {
MyAutowired annotation = f.getAnnotation(MyAutowired.class);
String beanName = annotation.value().trim();
if ("".equals(beanName)) {
beanName = f.getType().getName();
}
//强制访问
f.setAccessible(true);
try {
//赋值
f.set(v, ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
});
}
/**
* 功能描述: 实例化bean至IoC容器
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年07月28日 20:55:39
* @param
* @return: void
**/
private void doInstance() {
classNameList.forEach(v -> {
try {
Class<?> clazz = Class.forName(v);
//只初始化加了MyController注解和MyService注解的类
if (clazz.isAnnotationPresent(MyController.class)) {
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, clazz.newInstance());
} else if (clazz.isAnnotationPresent(MyService.class)) {
// 1.默认首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
// 2.自定义beanName
MyService myService = clazz.getAnnotation(MyService.class);
if (!"".equals(myService.value())) {
beanName = myService.value();
}
// 3.如果是接口 必须创建实现类的实例
for (Class<?> i : clazz.getInterfaces()) {
if (ioc.containsKey(i)) {
throw new Exception("This beanName is exists!");
}
beanName = i.getName();
}
//将实例化的对象放入IoC容器中
ioc.put(beanName, clazz.newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
/**
* 功能描述: 扫描相关的类 加入到classNameList
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年07月28日 20:55:10
* @param scanPackage
* @return: void
**/
private void doScanner(String scanPackage) {
//获取根目录 拿到com.wqfry替换成/com/wqfrw
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classFile = new File(url.getFile());
for (File file : classFile.listFiles()) {
//如果file是文件夹 则递归调用
if (file.isDirectory()) {
doScanner(scanPackage + "." + file.getName());
} else {
//如果非class文件 则跳过
if (!file.getName().endsWith(".class")) {
continue;
}
String className = (scanPackage + "." + file.getName()).replace(".class", "");
//类名+包路径放入到类名集合中 方便后续实例化
classNameList.add(className);
}
}
}
/**
* 功能描述: 加载配置文件
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年07月28日 20:54:57
* @param contextConfigLocation
* @return: void
**/
private void doLoadConfig(String contextConfigLocation) {
InputStream resource = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
//加载配置文件
contextConfig.load(resource);
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭文件流
if (resource != null) {
try {
resource.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 首字母转小写
*
* @param className
* @return
*/
private String toLowerFirstCase(String className) {
char[] chars = className.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
}
运行阶段
部分代码在上面,这里我贴出Controller与Server的代码
TestController
@MyController
@MyRequestMapping("/test")
public class TestController { @MyAutowired
private ITestService testService; @MyRequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp, @MyRequestParam("name") String name){
String result = testService.query(name);
try {
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
} @MyRequestMapping("/add")
public void add(HttpServletRequest req, HttpServletResponse resp, @MyRequestParam("name") String name){
System.out.println("add");
} @MyRequestMapping("/remove")
public void remove(HttpServletRequest req, HttpServletResponse resp, @MyRequestParam("name") String name){
System.out.println("remove");
} }
ITestService
public interface ITestService {
String query(String name);
}
TestServiceImpl
@MyService
public class TestServiceImpl implements ITestService { @Override
public String query(String name) {
return "hello " + name + "!";
} }
实际调用

总结
以上只是简略实现了Spring的核心思想,真正的Spring当然要比此复杂许多,但是学习都是由浅至深的,希望大家不仅会用工具,并且都能知道为什么要这样用。
附上原视频链接:https://live.gupaoedu.com/watch/1284875
Spring学习之——手写Mini版Spring源码的更多相关文章
- Spring学习之——手写Spring源码V2.0(实现IOC、D、MVC、AOP)
前言 在上一篇<Spring学习之——手写Spring源码(V1.0)>中,我实现了一个Mini版本的Spring框架,在这几天,博主又看了不少关于Spring源码解析的视频,受益匪浅,也 ...
- 手写mini版MVC框架
目录 1, Springmvc基本原理流程 2,注解开发 编写测试代码: 目录结构: 3,编写自定义DispatcherServlet中的初始化流程: 3.1 加载配置文件 3.2 扫描相关的类,扫描 ...
- OpenCV+TensorFlow图片手写数字识别(附源码)
初次接触TensorFlow,而手写数字训练识别是其最基本的入门教程,网上关于训练的教程很多,但是模型的测试大多都是官方提供的一些素材,能不能自己随便写一串数字让机器识别出来呢?纸上得来终觉浅,带着这 ...
- javaScript(js)手写原生任务定时器源码
javaScript(js)手写原生任务定时器 功能介绍 定时器顾名思义就是在某个特定的时间去执行一些任务,现代的应用程序早已不是以前的那些由简单的增删改查拼凑而成的程序了,高复杂性早已是标配,而任务 ...
- Spring系列之手写一个SpringMVC
目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 Spring系列之AOP的原理及手动实现 Spring系列之手写注解与配置文件的解析 引言 在前面的几个章节中我 ...
- Spring Boot启动命令参数详解及源码分析
使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring ...
- Spring PropertyResolver 占位符解析(二)源码分析
Spring PropertyResolver 占位符解析(二)源码分析 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) ...
- 6 手写Java LinkedHashMap 核心源码
概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...
- 3 手写Java HashMap核心源码
手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...
随机推荐
- Python之浅谈函数
目录 文件的高级应用 文件修改的两种方式 第一种 第二种 函数的定义 函数的参数 函数的返回值 文件的高级应用 r+即可读又可写,并且是在后面追加 w+清空文件的功能是w提供的 a+a有追加的功能,a ...
- Linux 递归批量删除文件夹或文件的命令
笔者有一次Linux服务器上的网站被别人在每一个文件夹下恶意注入了目录和文件,由于项目的目录太多,手动删除费时费力,最后用Linux的命令批量删除了注入内容.相关命令介绍如下: 递归批量删除文件夹: ...
- HTML5(四)Drag and Drop
HTML5 拖放(Drag 和 Drop) 拖放 拖放是一种常见的特性,即抓取对象以后拖到另一个位置. 在 HTML5 中,拖放是标准的一部分,任何元素都能够拖放. 设置元素为可拖放 首先,为了使元素 ...
- Google 出品的 Java 编码规范,强烈推荐,既权威又科学
这份文档是 Google Java 编程风格规范的完整定义.当且仅当一个Java源文件符合此文档中的规则, 我们才认为它符合Google的Java编程风格.原文:google.github.io/st ...
- 数据可视化之PowerQuery篇(四)二维表转一维表,看这篇文章就够了
https://zhuanlan.zhihu.com/p/69187094 数据分析的源数据应该是规范的,而规范的其中一个标准就是数据源应该是一维表,它会让之后的数据分析工作变得简单高效. 在之前的文 ...
- ES6语法——Promise对象
一.概念 Promise是异步编程的一种解决方案(解决回调地狱的问题),是一个能够获取异步操作信息的对象.Promise的内部保存着某个未来才会结束的事件(通常是一个异步操作) 二.特点 1.Prom ...
- Sharding-Proxy的基本功能使用
Sharding-Proxy是一个分布式数据库中间件,定位为透明化的数据库代理端.作为开发人员可以完全把它当成数据库,而它具体的分片规则在Sharding-Proxy中配置.它的整体架构图如下: 在架 ...
- STL源码剖析:配接器
启 配接器就是适配器 STL中的适配器一共三种: 迭代器适配器 是一种观念上的改变,如将赋值操作变成插入,前进变成后退,等 函数适配器 STL中最广泛的配接器群体 可以实现连续配接 配接操作:bind ...
- 17 个 Python 特别实用的操作技巧,记得收藏!
Python 是一门非常优美的语言,其简洁易用令人不得不感概人生苦短.在本文中,作者 Gautham Santhosh 带我们回顾了 17 个非常有用的 Python 技巧,例如查找.分割和合并列表等 ...
- 一个有趣的问题, 你知道SqlDataAdapter中的Fill是怎么实现的吗
一:背景 1. 讲故事 最近因为各方面原因换了一份工作,去了一家主营物联柜的公司,有意思的是物联柜上的终端是用 wpf 写的,代码也算是年久失修,感觉技术债还是蛮重的,前几天在调试一个bug的时候,看 ...