Spring 深度学习 — 手写简易版 SpringMVC【思想学习】

前言

通过简单的代码,来学习 Spring 的设计思想,掌握 Spring 框架的基本脉络。主要目的是为了学习思想,有了思想之后,我们后面再学习源码的时候,就会相对轻松许多。

一、 mini 版 Spring MVC 基本架构设计

基本思路分为三步,因为是 mini 版,所以只是简单的实现基本功能。具体步骤如下:

1、配置阶段

配置 web.xml  --> DispatchServlet

设定 init-param  --> contextConfigLocation=classPath:application.xml

设定 url-pattern  --> /*

配置 Annotation  --> @Controller @Service @Autowrited @RequestMapping ...

2、初始化阶段

调用 init 方法  --> 加载配置文件

IOC 容器初始化  --> Map<String,Object>

扫描相关的类  --> scan-package = "xxx"

创建实例并保存到容器  --> 通过反射机制将类实例化放到 IOC 容器中

进行 DI 操作  --> 扫描 IOC 容器中的实例,给没有赋值的属性自动赋值

初始化 HandlerMapping  --> 将一个 URL 和一个 Method 进行一对一的关联映射 Map<String,Method>

3、运行阶段

调用 doPost() / doGet() --> Web容器调用 doPost/doGet方法,获得 request / response 对象

匹配 HandlerMapping  --> 从 request 对象中获得用户输入的 URL,找到其对应的 Method

反射调用 method.invoker() --> 利用反射调用方法并返回结果

response.getWrite().write() --> 将返回结果,输出到浏览器

二、mini 版 SpringMVC 的具体代码实现

        清楚了基本的设计思想之后,我们根据上面的三个步骤来初步手写自己的 mini 版 MVC。

1. 自定义配置

配置 application.properties文件

为了解析方便,我们这里使用 application.properties 文件来代替 application.xml,具体配置如下,很简单:

scanPackage=com.mmt.mini

 配置 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/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
         version="2.4">
    <display-name>Mini Web Application</display-name>
    <servlet>
        <servlet-name>mmt-mini-mvc</servlet-name>
        <!-- 最主要的是这个类 -->
        <servlet-class>com.mmt.mini.mvc.framework.MmtDispatchServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:application.properties</param-value>
        </init-param>

        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>mmt-mini-mvc</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

自定义 annotation

编写我们基本使用的几个常用注解,如:@controller、@service等,只不过,这里是完全使用我们自己的写的注解,包括项目中的依赖,都只是单纯了引入了一个 servlet 的依赖,和 spring 相关的包,一个都没有使用,先贴一下 pom.xml 中的依赖:

自定义 @MmtController

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MmtController {

	String value() default "";
}

自定义 @MmtService

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

自定义 @MmtAutowired

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MmtAutowired {

	String value() default "";
}

自定义 @MmtRequestMapping

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MmtRequestMapping {
    String value() default "";
}

自定义 @MmtRequestParam

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

好了,基本的注解配置完成后,下面来配置业务实现类 DemoService:

package com.mmt.mini.demo.service;


public interface IDemoService {

	String get(String name);
}
package com.mmt.mini.demo.service.impl;

import com.mmt.mini.demo.service.IDemoService;
import com.mmt.mini.mvc.framework.annotation.MmtService;

/**
 * 核心业务逻辑  使用的是我们自己定义的注解
 */
@MmtService
public class DemoService implements IDemoService {

	@Override
	public String get(String name) {
		return "Hello MVC :  " + name + ",from service.";
	}

}

配置请求入口类 DemoAction :

package com.mmt.mini.demo.action;

import com.mmt.mini.demo.service.IDemoService;
import com.mmt.mini.mvc.framework.annotation.MmtAutowired;
import com.mmt.mini.mvc.framework.annotation.MmtController;
import com.mmt.mini.mvc.framework.annotation.MmtRequestMapping;
import com.mmt.mini.mvc.framework.annotation.MmtRequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@MmtController
@MmtRequestMapping("/demo")
public class DemoAction {

	@MmtAutowired
	IDemoService demoService;

