Spring深度学习 — 高仿 Spring(MVC)

前言

上一篇通过简单的设计完成了 IOC 和 DI 的初始化工作,这一篇继续通过代码来体会 MVC 的设计思想。

一、初始化 MVC 九大组件

        在mini 版的时候,我们知道 MVC 的九大组件是在 DispatcherServlet类中的 init()方法中进行初始化的,而且通过源码查看也看到了initStrategies方法,下面,我们来仿照着来写一下:

/**
	 * 通过 init方法来进行 ioc 容器的初始化动作 和 初始化 MVC 相关的 9 大组件
	 *
	 * @param config
	 * @throws ServletException
	 */
	@Override
	public void init(ServletConfig config) throws ServletException {
		// 初始化 IOC 相关
		applicationContext = new ApplicationContext(config.getInitParameter("contextConfigLocation"));
		// 初始化 MVC 部分
		//初始化九大组件
		initStrategies(applicationContext);
	}

	private void initStrategies(ApplicationContext context) {
		//多文件上传的组件
		initMultipartResolver(context);
		//初始化本地语言环境
		initLocaleResolver(context);
		//初始化模板处理器
		initThemeResolver(context);
		//初始化 handlerMapping
		initHandlerMappings(context);
		//初始化参数适配器
		initHandlerAdapters(context);
		//初始化异常拦截器
		initHandlerExceptionResolvers(context);
		//初始化视图预处理器
		initRequestToViewNameTranslator(context);
		//初始化视图转换器
		initViewResolvers(context);
		//FlashMap管理器
		initFlashMapManager(context);
	}

当然,九大组件,功能涉及到的太多,这里,我们只抽取核心部分进行高仿,也就是 initHandlerMappings、initHandlerAdapters、initViewResolvers三个部分来完成整个 MVC 流程的运转:

initHandlerMappings

首先,我们要定义一个类,来保存它们之间的映射关系,也就是 HandlerMapping

@Data
@AllArgsConstructor
public class HandlerMapping {

	/**
	 * 请求的 URL
	 */
	private Pattern pattern;
	/**
	 * 具体的类
	 */
	private Object controller;
	/**
	 * 具体的方法
	 */
	private Method method;
}

 接下来,通过 ApplicationContext 来完成 标注了@MmtController注解的bean,因为在初始化 IOC 的时候,已经将标有指定注解的类都交给IOC统一管理了:

private void initHandlerMappings(ApplicationContext context) {
		if (context.getBeanDefinitionCount() == 0) {
			return;
		}
		String[] beanDefinitionNames = context.getBeanDefinitionNames();
		for (String beanName : beanDefinitionNames) {
			Object instance = applicationContext.getBean(beanName);
			Class<?> clazz = instance.getClass();
			// 如果不是 controller 类,则不予处理
			if (!clazz.isAnnotationPresent(MmtController.class)) {
				continue;
			}
			//相当于提取 class上配置的url
			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;
				}
				//提取每个方法上面配置的url
				MmtRequestMapping requestMapping = method.getAnnotation(MmtRequestMapping.class);

				// 正则替换,统一处理 /,避免没有写/,或者多写/
				String regex = ("/" + baseUrl + "/" + requestMapping.value().replaceAll("\\*", ".*")).replaceAll("/+", "/");
				Pattern pattern = Pattern.compile(regex);
				//handlerMapping.put(url,method);
				handlerMappings.add(new HandlerMapping(pattern, instance, method));
				System.out.println("Mapped : " + regex + "," + method);
			}
		}
	}

至此,handlerMapping 初始化完毕。

initHandlerAdapters

        下面进行形参适配器的初始化工作,也就是方法里面带的参数,这里需要将方法和参数进行关系绑定,也就是我们需要一个 map 来进行保存,它的样子应该是这样的 Map<HandlerMapping,HandlerAdapter>,我们首先先来创建出保存形参的类 HandlerAdapter:

public class HandlerAdapter {
    //TODO 这个里面需要做什么呢?先不管,先把整体架构搭完,再回来填空处理
}

来进行初始化形参适配器相关工作,即有多少个请求,就有多少个HandlerMapping ,相同就会存在多少个形参适配器:

private void initHandlerAdapters(ApplicationContext context) {
		for (HandlerMapping handlerMapping : handlerMappings) {
			this.handlerAdapters.put(handlerMapping, new HandlerAdapter());
		}
	}

注意,这里初始化的步骤,一定是先初始化 handlerMapping,后初始化 handlerAdapter,这里用到的 handlerAdapters就是定义的Map<HandlerMapping,HandlerAdapter> 这个类型的 map。这里只是将每一个 handlerMapping 保存到了 handlerAdapters 中,并没有做什么,后面我们填空的时候会进行处理。

initViewResolvers

        初始化完形参适配器后,接下来初始化另外一个必须组件,视图解析器,这里写的比较简单粗暴,仅处理了一个一级目录,多级目录没有处理,后续优化:

private void initViewResolvers(ApplicationContext context) {
		String templateRoot = context.getConfig().getProperty("templateRoot");
		String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
		//TODO 待优化 这里写的比较简单粗暴,只是处理了单一路径,多级文件路径未处理
		File templateRootDir = new File(templateRootPath);
		for (File file : templateRootDir.listFiles()) {
			this.viewResolvers.add(new ViewResolver(templateRoot));
		}
	}

