【Spring】重构--仿写Spring核心逻辑(四)实现MVC(webmvc包)

系列文章:

在前面两篇,我们已经成功实现了 Spring 最核心的功能 IOC和DI,从这篇开始,我们来看看如何实现 MVC:

在这里插入图片描述
MVC 的核心就是那九大组件 【Spring】MVC:九大核心组件分析,而其中最重要的三个:HandlerMappings,HandlerAdapters,ViewResolvers。

1.MYHandlerMapping

封装 [Controller对象,处理请求的方法,可以处理的请求路径] 的一对一关系。实质上是把把 IOC 容器管理的Bean实例进行了封装(包括代理对象的替换),并建立了映射关系。

public class MYHandlerMapping {

    // 处理请求的具体Controller对象
    private Object Controller;
    // 处理请求的具体方法
    private Method method;
    // 可以处理的请求路径
    private Pattern pattern;
	
	// 构造函数
    public MYHandlerMapping(Object controller, Method method, Pattern pattern) {
        Controller = controller;
        this.method = method;
        this.pattern = pattern;
    }
	
	// getter、setter...
    public Object getController() {
        return Controller;
    }

    public void setController(Object controller) {
        Controller = controller;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public Pattern getPattern() {
        return pattern;
    }

    public void setPattern(Pattern pattern) {
        this.pattern = pattern;
    }
}

2.MYHandlerAdpter

将Request变成Handler可以处理的参数,并与其形参匹配后执行

每个 HandlerMapping 都对应一个 HandlerAdpter。因为要拿到HandlerMapping才能干活,所以有几个 HandlerMapping 就有几个 HandlerAdapter。

public class MYHandlerAdpter {
	//......
}

supports()

判断当前handler能否被当前adpter进行适配,即将传入参数转换成该handler的参数并处理。因为可能要被适配handler还有文件上传等,所以这个判断还是有必要的

public boolean supports(Object handler) {
    return (handler instanceof MYHandlerMapping);
}

handle()

处理请求,核心是将 request 的请求参数与处理方法的入参一一对应,然后通过 IOC 容器中的bean实例去执行方法,最后将执行结果返回

public MYModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception{

    MYHandlerMapping handlerMapping = (MYHandlerMapping) handler;

    // 处理请求的方法的(参数名,参数位置)
    Map<String, Integer> paramIdxMapping = new HashMap<String, Integer>();

    // 拿到有注解的参数,获取其参数名和参数位置
    // 注:二维数组:[i][j];i-在参数中第几个位置,j-第几个注解(因为一个参数可能有多个注解
    Annotation[][] pa = handlerMapping.getMethod().getParameterAnnotations();
    for (int i = 0; i < pa.length; i++) {
        // 第 i 个位置参数的注解们
        for (Annotation a : pa[i]) {
            if (a instanceof MYRequestParam) {
                String paramName = ((MYRequestParam) a).value();
                if (!"".equals(paramName.trim())) {
                    // @RequestParam(value = paramName)
                    paramIdxMapping.put(paramName, i);
                }
            }
        }
    }

    // 获取方法中request,response参数位置
    Class<?>[] parameterTypes = handlerMapping.getMethod().getParameterTypes();
    // 从第一个参数开始遍历
    for (int i = 0; i < parameterTypes.length; i++) {
        Class<?> type = parameterTypes[i];
        if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
            // 注:这里放入的是类型,因为HTTPServletRequest与response唯一
            paramIdxMapping.put(type.getName(), i);
        }
    }

    // 最后传给method的参数列表,其中参数必须是与方法形参列表对应
    Object[] paramValues = new Object[parameterTypes.length];
    // 获取request的参数列表
    Map<String, String[]> params = req.getParameterMap();
    
