手写自己的SpringMvc框架


SpringMVC是Spring框架的一个模块,依托于Spring,基于MVC的设计模式的一个Web MVC框架。SpringMVC作为Java开发最优秀的MVC框架(没有之一),国内几乎做Java开发的都会用到。学会手写自己的SpringMVC框架,做公司最靓的仔。

SpringMVC的运行流程

有关SpringMVC的使用和运行流程网上很多博客都有写到,这里不做具体介绍。
在这里插入图片描述
还有另外一个版本:
在这里插入图片描述
在Tomcat的conf/server.xml中可以配置处理静态资源的方式(比如处理以.js结尾的请求跳转到xxx)
在这里插入图片描述

SpringMVC的核心组件

在这里插入图片描述
在这里插入图片描述
查看springframework的源码,DispatcherServlet继承了FrameworkServlet,FrameworkServlet继承了HttpServletBean,HttpServletBean继承了HttpServlet同时实现了EnvironmentCapable、EnvironmentAware接口。

HttpServletBean:直接继承自Java的HttpServlet,作用是将Servlet中配置的参数设置到相应的属性。
FrameworkServlet:初始化了WebApplicationContext
DispatcherServlet:初始化了自身的9个组件
在这里插入图片描述
图中分为三大块,最下面是Spring MVC,基于Spring和servlet-api实现。
所以说SpringMVC依托于Spring,继承了HttpServlet。

HttpServlet是来自servlet-api Jar包,servlet-api Jar包定义了很多接口、Servlet规范(需要使用Servlet的地方才去实现这些接口,比如Tomcat、netty这些Web容器实现了Servlet规范,我们写的项目/Servlet丢到服务器中才能运行)。

在这里插入图片描述
查看源码,DispatcherServlet初始化了九大组件,比较常用的是HandlerMapping和HandlerAdapter

DispatcherServlet是SprinMVC的前端控制器,在Web.xml中配置,是一个中转类,接收请求,再中转给别的类来处理。

HandlerMapping维护了一个Map,请求的URL和处理请求的类的实例之间的映射。

HandlerAdapter:处理类,处理请求,处理完会返回一个ModelAndView的对象

DispatcherServlet再根据HandlerAdapter返回的ModelAndView封装一个ViewResolver

ModelAndView:Model和View两个对象集成,Model是数据,View表示页面属于什么类型(JSP/XML/PDF/JSON)
但是现在开发都是前后端分离的,不会直接返回页面的方式,很少会去直接返回ModelAndView对象,基本上都是使用RESTful API 返回Json和前端/客户端交互,SpringMVC的默认Json解析器是Jackson

SpringMVC的底层加载流程

加载

  1. 通过web.xml加载DispatcherServlet
  2. 加载Spring的xml配置文件(init-param来指定哪个xml文件作为SpringMVC的参数启动)
  3. 配置Servlet的URL地址,拦截哪些地址(URL-Pattern)

初始化

  • SpringMVC的核心流程:初始化9大组件(init)
  • 扫描指定包下的类,扫描指定注解的类(scanpackage=com.xxx.xxx)
  • 实例化,把扫描到的类实例化后放在上下文Context中(IOC的功能)
  • mapping操作(HandlerMapping),处理请求的url地址和对应类的映射关系

运行

  • DispatcherServlet前端控制器拦截请求,调用doDispatche,从HandlerMapping(map)中获取请求url的method
  • 得到method和处理类的全路径后,通过反射来调用执行(invoke)
  • response.print()/response.getWriter() 响应输出,可以设置Http头信息,将响应结果输出为什么类型(JSON/XML/HTML/PDF)

设计自己的SpringMVC框架

  • 读取配置:通过web.xml加载自己写的MyDispatcherServlet和读取配置文件。
  • 初始化,这里不需要把9大组件全部实现,只要实现HandlerMapping就行。
    (1)加载配置文件
    (2)扫描配置的包下的类
    (3)通过反射机制实例化包下的类,并放到ioc容器中(beanName-bean)beanName默认首字母小写;
    (4)实例化HandlerMapping
  • 运行
    (1)获取请求传入的参数并处理参数
    (2)通过初始化的HandlerMapping中拿出url对应的方法名,反射调用

手写自己的MVC框架

web.xml
  <servlet>
    <servlet-name>MySpringMvc</servlet-name>
    <servlet-class>com.zlin.mvc.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:application.properties</param-value>
    </init-param>
    <!-- load-on-startup 赋值为1表示项目丢到服务器中,项目一启动,容器一初始化时就会立马加载这个servlet -->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>MySpringMvc</servlet-name>
    <url-pattern>/test</url-pattern>
  </servlet-mapping>