好了,到这里,初始化 3 个组件,已经够完成简单版的业务。下面来完善一下具体的业务拦截操作。

二、完善 DispatcherServlet业务处理功能

        我们都知道,当我们发送一个请求的时候,请求会到 dispatcherServlet 的 doPost方法中,那么这里,也就是具体实现 MVC 请求分发处理的地方:

@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// 委派,根据URL去找到一个对应的Method并通过response返回
		try {
			doDispatch(req, resp);
		} catch (Exception e) {
			e.printStackTrace();
			resp.getWriter().write("500 Exception,Detail : " + Arrays.toString(e.getStackTrace()));
		}
	}

 doDispatch

/**
* mvc 响应
* 通过初始化动作,完成了对 handlerMapping 的封装、完成了对返回值的封装 ModelAndView
* 这里,通过 request 请求的 URL 来获取对应的 handlerMapping 进行处理
*
* @param req
* @param resp
* @throws Exception
*/
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
	// 1、通过URL获得一个HandlerMapping
	HandlerMapping handlerMapping = getHandler(req);
	if (handlerMapping == null) {
		processDispatchResult(req, resp, new ModelAndView("404"));
		return;
	}
	// 2、根据一个HandlerMapping获得一个HandlerAdapter
	HandlerAdapter adapter = getHandlerAdapter(handlerMapping);
	// 3、解析某一个方法的形参和返回值之后,统一封装为ModelAndView对象
	ModelAndView mav = adapter.handler(req, resp, handlerMapping);
	// 4、把ModelAndView变成一个ViewResolver
	processDispatchResult(req, resp, mav);
}

/**
 * 通过URL获得一个HandlerMapping
 *
 * @param request
 * @author: <a href="568227120@qq.com">heliang.wang</a>
 * @date: 2022/5/24 9:57 上午
 * @return: com.mmt.imitate.spring.framework.webmvc.servlet.HandlerMapping
 */
private HandlerMapping getHandler(HttpServletRequest request) {
	if (this.handlerMappings.isEmpty()) {
		return null;
	}
	String url = request.getRequestURI();
	String contextPath = request.getContextPath();
	url = url.replaceAll(contextPath, "").replaceAll("/+", "/");
	for (HandlerMapping mapping : handlerMappings) {
		Matcher matcher = mapping.getPattern().matcher(url);
		if (!matcher.matches()) {
			continue;
		}
		return mapping;
	}
	return null;
}

 渲染视图的方法:

public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse resp) throws Exception {
	StringBuffer sb = new StringBuffer();
	RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r");
	// 根据自己定义的占位符匹配页面上的内容
	Pattern pattern = Pattern.compile("¥\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE);
	String line;
	while (null != (line = ra.readLine())) {
		line = new String(line.getBytes("ISO-8859-1"), "utf-8");
		Matcher matcher = pattern.matcher(line);
		// 如果可以匹配到正则占位符,则进行参数替换
		while (matcher.find()) {
			String paramName = matcher.group();
			// 在这里进行了页面表达式的替换,这个 '$' 可以换成任意你喜欢的,页面上的占位符需要同步更新
			paramName = paramName.replaceAll("¥\\{|\\}", "");
			Object paramValue = model.get(paramName);
			line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
			matcher = pattern.matcher(line);
		}
		sb.append(line);
	}
	resp.setCharacterEncoding("utf-8");
	resp.getWriter().write(sb.toString());
}

由于代码量比较大,这里就不一一贴图了,最新版代码已上传只码云,感兴趣的小伙伴自行去下载【下载地址在上一篇 IOC 中】。

三、总结

        整体的思路大概就是这样:

1、容器启动时,在 DispatcherServlet类中的 init()方法中进行 IOC 容器初始化、MVC九大组件初始化工作;

2、初始化HandlerMapping --> 将一个请求 URL 与一个 mapping 的关系进行保存;

3、初始化 HandlerAdapter --> 形参适配器,将一个请求中的参数(适配器)与 handlerMapping 进行关系绑定;

4、初始化 ViewResolver  --> 视图解析器,通过配置文件中的路径,读取对应下面的文件

5、初始化工作完毕后,进行业务请求的分发处理,请求到了 DispatcherServlet中的 doPost 中,通过 request 获取到一个对应的HandlerMapping;

6、检查是否能匹配到请求,匹配不到说明是无效请求,如果可以匹配到,通过 HandlerMapping 获取对应的形参列表 HandlerAdapter;

7、解析HandlerMapping 中的行参和返回结果并通过反射执行,统一封装成 ModelAndView对象;

8、将 ModelAndView 对象交给视图解析器 ViewResolver,获取到对应的返回页面对象 View;

9、解析 ModelAndView中的参数,通过文件流读取文件,根据页面中的占位符进行对应表达式参数替换;

10、通过 response 响应给页面。

至此,MVC 的大概流程完毕,虽然没有 SpringMVC那样的完美,但也可以正常运行了。通过这些简单的代码,比较容易理解它的设计思想。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值