    // 遍历Request的参数列表,填充方法的参数 paramValues
    for (Map.Entry<String, String[]> parm : params.entrySet()) {
        // 根据参数名,判断当前参数是否是method所需
        if (!paramIdxMapping.containsKey(parm.getKey())) {
            continue;
        }

        // 拿到当前参数value
        // 通过正则将 []删除,空白字符换成 ,
        String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]","").replaceAll("\\s",",");
        // 拿到当前参数在方法形参中的位置
        Integer idx = paramIdxMapping.get(parm.getKey());
        // 放入实参数组
        // 注:Request中带的参数都是String类型,这里需要将它们转为method需要的正确类型
        //     paramTypes[idx] = idx位置的类型
        paramValues[idx] = caseStringValue(value,parameterTypes[idx]);
    }
    
    // 判断当前方法是否需要Request,Response作为参数
    if (paramIdxMapping.containsKey(HttpServletRequest.class.getName())) {
        Integer reqIdx = paramIdxMapping.get(HttpServletRequest.class.getName());
        // 拿到Request位置
        paramValues[reqIdx] = req;
    }
    if (paramIdxMapping.containsKey(HttpServletResponse.class.getName())) {
        Integer respIdx = paramIdxMapping.get(HttpServletResponse.class.getName());
        paramValues[respIdx] = resp;
    }

    // 执行方法,获取返回结果
    Object result = handlerMapping.getMethod().invoke(handlerMapping.getController(), paramValues);
    
    // 如果该方法返回null(出错。。),或没有返回值(增加,删除。。。)。
    // 那么,执行完该方法就完事了,没有后续步骤了
    if (result == null || result instanceof Void) {
        return null;
    }
    // 如果该方法返回ModelAndView
    // 那么,还需要再多走一步,即对 ModelAndView进行解析
    boolean isModelAndView = handlerMapping.getMethod().getReturnType() == MYModelAndView.class;
    if (isModelAndView) {
    	// 注意,这里要将返回值强转为ModelAndView
        return (MYModelAndView) result;
    }
	
    return null;
}

caseStringValue()

将String类型的value,转换为指定类型

private Object caseStringValue(String value, Class<?> parameterType) {
    if (String.class == parameterType) {
        return value;
    }

    if (Integer.class == parameterType) {
        return Integer.valueOf(value);
    } else if (Double.class == parameterType) {
        return Double.valueOf(value);
    } else {
        if (value != null) {
            return value;
        }
        return null;
    }

    //...还有Long等
    // 可以考虑策略模式
}

3.MYModelAndView

Controller 层返回的对象,里面包含了要返回的页面,以及页面里所需要的参数。需要Resolver(模板引擎)去解析成View,View再通过模板引擎即系model,然后返回页面。

ModelAndView(ViewResolver) —> View(模板引擎解析model)—> HTML

public class MYModelAndView {

    private String ViewName;

    private Map<String, ?> model;

    public MYModelAndView(String viewName, Map<String, ?> model) {
        ViewName = viewName;
        this.model = model;
    }

    public MYModelAndView(String viewName) {
        ViewName = viewName;
    }

    public String getViewName() {
        return ViewName;
    }

    public Map<String, ?> getModel() {
        return model;
    }
}

4.MYViewResolver

解析ModelAndeView的View路径,返回视图对象View。

注意:ViewResolver有多种,可将ModelAndView解析成多种View(如html,json,outputStream等)。

public class MYViewResolver {

    private final String DEFALUT_TEMPALTE_SUFIX = ".html";

    // 视图目录
    private File templateRootDir;
    
    public MYViewResolver(String templateRoot) {
        String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
        this.templateRootDir = new File(templateRootPath);
    }

    // 通过页面Name,返回相应View视图
    public MYView resolveViewName(String viewName, Locale locale) throws Exception {
        if (null == viewName || "".equals(viewName.trim())) {
            return null;
        }
        // 给没有 .html的加上后缀(我们可以在ModelAndView中写500.html,也可以直接写 500)
        viewName = viewName.endsWith(DEFALUT_TEMPALTE_SUFIX) ? viewName : (viewName + DEFALUT_TEMPALTE_SUFIX);
        // 返回相应视图
        File templateFile = new File((templateRootDir.getPath() + "/" + viewName).replaceAll("/+", "/"));
        return new MYView(templateFile);
    }
}

5.MYView

视图对象,负责解析model,并返回页面。

注意:可以有多种返回结果(如 html,json),但这里就定义了一种基本的html返回

public class MYView {

