本文主要是介绍[Spring] 30个类手写 Spring Mini 版本系列(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
[Spring] 30个类手写 Spring Mini 版本系列(二)
简介
为了更深入的了解 Spring 的实现原理和设计思想,一直打算出个系列文章,从零开始重新学习 Spring。在[Spring] 30个类手写 Spring Mini 版本系列(一)中,我们直接通过 Servlet API 初步实现了 Spring 的简易版。针对V1.1.0版,今天我们来做下优化,主要针对主要流程节点进行初步的方法封装,便于后续模块设计。
目录
- [Spring] 30个类手写 Spring Mini 版本系列(二)
- V1 版本
- V1.2.0 版本
- 代码重构
- 效果演示
- 小结
- 更多
手机用户请
横屏
获取最佳阅读体验,REFERENCES
中是本文参考的链接,如需要链接和更多资源,可以加入『知识星球』获取长期知识分享服务。
正文
基本思路
- 配置阶段
- 配置
web.xml
- DispatchSevlet
- 设定
init-param
- contextConfigLocation = classpath:application.xml
- 设定
url-pattern
- /*
- 定义
Annotation
- @Controller
- @Service
- @Autowried
- @RequestMapping
- 配置
- 初始化阶段
- 调用
init()
方法- 加载配置文件
IOC
容器初始化- Map<String,Object>
- 扫描相关的类
- Scan-package=“com.yido”
- 创建实例化并保存至容器
- 通过反射机制将类实例化放入 IOC 容器
- 进行 DI
- 扫描 IOC 容器中的实例,给没有赋值的属性自动赋值
- 初始化 HandlerMapping
- 将 URL 和 Method 建立一对一的映射关系
- 调用
- 运行阶段
- 调用 doPost() / doGet()
- Web 容器调用 doPost() / doGet() ,获得 request / response 对象
- 匹配 HandlerMapping
- 从 request 对象中获取用户输入的 url , 找到对应的 Method
- 反射调用 method.invoke()
- 利用反射调用方法并返回结果
- 返回结果
- 利用 response.getWriter().write(), 将返回结果输出到浏览器
- 调用 doPost() / doGet()
V1 版本
准备工作
此处不再赘述,基于[Spring] 30个类手写 Spring Mini 版本系列(一)做优化。
V1.2.0 版本
注解和 Controller 复用,主要针对 XDispatchServlet 进行重构,主要是方法流程的编排,便于后续实现 Spring 中的设计
-
方法功能分割
-
设计流程分割
代码重构
/** @ProjectName: 编程学习* @Copyright: 2019 HangZhou Ashe Dev, Ltd. All Right Reserved.* @address: https://yiyuery.github.io/NoteBooks/* @date: 2020/4/12 5:30 下午* @description: 本内容仅限于编程技术学习使用,转发请注明出处.*/
package com.yido.mvcframework.v2.servlet;import com.yido.mvcframework.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.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;/*** <p>* 手写一个 请求转发器 DispatchServlet* - 代码重构,流程清晰化* </p>** @author Helios* @date 2020/4/12 5:30 下午*/
public class XDispatchServlet extends HttpServlet {/*** 配置*/private Properties contextConfig = new Properties();//享元模式,缓存private List<String> classNames = new ArrayList<String>();//IoC容器,key默认是类名首字母小写,value就是对应的实例对象private Map<String, Object> ioc = new HashMap<String, Object>();//URL 和 Method 映射关系private Map<String, Method> handlerMapping = new HashMap<String, Method>();/*** Get 请求处理转发** @param req* @param resp* @throws ServletException* @throws IOException*/@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);}/*** Post请求处理转发** @param req* @param resp* @throws ServletException* @throws IOException*/@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {try {//【运行阶段】//6. 委派:根据 URL 找到一个对应的 Method ,并通过 response 返回doDispatch(req, resp);} catch (Exception e) {e.printStackTrace();//出现异常,返回堆栈信息resp.getWriter().write("500 Exception, Detail: " + Arrays.toString(e.getStackTrace()));}}/*** 根据 URL 找到一个对应的 Method ,并通过 response 返回** @param req* @param resp*/private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException, InvocationTargetException, IllegalAccessException {//1. 获取参数和请求路径String url = req.getRequestURI();String contextPath = req.getContextPath();//替换请求上下文url = url.replace(contextPath, "")//替换多余 '/'.replaceAll("/+", "/");// 2. 获取请求处理器if (!this.handlerMapping.containsKey(url)) {resp.getWriter().write("404 Not Found!");return;}Map<String, String[]> params = req.getParameterMap();Method method = this.handlerMapping.get(url);// 3. 解析参数//3.1 获取形参列表Class<?>[] parameterTypes = method.getParameterTypes();Object[] paramValues = new Object[parameterTypes.length];boolean directReturn = true;for (int i = 0; i < parameterTypes.length; i++) {Class<?> parameterType = parameterTypes[i];if (parameterType == HttpServletRequest.class) {paramValues[i] = req;} else if (parameterType == HttpServletResponse.class) {paramValues[i] = resp;directReturn = false;} else if (parameterType == String.class) {//通过运行时状态去获取Annotation[][] parameterAnnotations = method.getParameterAnnotations();for (int j = 0; j < parameterAnnotations.length; j++) {for (Annotation annotation : parameterAnnotations[j]) {if (annotation instanceof XRequestParam) {String paramName = ((XRequestParam) annotation).value();if (!"".equals(paramName.trim())) {paramValues[i] = Arrays.toString(params.get(paramName)).replaceAll("\\[|\\]", "").replaceAll("\\s+", ",");}}}}}}//暂时硬编码String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());//赋值实参列表Object result = method.invoke(ioc.get(beanName), paramValues);//方法入参没有 resp 时 & 方法返回值部不为 void 直接返回if (!"void".equals(method.getReturnType().getName()) && directReturn) {resp.getWriter().write(Arrays.toString(new Object[]{result}).replaceAll("\\[|\\]", ""));}}/*** 加载配置并缓存 Controller 实例和 初始化请求对应的 Method映射** @param config* @throws ServletException*/@Overridepublic void init(ServletConfig config) throws ServletException {//【初始化阶段】//====== 初始化加载 =====//1. 读取配置文件doLoadConfig(config.getInitParameter("contextConfigLocation"));//2. 扫描doScanner(contextConfig.getProperty("scanPackage"));//====== IOC 容器 =====//3. 初始化 IOC 容器,将扫描到的相关类实例化,保存到 IOC 容器doInstance();//AOP 生成新的代理对象//====== DI =====//4. 完成依赖注入doAutoWired();//====== MVC =====//5. 初始化 HandlerMappingdoInitHandlerMapping();System.out.println("XSpring MVC Framework has been initialed");}/*** 读取配置文件** @param contextConfigLocation*/private void doLoadConfig(String contextConfigLocation) {try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation)) {contextConfig.load(is);} catch (IOException e) {e.printStackTrace();}}private void doScanner(String scanPackage) {URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));File rootDir = new File(url.getFile());for (File file : rootDir.listFiles()) {if (file.isDirectory()) {doScanner(scanPackage + "." + file.getName());} else {if (!file.getName().endsWith(".class")) {continue;}String clazzName = scanPackage + "." + file.getName().replace(".class", "");classNames.add(clazzName);}}}/*** 完成依赖注入*/private void doAutoWired() {if (ioc.isEmpty()) {return;}Collection<Object> values = ioc.values();for (Object value : values) {if (null == value) {continue;}Class clazz = value.getClass();if (clazz.isAnnotationPresent(XController.class)) {Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {if (!field.isAnnotationPresent(XAutowired.class)) {continue;}XAutowired autowired = field.getAnnotation(XAutowired.class);String beanName = autowired.value();if ("".equals(beanName)) {beanName = toLowerFirstCase(field.getType().getSimpleName());}//注入依赖实例field.setAccessible(true);try {field.set(value, ioc.get(beanName));} catch (IllegalAccessException e) {e.printStackTrace();}}}}//Spring 实例平铺后只需要再次扫描进行一次 DI 操作即可解决依赖注入的问题,此处演示未涉及,暂不做此处理}/*** 初始化 IOC 容器,将扫描到的相关类实例化,保存到 IOC 容器*/private void doInstance() {if (classNames.isEmpty()) {return;}try {for (String clazzName : classNames) {if (!clazzName.contains(".")) {continue;}Class<?> clazz = Class.forName(clazzName);// 1. 处理 XControllerif (clazz.isAnnotationPresent(XController.class)) {ioc.put(toLowerFirstCase(clazz.getSimpleName()), clazz.newInstance());// 2. 处理 XService} else if (clazz.isAnnotationPresent(XService.class)) {//2.1 在多个包下出现相同的类名,只能(自己)起一个全局唯一的名字//自定义命名XService service = clazz.getAnnotation(XService.class);String beanName = service.value();//2.2 默认的类名首字母小写if ("".equals(beanName)) {beanName = toLowerFirstCase(clazz.getSimpleName());}//2.3 如果是接口//判断有多少个实现类,如果有多个重名实例,只能抛异常Object instance = clazz.newInstance();ioc.put(beanName, instance);for (Class<?> i : clazz.getInterfaces()) {if (ioc.containsKey(toLowerFirstCase(i.getSimpleName()))) {//不允许一个接口映射多个实例,默认取第一个throw new Exception("The " + i.getName() + " is exists!!");}ioc.put(toLowerFirstCase(i.getSimpleName()), instance);}}}} catch (Exception e) {e.printStackTrace();}}/*** 首字母小写** @param name* @return*/private String toLowerFirstCase(String name) {char[] chars = name.toCharArray();chars[0] += 32;return String.valueOf(chars);}/*** 初始化 HandlerMapping*/private void doInitHandlerMapping() {if (classNames.isEmpty()) {return;}try {for (Map.Entry<String, Object> entry : ioc.entrySet()) {Class<?> clazz = entry.getValue().getClass();if (!clazz.isAnnotationPresent(XController.class)) {continue;}String baseUrl = "";// 1. 处理 XController 中方法映射if (clazz.isAnnotationPresent(XController.class)) {// 1.1 解析请求路径前缀if (clazz.isAnnotationPresent(XRequestMapping.class)) {XRequestMapping requestMapping = clazz.getAnnotation(XRequestMapping.class);baseUrl = requestMapping.value();}// 1.2 解析public方法Method[] methods = clazz.getMethods();for (Method method : methods) {if (method.isAnnotationPresent(XRequestMapping.class)) {XRequestMapping annotation = method.getAnnotation(XRequestMapping.class);//替换多/为单/String url = ("/" + baseUrl + "/" + annotation.value()).replaceAll("/+", "/");handlerMapping.put(url, method);System.out.println("> Mapped--------->url: " + url + "," + method.getName());}}}}} catch (Exception e) {e.printStackTrace();}}}
效果演示
/*** support:* spring-v1* spring-v2** @param name* @return*/@XRequestMapping("/v2/welcome")public String welcome2(@XRequestParam(value = "name") String name) {return helloService.welcome(name);}/*** support:* spring-v1* spring-v2** @param name* @return*/@XRequestMapping("/v3/welcome")public String welcome3(@XRequestParam(value = "name") String name, @XRequestParam("nick") String nick) {return helloService.welcome(nick + " " + name);}
为了便于区分,对接口添加版本标识
小结
- 增加 request \ response 和其他参数的自动注入(目前仅支持 String)
- 支持方法返回结果的直接渲染
- 流程更加清晰
- 初始化加载配置
- 实例化并放入容器,控制反转:IOC
- 依赖注入:DI
- 前端请求处理方式映射表 HandlerMapping
- 运行状态下请求处理:doDispatch
- 但是 HandleMapping 管理过程中暴露了太多细节,还有些小问题需要优化
- 没有明确划分 AOP、IOC、MVC 模块
To do Continue...
更多
扫码关注
架构探险之道
,回复『源码』,获取本文相关源码和资源链接
知识星球(扫码加入,获取珍贵笔记、视频、电子书的等资源)
这篇关于[Spring] 30个类手写 Spring Mini 版本系列(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!