	@MmtRequestMapping("/query")
	public void query(HttpServletRequest req, HttpServletResponse resp, @MmtRequestParam("name") String name) {
		String result = demoService.get(name);
		try {
			resp.getWriter().write(result);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@MmtRequestMapping("/add")
	public void add(HttpServletRequest req, HttpServletResponse resp,
					@MmtRequestParam("a") Integer a, @MmtRequestParam("b") Integer b) {
		try {
			resp.getWriter().write(a + "+" + b + "=" + (a + b));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@MmtRequestMapping("/sub")
	public void add(HttpServletRequest req, HttpServletResponse resp,
					@MmtRequestParam("a") Double a, @MmtRequestParam("b") Double b) {
		try {
			resp.getWriter().write(a + "-" + b + "=" + (a - b));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@MmtRequestMapping("/remove")
	public String remove(@MmtRequestParam("id") Integer id) {
		return "" + id;
	}

}

大家注意看,这里使用的均是上面我们自己定义的注解,和 Spring 没有关系,看起来,是不是有点那个意思了?

好了,配置完了之后,配置阶段就完毕了,下面要进入容器初始化过程。

2. 容器初始化【核心内容】

初始化话容器的时候,进入到核心类 MmtDispatchServlet 中,利用 HttpServlet 的生命中周期,进行自定义的初始化过程,这里直接上代码,通过代码和注释,有助于理解:

package com.mmt.mini.mvc.framework;

import com.mmt.mini.mvc.framework.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.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * mini mvc 入口
 *
 * @author: <a href="568227120@qq.com">heliang.wang</a>
 * @date: 2022/5/15 3:19 下午
 * @version: 1.0
 */
public class MmtDispatchServlet extends HttpServlet {

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

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

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

	/**
	 * 思考:为什么不用Map
	 * 你用Map的话,key,只能是url
	 * Handler 本身的功能就是把url和method对应关系,已经具备了Map的功能
	 * 根据设计原则:冗余的感觉了,单一职责,最少知道原则,帮助我们更好的理解
	 */
	private List<Handler> handlerMapping = new ArrayList<Handler>();

	@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 {
		//6、调用,运行阶段
		try {
			doDispatch(req, resp);
		} catch (Exception e) {
			e.printStackTrace();
			resp.getWriter().write("500 Exection,Detail : " + Arrays.toString(e.getStackTrace()));
		}
	}

	/**
	 * 运行阶段,处理具体的浏览器请求
	 *
	 * @param req
	 * @param resp
	 * @throws Exception
	 */
	private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {

		Handler handler = getHandler(req);
		if (handler == null) {
			resp.getWriter().write("404 Not Found!!!");
			return;
		}
		//获得方法的形参列表
		Class<?>[] paramTypes = handler.getParamTypes();
		Object[] paramValues = new Object[paramTypes.length];
		Map<String, String[]> params = req.getParameterMap();
		for (Map.Entry<String, String[]> parm : params.entrySet()) {
			String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]", "").replaceAll("\\s", ",");
			if (!handler.paramIndexMapping.containsKey(parm.getKey())) {
				continue;
			}
			int index = handler.paramIndexMapping.get(parm.getKey());
			paramValues[index] = convert(paramTypes[index], value);
		}

		if (handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
			int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
			paramValues[reqIndex] = req;
		}

		if (handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
			int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
			paramValues[respIndex] = resp;
		}
		// 通过反射执行方法 method.invoke
		Object returnValue = handler.method.invoke(handler.controller, paramValues);
		if (returnValue == null || returnValue instanceof Void) {
			return;
		}
		// 这里写的比较简单,直接返回浏览器了,其实还会有视图解析器的步骤,理解思想即可
		resp.getWriter().write(returnValue.toString());
	}

	/**
	 * 从request 中获取请求
	 * @param req
	 * @return
	 */
	private Handler getHandler(HttpServletRequest req) {
		if (handlerMapping.isEmpty()) {
			return null;
		}
		//绝对路径
		String url = req.getRequestURI();
		//处理成相对路径
		String contextPath = req.getContextPath();
		url = url.replaceAll(contextPath, "").replaceAll("/+", "/");

		for (Handler handler : this.handlerMapping) {
			Matcher matcher = handler.getPattern().matcher(url);
			if (!matcher.matches()) {
				continue;
			}
			return handler;
		}
		return null;
	}

	/**
	 * url传过来的参数都是String类型的,HTTP是基于字符串协议
	 * 只需要把String转换为任意类型就好
	 *
	 * @param type
	 * @param value
	 * @return
	 */
	private Object convert(Class<?> type, String value) {
		//如果是int
		if (Integer.class == type) {
			return Integer.valueOf(value);
		} else if (Double.class == type) {
			return Double.valueOf(value);
		}
		//如果还有double或者其他类型,继续加if
		//这时候,我们应该想到策略模式了
		//在这里暂时不实现,希望小伙伴自己来实现
		return value;
	}


	/**
	 * 初始化过程
	 *
	 * @throws ServletException
	 */
	@Override
	public void init(ServletConfig config) throws ServletException {
		//1、加载配置文件 <param-name>contextConfigLocation</param-name>
		doLoadConfig(config.getInitParameter("contextConfigLocation"));
		//2、扫描相关的类
		doScanner(contextConfig.getProperty("scanPackage"));
		//3、初始化扫描到的类,并且将它们放入到ICO容器之中
		doInstance();
		//4、完成依赖注入
		doAutowired();
		//5、初始化HandlerMapping
		initHandlerMapping();

		System.out.println("MMT MINI Spring framework is init.");
	}

	/**
	 * 加载配置文件
	 *
	 * @param contextConfigLocation
	 */
	private void doLoadConfig(String contextConfigLocation) {
		//直接从类路径下找到Spring主配置文件所在的路径
		//并且将其读取出来放到Properties对象中
		//相对于scanPackage=com.mmt.mini 从文件中保存到了内存中
		// 需要对路径进行处理,web.xml中配置的路径为 classpath:application.properties
		InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation.replace("classpath:", ""));
		try {
			contextConfig.load(fis);
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (null != fis) {
				try {
					fis.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * 扫描出相关的类
	 *
	 * @param scanPackage
	 */
	private void doScanner(String scanPackage) {
		//scanPackage = com.mmt.mini ,存储的是包路径
		//转换为文件路径,实际上就是把.替换为/就OK了
		//classpath
		URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
		File classPath = new File(url.getFile());
		for (File file : classPath.listFiles()) {
			if (file.isDirectory()) {
				doScanner(scanPackage + "." + file.getName());
			} else {
				if (!file.getName().endsWith(".class")) {
					continue;
				}
				String className = (scanPackage + "." + file.getName().replace(".class", ""));
				classNames.add(className);
			}
		}
	}

	/**
	 * 初始化 bean
	 */
	private void doInstance() {
		//初始化,为DI做准备
		if (classNames.isEmpty()) {
			return;
		}
		try {
			for (String className : classNames) {
				Class<?> clazz = Class.forName(className);
				//什么样的类才需要初始化呢?
				//加了注解的类,才初始化,怎么判断?
				//为了简化代码逻辑,主要体会设计思想,只举例 @Controller和@Service,
				// @Componment...就一一举例了
				if (clazz.isAnnotationPresent(MmtController.class)) {
					Object instance = clazz.newInstance();
					//Spring默认类名首字母小写
					String beanName = toLowerFirstCase(clazz.getSimpleName());
					ioc.put(beanName, instance);
				} else if (clazz.isAnnotationPresent(MmtService.class)) {
					//1、自定义的beanName
					MmtService service = clazz.getAnnotation(MmtService.class);
					String beanName = service.value();
					//2、默认类名首字母小写
					if ("".equals(beanName.trim())) {
						beanName = toLowerFirstCase(clazz.getSimpleName());
					}
					Object instance = clazz.newInstance();
					ioc.put(beanName, instance);
					//3、根据类型自动赋值,投机取巧的方式
					for (Class<?> i : clazz.getInterfaces()) {
						if (ioc.containsKey(i.getName())) {
							throw new Exception("The “" + i.getName() + "” is exists!!");
						}
						//把接口的类型直接当成key了
						ioc.put(i.getName(), instance);
					}
				} else {
					continue;
				}

			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 自动依赖注入
	 */
	private void doAutowired() {
		if (ioc.isEmpty()) {
			return;
		}

		for (Map.Entry<String, Object> entry : ioc.entrySet()) {
			//Declared 所有的,特定的 字段,包括private/protected/default
			//正常来说,普通的OOP编程只能拿到public的属性
			Field[] fields = entry.getValue().getClass().getDeclaredFields();
			for (Field field : fields) {
				if (!field.isAnnotationPresent(MmtAutowired.class)) {
					continue;
				}
				MmtAutowired autowired = field.getAnnotation(MmtAutowired.class);
				//如果用户没有自定义beanName,默认就根据类型注入
				//这个地方省去了对类名首字母小写的情况的判断
				String beanName = autowired.value().trim();
				if ("".equals(beanName)) {
					//获得接口的类型,作为key待会拿这个key到ioc容器中去取值
					beanName = field.getType().getName();
				}
				//如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
				//反射中叫做暴力访问, 强吻
				field.setAccessible(true);
				try {
					//用反射机制,动态给字段赋值
					field.set(entry.getValue(), ioc.get(beanName));
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * 初始化url和Method的一对一对应关系
	 */
	private void initHandlerMapping() {
		if (ioc.isEmpty()) {
			return;
		}
		for (Map.Entry<String, Object> entry : ioc.entrySet()) {
			Class<?> clazz = entry.getValue().getClass();

			if (!clazz.isAnnotationPresent(MmtController.class)) {
				continue;
			}
			//保存写在类上面的@MmtRequestMapping("/demo")
			String baseUrl = "";
			if (clazz.isAnnotationPresent(MmtRequestMapping.class)) {
				MmtRequestMapping requestMapping = clazz.getAnnotation(MmtRequestMapping.class);
				baseUrl = requestMapping.value();
			}

			//默认获取所有的public方法
			for (Method method : clazz.getMethods()) {
				if (!method.isAnnotationPresent(MmtRequestMapping.class)) {
					continue;
				}
				MmtRequestMapping requestMapping = method.getAnnotation(MmtRequestMapping.class);
				//优化,防止 requestmapping中的请求路径没有写/ 或者都写了 / ,在这里,不管写没写,通统一处理一下
				String regex = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
				Pattern pattern = Pattern.compile(regex);
				this.handlerMapping.add(new Handler(pattern, entry.getValue(), method));
				System.out.println("Mapped :" + pattern + "," + method);
			}
		}
	}

	/**
	 * 如果类名本身是小写字母,确实会出问题
	 * 但是我要说明的是:这个方法是我自己用,private的
	 * 传值也是自己传,类也都遵循了驼峰命名法
	 * 默认传入的值,存在首字母小写的情况,也不可能出现非字母的情况
	 * 为了简化程序逻辑,就不做其他判断了,大家了解就OK
	 * 其实用写注释的时间都能够把逻辑写完了
	 *
	 * @param simpleName
	 * @return
	 */
	private String toLowerFirstCase(String simpleName) {
		char[] chars = simpleName.toCharArray();
		//之所以加,是因为大小写字母的ASCII码相差32,
		// 而且大写字母的ASCII码要小于小写字母的ASCII码
		//在Java中,对char做算学运算,实际上就是对ASCII码做算学运算
		chars[0] += 32;
		return String.valueOf(chars);
	}


	/**
	 * 保存一个url和一个Method的关系
	 */
	public class Handler {
		//必须把url放到HandlerMapping才好理解吧
		private Pattern pattern;
		private Method method;
		private Object controller;
		private Class<?>[] paramTypes;

		public Pattern getPattern() {
			return pattern;
		}

		public Method getMethod() {
			return method;
		}

		public Object getController() {
			return controller;
		}

		public Class<?>[] getParamTypes() {
			return paramTypes;
		}

		//形参列表
		//参数的名字作为key,参数的顺序,位置作为值
		private Map<String, Integer> paramIndexMapping;

		public Handler(Pattern pattern, Object controller, Method method) {
			this.pattern = pattern;
			this.method = method;
			this.controller = controller;

			paramTypes = method.getParameterTypes();

			paramIndexMapping = new HashMap<String, Integer>();
			putParamIndexMapping(method);
		}

		private void putParamIndexMapping(Method method) {

			//提取方法中加了注解的参数
			//把方法上的注解拿到,得到的是一个二维数组
			//因为一个参数可以有多个注解,而一个方法又有多个参数
			Annotation[][] pa = method.getParameterAnnotations();
			for (int i = 0; i < pa.length; i++) {
				for (Annotation a : pa[i]) {
					if (a instanceof MmtRequestParam) {
						String paramName = ((MmtRequestParam) a).value();
						if (!"".equals(paramName.trim())) {
							paramIndexMapping.put(paramName, i);
						}
					}
				}
			}
			//提取方法中的request和response参数
			Class<?>[] paramsTypes = method.getParameterTypes();
			for (int i = 0; i < paramsTypes.length; i++) {
				Class<?> type = paramsTypes[i];
				if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
					paramIndexMapping.put(type.getName(), i);
				}
			}
		}
	}
}

3. 运行阶段

运行阶段的代码,已经放在了上面代码中,也就是第六步,doPost 方法中,利用委派墨水,通过 doDispatch方法,从 request 中获取对应的请求及参数,拿到请求后,通过我们自定义的HandlerMapping 中进行匹配,如果匹配成功,说明请求正确,存在我们的系统中,通过 method.invoker反射执行方法,从而返回响应。

以上,mini 版的 springMvc 框架就写完了,虽然功能不是很完善,但是基本的脉络思想已经描述清楚。

4. 运行效果图

 真正的 Spring 要复杂很多,本文通过手写简易版的 SpringMVC,了解其基本的设计思想以及设计模式的简单使用,在 Spring 的源码中,使用了大量的设计模式,所以他的代码才会如此的优雅。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值