    public final String DEFULAT_CONTENT_TYPE = "text/html;charset=utf-8";
    // 视图代表的页面(文件)
    private File viewFile;

    public MYView(File viewFile) {
        this.viewFile = viewFile;
    }
    
	//......
}

render()

解析model中数据(相当于自定义模板引擎) ,最后将解析的结果通过response写出

模板引擎是解析html/jsp等页面中的 {{}} 等数据标签的工具

public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception{
    
    StringBuilder sb = new StringBuilder();
	
	// 通过 RandomAccessFile 将要返回的页面读进内存
    // 注:RandomAccessFile(提供文件读写功能) > FileInputStream + FileOutputStream。所以要指定 r或w 模式,另外 RandomAccessFile 还提供随机读写(seek等)。
    RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r");

    // 逐行读取html文件,并进行数据解析
    String line = null;
    while(null != (line = ra.readLine())) {
        // 为了字符集匹配,这里通过字节读取line,然后再new String
        line = new String(line.getBytes("ISO-8859-1"), "utf-8");

        // 通过正则表达式判断有 ¥{  } 的位置,即需要放入数据的位置
        Pattern pattern = Pattern.compile("¥\\{[^\\}]+\\}",Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(line); 
        // 不断寻找有 ¥{  } 的位置
        while (matcher.find()){
            String paramName = matcher.group();
            // 获取模板中的参数名
            paramName = paramName.replaceAll("¥\\{|\\}","");
            // 在model中通过参数名获取相应参数
            Object paramValue = model.get(paramName);
            // 为 null 的话,就不管,最后输出的结果还是 ¥{ }
            if(null == paramValue){ continue;}
            // 不为null,就将 ¥{ } 替换为相应参数
            // 注意:这里对特殊字符要处理,比如异常
            line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
            // 更新 matcher,开始下一轮寻找
            matcher = pattern.matcher(line);
        }
        // 将当前line添加到要输出的html中
        sb.append(line);
    }
	
	// 将页面返回
    response.setCharacterEncoding("utf-8");
    response.getWriter().write(sb + "");
}

makeStringForRegExp()

处理特殊字符

public static String makeStringForRegExp(String str) {
    return str.replace("\\", "\\\\").replace("*", "\\*")
            .replace("+", "\\+").replace("|", "\\|")
            .replace("{", "\\{").replace("}", "\\}")
            .replace("(", "\\(").replace(")", "\\)")
            .replace("^", "\\^").replace("$", "\\$")
            .replace("[", "\\[").replace("]", "\\]")
            .replace("?", "\\?").replace(",", "\\,")
            .replace(".", "\\.").replace("&", "\\&");
}

6.MYDispatchServlet

DispatchServlet 入口类,负责初始化九大组件,然后分发请求

@Slf4j
public class MYDispatchServlet extends HttpServlet {
	
    private final String CONTEXT_CONFIG_LOCATION = "contextConfigLocation";
	
	// IOC 容器
    private MYApplicationContext context;
	
	// 保存 HandlerMapping 的容器(用于判断能否处理外部请求)
    private List<MYHandlerMapping> handlerMappings = new ArrayList<MYHandlerMapping>();
	// 保存 <HandlerMapping,HandlerAdpter> 映射关系的容器(用于获取执行方法的请求适配器)
    private Map<MYHandlerMapping, MYHandlerAdpter> handereAdpters = new HashMap<MYHandlerMapping, MYHandlerAdpter>();
	// 保存视图解析器的容器
    private List<MYViewResolver> viewResolvers = new ArrayList<MYViewResolver>();

	@Override
    public void init(ServletConfig config) throws ServletException {

        // 1.初始化ApplicationContext !!!
        // tomcat会加载web.xml并创建其中配置的servlet(即DispatchServlet),同时会执行init方法
        // 这里的config即web.xml配置信息,其中 contextConfigLocation 参数配置的是 application.properties 路径
        context = new MYApplicationContext(config.getInitParameter(CONTEXT_CONFIG_LOCATION));

        // 2.初始化SpringMVC九大组件
        initStrategies(context);
    }

    @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) {
            // 顶层异常处理,如果处理请求的方法抛出异常,在这里捕获后返回提前写好的500页面
            resp.getWriter().println("500 Exception,Details:\r\n" + Arrays.toString(e.getStackTrace()).replaceAll("\\[|\\]", "").replaceAll(",\\s", "\r\n"));
            e.printStackTrace();
        }
    }

	//......
}