application.properties

# 定义扫描包
scanPackage=com.zlin.mvc

自定义注解

package com.zlin.mvc.annotation;

import java.lang.annotation.*;

//jdk自带的注解(对自定义的注解进行注释的,注解的注解)  元注解
// @Target 表示自定义的这个注解能用在哪些地方, 用在类/方法/接口/字段/包..上面
// @Retention 表示注解的生命周期(被jvm加载 => 被jvm执行),
/* RetentionPolicy.RUNTIME 表示class文件被jvm加载成实例后还能获取到annotation的注释信息
    (即项目运行起来了能拿到annotation的信息) @Controller("/zlin")  能拿到value "/zlin"
 */
// 自定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {

    /**
     *  表示给Controller注册别名
     */
    String value() default "";
}

核心类DispatcherServlet

package com.zlin.mvc.servlet;

import com.google.gson.Gson;
import com.zlin.mvc.annotation.Controller;
import com.zlin.mvc.annotation.RequestMapping;
import com.zlin.mvc.annotation.ResponseBody;

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.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;

/**
 * 定义自己的前端控制器
 */
public class DispatcherServlet extends HttpServlet {
    private static final long seriaVersionUID = 1L;

    // 读取配置
    private Properties properties = new Properties();

    // 类的全路径名集合
    private List<String> classNames = new ArrayList<>();

    // ioc
    private Map<String, Object> ioc = new HashMap<>();

    // handlerMapping, url和对应具体方法的映射关系
    private Map<String, Method> handlerMapping = new HashMap<>();

    // controllerMap, url和对应controller的映射关系
    private Map<String, Object> controllerMap = new HashMap<>();

