前言
上一篇通过简单的设计完成了 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那样的完美,但也可以正常运行了。通过这些简单的代码,比较容易理解它的设计思想。