web.xml 相关配置如下

在这里插入图片描述

initStrategies()

protected void initStrategies(MYApplicationContext context) {
    // 多文件上传的组件
    initMultipartResolver(context);
    // 初始化本地语言环境
    initLocaleResolver(context);
    // 初始化模板处理器
    initThemeResolver(context);


    // handlerMapping,必须实现
    initHandlerMappings(context);
    // 初始化参数适配器,必须实现
    initHandlerAdapters(context);
    // 初始化异常拦截器
    initHandlerExceptionResolvers(context);
    // 初始化视图预处理器
    initRequestToViewNameTranslator(context);


    // 初始化视图转换器,必须实现
    initViewResolvers(context);
    // 参数缓存器
    initFlashMapManager(context);
}

这些非必须实现的组件,我们将初始化方法直接空实现就好

private void initMultipartResolver(MYApplicationContext context) {
}
private void initLocaleResolver(MYApplicationContext context) {
}
private void initThemeResolver(MYApplicationContext context) {
}
private void initHandlerExceptionResolvers(MYApplicationContext context) {
}
private void initRequestToViewNameTranslator(MYApplicationContext context) {
}
private void initFlashMapManager(MYApplicationContext context) {
}

initHandlerMappings()

初始化 HandlerMapping

private void initHandlerMappings(MYApplicationContext context) {

    // 通过 ApplicationContext#getBeanDefinitionNames 拿到所有的 beanName
    String[] beanNames = context.getBeanDefinitionNames();

    try {
    	// 根据 beanName 遍历所有 bean,去寻找所有 Controller 对象
        for (String beanName : beanNames) {
        
        	// 获取到具体的bean(由于是单例的,在factoryBeanObjectCache容器中就能获取到)
            Object controller = context.getBean(beanName);
			// 获取到bean的Class,然后判断是否有 @MYController 注解
            Class<?> clazz = controller.getClass();
            // 如果不是 Controller 就返回进行下一轮循环
            if (!clazz.isAnnotationPresent(MYController.class)) {
                continue;
            }

            // 获取当前 Controller 的公有url,即类上 @RequestMapping 的路径
            String baseUrl = "";
            if (clazz.isAnnotationPresent(MYRequestMapping.class)) {
                MYRequestMapping annotation = clazz.getAnnotation(MYRequestMapping.class);
                baseUrl = annotation.value();
            }

            // 获取所有方法的处理路径
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                if (!method.isAnnotationPresent(MYRequestMapping.class)) {
                    continue;
                }

                MYRequestMapping annotation = method.getAnnotation(MYRequestMapping.class);
                String regex = ("/" + baseUrl + "/" + annotation.value().replaceAll("\\*", ".*")).replaceAll("/+", "/");
                Pattern pattern = Pattern.compile(regex);
                // 构建处理器,并加入handlerMapping
                this.handlerMappings.add(new MYHandlerMapping(controller, method, pattern));
                log.info("Mapped " + regex + "," + method);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

initHandlerAdapters()

初始化参数适配器

private void initHandlerAdapters(MYApplicationContext context) {
	// 为每一个 HandlerMapping 都创建一个 HandlerAdpter
    for (MYHandlerMapping handlerMapping : this.handlerMappings) {
        this.handereAdpters.put(handlerMapping, new MYHandlerAdpter());
    }
}

initViewResolvers()

初始化视图转换器

private void initViewResolvers(MYApplicationContext context) {
    // 拿到在配置文件中配置的模板存放路径(layouts)
    String templateRoot = context.getConfig().getProperty("templateRoot");
    
    // 通过相对路径找到目标后,获取到绝对路径
    // 注:getResourse返回的是URL对象,getFile返回文件的绝对路径
    String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
	
	// 拿到模板目录下的所有文件名(这里是所有html名)
    File templateRootDir = new File(templateRootPath);
    String[] templates = templateRootDir.list();
    
    // 视图解析器可以有多种,且不同的模板需要不同的Resolver去解析成不同的View(jsp,html,json。。)
    // 但这里其实就只有一种(解析成html)
    // 为了仿真才写了这个循环,其实只循环一次
    for (int i = 0; i < templates.length; i ++) {
        this.viewResolvers.add(new MYViewResolver(templateRoot));
    }
}

doDispatch()

请求分发 --> 通过反射执行相应方法 --> 对结果进行解析与渲染。 到这里init方法已经执行过,即已经创建好了,所以这里处理请求其实只需要拿出相应组件即可。

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception{
    // 1.通过从Request中拿到URL,去匹配一个HandlerMapping
    MYHandlerMapping handler = getHandler(req);
    // 如果当前没有处理当前请求的方法,返回404页面
    if (handler == null) {
        processDispatchResult(req, resp, new MYModelAndView("404"));
        return;
    }

    // 2.获取当前handler对应的处理参数的Adpter
    MYHandlerAdpter handlerAdpter = getHandlerAdptor(handler);

    // 3.Adpter负责处理 request 中携带的参数然后执行处理请求的方法
    // 执行的结果可能是null(增加、删除、异常...)也可能是ModelAndView(查询...)
    // Adpter真正调用处理请求的方法,返回ModelAndView(存储了页面上值,和页面模板的名称)
    MYModelAndView mv = handlerAdpter.handle(req, resp, handler);

    // 4.真正输出,将方法执行进行处理然后返回
    // 如果上面返回的是 ModelAndView ,那么还要通过视图解析器和模板引擎进行解析
    processDispatchResult(req, resp, mv);
}

getHandler()

通过Request获取相应handler

private MYHandlerMapping getHandler(HttpServletRequest req) {
    if (this.handlerMappings.isEmpty()) return null;

    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replace(contextPath, "").replaceAll("/+", "/");

    for (MYHandlerMapping handler : this.handlerMappings) {
        Matcher matcher = handler.getPattern().matcher(url);
        // 如果没有匹配上就继续遍历handler
        if (!matcher.matches()) {
            continue;
        }
        return handler;
    }
    return null;
}

processDispatchResult()

将ModelAndView解析成 HTML、json、outputStream、freemark等 --> 然后解析数据 --> 最后输出给前端

private void processDispatchResult(HttpServletRequest req, HttpServletResponse resp, MYModelAndView mv) throws Exception {
    // null 表示方法返回类型是void,或返回值是null。不做额外处理
    if(mv == null) {
        return;
    }

    // 如果没有视图解析器就返回,因为无法处理ModelAndView
    if (this.viewResolvers.isEmpty()) {
        return;
    }

    // 遍历视图解析器
    for (MYViewResolver viewResolver : this.viewResolvers) {
        // 通过相应解析器,返回相应页面 View
        MYView view = viewResolver.resolveViewName(mv.getViewName(), null);
        // View通过模板引擎(自定义的)解析后输出
        view.render(mv.getModel(), req, resp);
        return;
    }
}

getHandlerAdptor()

获取 HandlerAdptor,然后就可执行处理请求的方法了

private MYHandlerAdpter getHandlerAdptor(MYHandlerMapping handler) {
    if (this.handereAdpters.isEmpty()) {
        return null;
    }
    MYHandlerAdpter handlerAdpter = this.handereAdpters.get(handler);
    // 判断当前handler能否被当前adptor进行适配
    if (handlerAdpter.supports(handler)) {
        return handlerAdpter;
    }
    return null;
}

到此 MVC 部分就写完了,我们测试一下

这里web容器我没有采用tomcat,而是用的jetty(已经提前配置好了,pom文件由于篇幅限制就不展示了,后面需要的同学可以在我的GitHub拉取源码)

首先,我们放问一个没有的页面
在这里插入图片描述
果然,跟 MYDIspatchServlet#doDispatch 中的分发逻辑一样,如果没有匹配到 handler 就返回 404 页面。下面我们再来访问一下
在这里插入图片描述
结果如下:
在这里插入图片描述
那如果有异常会是什么什么样子呢?

完整代码我放到 GitHub 上了,可以点击这里跳转…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A minor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值