    @Override
    public void init(ServletConfig config) throws ServletException {
        // 1.加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));

        // 2.初始化所有关联的类,扫描配置的包下面所有的类
        doScanner(properties.getProperty("scanPackage"));

        // 3.拿到扫描到的类,通过反射,实例化,并且放到IOC容器中(k-v beanName-bean) beanName默认首字母小写
        doInstance();

        // 4.初始化handlerMapping(url和method的映射, 将请求的url能对应上被执行的method)
        initHandlerMapping();

    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            // 处理请求
            doDispatch(req, resp);
        } catch (Exception e) {
            resp.getWriter().write("Server Exception!");
        }
    }

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException{
        if(handlerMapping.isEmpty()){
            return;
        }
        String url = req.getRequestURI();
        String contextPath = req.getContextPath();

        // 拼接url 并把多个/替换成一个
        url = url.replace(contextPath, "").replaceAll("/+", "/");

        if (!this.handlerMapping.containsKey(url)) {
            resp.getWriter().write("404 NOT FOUND!");
            return;
        }

        Method method = this.handlerMapping.get(url);

        // 获取方法的参数列表
        Class<?>[] parameterTypes = method.getParameterTypes();

        // 获取请求的参数
        Map<String, String[]> parameterMap = req.getParameterMap();

        // 保存参数值
        Object[] paramValues = new Object[parameterTypes.length];

        // 方法的参数列表
        for (int i = 0; i < parameterTypes.length; i++) {
            // 根据参数名称, 做某些处理
            String requestParam = parameterTypes[i].getSimpleName();
            if (requestParam.equals("HttpServletRequest")) {
                // 参数类型已经明确 这里强转类型
                paramValues[i] = req;
                continue;
            }
            if (requestParam.equals("HttpServletResponse")) {
                paramValues[i] = resp;
                continue;
            }
            if (requestParam.equals("String")) {
                for (Entry<String, String[]> param : parameterMap.entrySet()) {
                    String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "").replaceAll(",", "");
                    paramValues[i] = value;
                }
            }
        }
        // 利用反射机制来调用
        try {
            Object object = method.invoke(this.controllerMap.get(url), paramValues);
            // 这里使用Gson  官方使用的是jackson
            if (method.isAnnotationPresent(ResponseBody.class)) {
                String strJson = new Gson().toJson(object);
                resp.getWriter().write(strJson);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void doLoadConfig(String location){
        if (location.startsWith("classpath:")) {
            location = location.replace("classpath:", "");
        } else if (location.contains("/")) {
            int lastSplitIndex = location.lastIndexOf('/');
            location = location.substring(lastSplitIndex+1, location.length());
        }

        // 把web.xml中的contextConfigLocation对应的value值的文件加载到流里面
        InputStream resourcesAsStream = this.getClass().getClassLoader().getResourceAsStream(location);
        try {
            // 用Properties文件加载文件里的内容
            properties.load(resourcesAsStream);
        } catch (IOException e){
            e.printStackTrace();
        } finally {
            // 关闭流
            if (resourcesAsStream != null) {
                try {
                    resourcesAsStream.close();
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }

    private void doScanner(String packageName){
        // 把所有的.替换成/   com.zlin.mvc => com/zlin/mvc
        URL url = this.getClass().getClassLoader().getResource("/" + packageName.replaceAll("\\.", "/"));
        File dir = new File(url.getFile());
        for (File file : dir.listFiles()) {
            if (file.isDirectory()) {
                // 递归扫描包
                doScanner(packageName + "." + file.getName());
            } else {
                String className = packageName + "." +file.getName().replace(".class", "");
                classNames.add(className);
                System.out.println("Spring容器扫描到的类有:" + packageName + "." +file.getName());
            }
        }
    }

    private void doInstance(){
        if (classNames.isEmpty()) {
            return;
        }
        for (String className : classNames) {
            try {
                Class<?> clazz = Class.forName(className);

                // 通过反射来实例化, 只有加@Controller需要实例化
                if (clazz.isAnnotationPresent(Controller.class)) {
                    Controller controller = clazz.getAnnotation(Controller.class);
                    String key = controller.value();
                    if (!"".equals(key) && key != null) {
                        ioc.put(key, clazz.newInstance());
                    } else {
                        // 只拿字节码上含有Controller.class 对象的信息
                        ioc.put(toLowerFirstWord(clazz.getSimpleName()), clazz.newInstance());
                    }
                } else {
                    continue;
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    /**
    * 建立映射关系
    */
    private void initHandlerMapping(){
        if (ioc.isEmpty()) {
            return;
        }
        try {
            for (Map.Entry<String, Object> entry : ioc.entrySet()) {
                Class<? extends Object> clazz = entry.getValue().getClass();
                if (!clazz.isAnnotationPresent(Controller.class)) {
                    continue;
                }

                // 拼接url, Controller头的url + 方法上面的url
                String baseUrl = "";
                if (clazz.isAnnotationPresent(RequestMapping.class)) {
                    RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
                    baseUrl = requestMapping.value();
                }
                Method[] methods = clazz.getMethods();
                for (Method method : methods) {
                    if (!method.isAnnotationPresent(RequestMapping.class)) {
                        continue;
                    }
                    RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                    String methodUrl = requestMapping.value();

                    String url = (baseUrl + "/" + methodUrl).replaceAll("/+", "/");
                    // 这里存放实例和method
                    handlerMapping.put(url, method);

                    Object tmpValue = null;
                    String ctlName = toLowerFirstWord(clazz.getSimpleName());
                    if (ioc.containsKey(ctlName)) {
                        tmpValue = ioc.get(ctlName);
                    } else {
                        tmpValue = clazz.newInstance();
                    }
                    controllerMap.put(url, tmpValue);
                    // controllerMap.put(url, clazz.newInstance()); 不这样,因为这样可能会重复创建Controller,ioc容器可能已经包含了Controller实例

                    System.out.println(url + "," +method);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private String toLowerFirstWord(String string){
        if (string == null || string.length() == 0) {
            return string;
        } else {
            String upperFirstWord = string.substring(0, 1);
            String lowerFirstWord = upperFirstWord.toLowerCase();
            string = string.replaceFirst(upperFirstWord, lowerFirstWord);
            return string;
        }
    }
}

定义Controller

package com.zlin.mvc.controller;

import com.zlin.mvc.annotation.Controller;
import com.zlin.mvc.annotation.RequestMapping;
import com.zlin.mvc.annotation.ResponseBody;

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

@Controller
@RequestMapping("/zlin")
public class MyController {

    @RequestMapping("/test")
    @ResponseBody
    public String test(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String name = request.getParameter("name");
        return "name:" + name;
    }
}

这样自己的SpringMVC框架就完成了,想要添加什么功能可以继续扩展。
和官方的SpringMVC框架肯定没法比,官方的SpringMVC功能强大太多了。当然,了解SpringMVC的底层运行原理和理解其中的设计模式才是最重要的。

项目的GitHub地址https://github.com/zhonglinliu123/MySpringMvc

  • 9
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值