前言

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源码的更多相关文章

  1. Spring学习之——手写Spring源码V2.0(实现IOC、D、MVC、AOP)

    前言 在上一篇<Spring学习之——手写Spring源码(V1.0)>中,我实现了一个Mini版本的Spring框架,在这几天,博主又看了不少关于Spring源码解析的视频,受益匪浅,也 ...

  2. 手写mini版MVC框架

    目录 1, Springmvc基本原理流程 2,注解开发 编写测试代码: 目录结构: 3,编写自定义DispatcherServlet中的初始化流程: 3.1 加载配置文件 3.2 扫描相关的类,扫描 ...

  3. OpenCV+TensorFlow图片手写数字识别(附源码)

    初次接触TensorFlow,而手写数字训练识别是其最基本的入门教程,网上关于训练的教程很多,但是模型的测试大多都是官方提供的一些素材,能不能自己随便写一串数字让机器识别出来呢?纸上得来终觉浅,带着这 ...

  4. javaScript(js)手写原生任务定时器源码

    javaScript(js)手写原生任务定时器 功能介绍 定时器顾名思义就是在某个特定的时间去执行一些任务,现代的应用程序早已不是以前的那些由简单的增删改查拼凑而成的程序了,高复杂性早已是标配,而任务 ...

  5. Spring系列之手写一个SpringMVC

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 Spring系列之AOP的原理及手动实现 Spring系列之手写注解与配置文件的解析 引言 在前面的几个章节中我 ...

  6. Spring Boot启动命令参数详解及源码分析

    使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring ...

  7. Spring PropertyResolver 占位符解析(二)源码分析

    Spring PropertyResolver 占位符解析(二)源码分析 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) ...

  8. 6 手写Java LinkedHashMap 核心源码

    概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...

  9. 3 手写Java HashMap核心源码

    手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...

随机推荐

  1. 关于display的box和flex布局

    关于二者的区别于联系,在知乎上看到有人这么回答的 flex 2012年的语法,也将是以后标准的语法,大部分浏览器已经实现了无前缀版本. box是2009年的语法,已经过时,是需要加上对应前缀的. 另外 ...

  2. MySQL 查询 存储过程 视图 触发器 函数 索引 建表语句 数据库版本 当前登录用户 当前数据库名称

    MySQL 查询 存储过程 视图 触发器 函数 索引 建表语句 数据库版本 当前登录用户 当前数据库名称   INFORMATION_SCHEMA.TABLES INFORMATION_SCHEMA. ...

  3. 小师妹学JVM之:cache line对代码性能的影响

    目录 简介 一个奇怪的现象 两个问题的答案 CPU cache line inc 和 add 总结 简介 读万卷书不如行万里路,讲了这么多assembly和JVM的原理与优化,今天我们来点不一样的实战 ...

  4. Github和Azure DevOps的代码同步

    [前言]Github和Azure DevOps都提供了Git代码库功能,那么有没有办法将两边的代码库进行同步呢,答案是肯定的.这里的操作我都是用Azure DevOps的Pipelines功能来完成的 ...

  5. Django---drf入门

    目录 1 web开发模式 2 api接口 3 postman的使用 4 Restful规范(重点) 5 drf的安装和简单使用 3 cbv源码 4 APIView源码分析 1 web开发模式 #前后端 ...

  6. 重学c#系列——对c#粗浅的认识(一)

    前言 什么是c#呢? 首先你是如何读c#的呢?c sharp?或者c 井? 官方读法是:see sharp. 有没有发现开发多年,然后感觉名字不对. tip:为个人重新整理,如学习还是看官网,c# 文 ...

  7. NEST教程系列:推断索引名

    NEST教程系列:三种推断索引名写法 目录 NEST教程系列:三种推断索引名写法 连接时设置默认索引 设置 .NET 类映射索引名 在创建请求的时候直接显式指定索引名 总结 连接时设置默认索引 构建 ...

  8. Scala 面向对象(一):类与对象基础(一)

    1 如何定义类 [修饰符] class 类名 { 类体 } 定义类的注意事项 1)scala语法中,类并不声明为public,所有这些类都具有公有可见性(即默认就是public), 2)一个Scala ...

  9. JVM 专题十一:运行时数据区(六)方法区

    1. 栈.堆.方法区关系交互 运行时数据区结构图: 从线程共享与否的角度来看: 2. 方法区的理解 2.1 方法区在哪里? <Java虚拟机规范>中明确说明:“尽管所有的方法区在逻辑上属于 ...

  10. 数据库01 /Mysql初识、基本指令、数据库密码相关、创建用户及授权

    数据库01 /Mysql初识.基本指令.数据库密码相关.创建用户及授权 目录 数据库01 /Mysql初识.基本指令.数据库密码相关.创建用户及授权 1. 数据库概述 2. 数据库管理系统/DBMS ...