手写模拟SpringMVC

Spring 专栏收录该内容
19 篇文章 4 订阅

前言:

springmvc想必大家都有用过,但是你有去真正的了解过springmvc究竟是怎么进行工作的吗?

为什么加上@controller @RequestMapping等注解,就会映射到指定类的方法上呢?

下面我们就来探究一下,

手写模仿一个springmvc。

注:代码演示主要侧重原理的模拟,有很多细节存在不足,请勿较真。

          如果有什么改进意见,欢迎大家提出,不胜感激 :)

          项目源码地址:https://github.com/wangming2674/spring-mvc-test     项目:imitate-mvc

一、 了解springmvc原理 :

1.DsipatcherServlet简介

我们都知道在平时使用springmvc时,总要配置一个springmvc的核心处理器,即DispatcherServlet,其实它本质上就是一个servlet,我们可以通过类关系图看到DispatcherServlet继承了HttpServlet

那DispatcherServlet做了什么呢?

简单的来说,就是重写了里面的doPost()方法,用来去处理mvc的相关事宜即控制流程

明白了这个就好说了,那么我们也可以这样去实现一个我们自己的DispatcherServlet。

 

2.spring mvc调用到Controller执行的原理

接下来,让我们捋一下,spring mvc调用到Controller执行的大体步骤:

  1. 通过加载配置文件web.xml进而加载spring-mvc.xml。
  2. 根据配置文件给定的目录来扫描整个项目。
  3. 扫描所有加了@Controller注解的类。
  4. 当扫描到加了@Controller注解的类之后遍历里面所有的方法。
  5. 拿到方法对象之后 解析方法上面是否加了@RequestMapping注解。
  6. 定义一个Map集合把@RequstMapping的value 与方法对象绑定起来,即Map<String,Method>。
  7. 定义一个Map把声名该方法的类的对象绑定起来,即Map<String,Object>。
  8. 拦截到请求之后拿到请求的URI。
  9. 处理请求,例如执行指定方法,返回字符串或跳转到相应视图。

了解了这些,下面就让我们开始动手实现一个自己的springMVC。

 

二、模拟springMVC实现 

引入Maven依赖:

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

       <!--用来解析XML,你也可以采用其他解析技术,这里为了方便,使用dom4j-->
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>

1.创建我们自己的evanMvc.xml

类似spring-mvc.xml,这里我们叫做evanMvc.xml,并在其中写好相应配置。

<beans>
    <componentScan package="com">
    </componentScan>

    <view prefix="/page/" suffix=".html">
    </view>
</beans>

2.创建我们自己的DispatcherServlet。

类似springmvc的DispatcherServlet,这里我们叫做EvanDispatcherServlet。创建完后,让它继承HttpServlet并重写init()、doGet()、doPost()方法,代码如下。

package com.evan.servlet;

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.IOException;

/**
 * @ClassName EvanDispatcherServlet
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/11/14 23:38
 */
public class EvanDispatcherServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

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

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }
}

3.配置web.xml

将我们的EvanDispatcherServlet和evanMvc.xml配置进去,并拦截*.do请求,当然你也可以选择拦截其他的请求。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <servlet>
        <servlet-name>evanMvc</servlet-name>
        <servlet-class>com.evan.servlet.EvanDispatcherServlet</servlet-class>
        <init-param>
            <param-name>xmlPathLocal</param-name>
            <param-value>evanMvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>evanMvc</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

4.编写我们所需要的注解。

如果你对自定义注解有什么疑问,请参考我的另一篇文章=>Java中自定义注解的使用详解

@Controller:

package com.evan.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @ClassName Controller
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/11/14 23:36
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}

 @RequestMapping

package com.evan.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @ClassName RequestMapping
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/11/14 23:42
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String value() default "";
}

@ResponseBody

package com.evan.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @ClassName ResponseBody
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/11/14 23:43
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

5.编写TestController

package com.evan.controller;

import com.evan.annotation.Controller;
import com.evan.annotation.RequestMapping;
import com.evan.annotation.ResponseBody;
import com.evan.entity.UserEntity;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @ClassName TestController
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/11/14 23:37
 */
@Controller
@RequestMapping("/test")
public class TestController {

    @RequestMapping("/show.do")
    @ResponseBody
    public Object show(String content, HttpServletRequest request, HttpServletResponse response, UserEntity userEntity){
        System.out.println(content);
        System.out.println(request);
        System.out.println(response);
        System.out.println(userEntity);
        return  "Show is successful.";

    }

    @RequestMapping("/view.do")
    public Object view(){
        return "index";
    }
}

6.编写实体类

这里我们为了方便测试给方法绑定object类型的参数,需要创建一个实体类UserEntity。

package com.evan.entity;

/**
 * @ClassName UserEntity
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/11/14 23:39
 */
public class UserEntity {

    private String name;

    private String age;

    private String sex;


    @Override
    public String toString() {
        return "UserEntity{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

7.创建视图index.html

为了后续方便测试,我们还需要创建一个视图文件。主要是为了测试解析跳转视图。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>恭喜你测试成功 : )</h1>
<a href="https://evanwang.blog.csdn.net/"><b>→ Evan'Blog</b></a>
<br>
<br>
<b>如果你有收获,麻烦来我的博客加个关注点个赞吧 谢谢!</b>
</body>
</html>

8.完善EvanDispatcherServlet

package com.evan.servlet;

import com.evan.annotation.Controller;
import com.evan.annotation.RequestMapping;
import com.evan.annotation.ResponseBody;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

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.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName EvanDispatcherServlet
 * @Description
 * @Author EvanWang
 * @Version 1.0.0
 * @Date 2019/11/14 23:38
 */
public class EvanDispatcherServlet extends HttpServlet {

    private static String COMPONENT_SCAN_ELEMENT_PACKAGE_NAME = "package";

    private static String COMPONENT_SCAN_ELEMENT_NAME = "componentScan";

    private static String XML_PATH_LOCAL = "xmlPathLocal";

    private static String prefix = "";
    private static String suffix = "";
    //获取类加载的根路径 "/F:/project/imitate-mvc/target/classes/"
    private static String projectPath = EvanDispatcherServlet.class.getResource("/").getPath();
    //存放URI和其对应的Method
    private static Map<String, Method> methodMap = new HashMap<>();
    //存放method和声明方法的class的对象。
    private static Map<String, Object> controllerInstanceMap = new HashMap<>();


    @Override
    public void init(ServletConfig config) throws ServletException {
        projectPath = projectPath.replaceAll("%20", " ");
        //获取由tomcat解析的web.xml中,xmlPathLocal对应的属性值。
        String initParaValue = config.getInitParameter(XML_PATH_LOCAL);
        //解析xml文件,evanMvc.xml路径绝对路径
        File mvcXml = new File(projectPath + initParaValue);
        //因为我们要使用dom4j解析xml,先将文件对象转化成Document。
        Document document = parseFileToDocument(mvcXml);
        //根元素<beans></beans>
        Element rootElement = document.getRootElement();
        //获取<view></view>中的值
        Element viewElement = rootElement.element("view");
        prefix = viewElement.attribute("prefix").getValue();
        suffix = viewElement.attribute("suffix").getValue();
        //获取<componentScan></componentScan>的值
        Element componentScanElement = rootElement.element(COMPONENT_SCAN_ELEMENT_NAME);
        String packageValue = componentScanElement.attribute(COMPONENT_SCAN_ELEMENT_PACKAGE_NAME).getValue();
        // 当前com文件夹的路径"/F:/project/imitate-mvc/target/classes/com"
        scanProjectByPath(projectPath + packageValue);

    }

    /**
     * @param file :你的xml文件对象
     * @return
     */
    public Document parseFileToDocument(File file) {
        SAXReader saxReader = new SAXReader();
        try {
            return saxReader.read(file);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }

    //递归解析项目所有文件 目的:最终扫描出我们定义定的TestController
    public void scanProjectByPath(String path) {
        File file = new File(path);
        scanControllerFile(file);
    }

    public void scanControllerFile(File file) {
        //递归解析项目
        if (file.isDirectory()) {
            for (File insideFile : file.listFiles()) {
                scanControllerFile(insideFile);
            }
        } else {
            //如果不是文件夹,注意:这里在处理时候会出现\\的转义问题
            //当前会得到的路径(filePath):F:\project\imitate-mvc\target\classes\com\evan\annotation\TestController.class
            //最终我们希望得到:com.evan.controller.TestController
            String filePath = file.getPath();
            //截取文件后缀,判断是否类.class文件。
            String suffix = filePath.substring(filePath.lastIndexOf("."));
            if (suffix.equals(".class")) {
                //替换得到 com\evan\controller\TestController.class
                String classPath = filePath.replace(new File(projectPath).getPath() + "\\", "");
                //替换得到com.evan.controller.TestController.class
                classPath = classPath.replaceAll("\\\\", ".");
                String className = classPath.substring(0, classPath.lastIndexOf("."));
                handleControllerClass(className);
            }

        }
    }

    private void handleControllerClass(String className) {
        try {
            Class<?> clazz = Class.forName(className);
            if (clazz.isAnnotationPresent(Controller.class)) {
                RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
                String classRequestMappingUrl = "";
                if (classRequestMapping != null) {
                    classRequestMappingUrl = classRequestMapping.value();
                }
                for (Method method : clazz.getDeclaredMethods()) {
                    //判断是不是合成方法
                    if (!method.isSynthetic()) {
                        RequestMapping annotation = method.getAnnotation(RequestMapping.class);
                        if (annotation != null) {
                            String methodRequestMappingUrl;
                            methodRequestMappingUrl = annotation.value();
                            //打印下结果
                            System.out.println("类:" + clazz.getName() + "的" + method.getName() + "方法被映射到了" + classRequestMappingUrl + methodRequestMappingUrl + "上面");
                            //将指定url对应的method放入methodMap。
                            methodMap.put(classRequestMappingUrl + methodRequestMappingUrl, method);
                            controllerInstanceMap.put(method.getName(), clazz.newInstance());
                        }

                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //为了方便我们在doGet中,调用doPost
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //拿到请求的URI
        String requestURI = req.getRequestURI();
        //通过请求的URI拿到对应的method
        Method method = methodMap.get(requestURI);
        if (method != null) {
            //形参数组,jdk8以前直接拿参数名称拿不到
            Parameter[] formalParameters = method.getParameters();
            //实参数组
            Object[] actualParameters = new Object[formalParameters.length];
            //对参数做简单处理
            for (int index = 0; index < formalParameters.length; index++) {
                Parameter parameter = formalParameters[index];
                String name = parameter.getName();
                Class type = parameter.getType();
                if (type.equals(String.class)) {
                    actualParameters[index] = req.getParameter(name);
                } else if (type.equals(HttpServletRequest.class)) {
                    actualParameters[index] = req;
                } else if (type.equals(HttpServletResponse.class)) {
                    actualParameters[index] = resp;
                } else {
                    try {
                        //根据类型创建对象
                        Object paramObject = type.newInstance();
                        for (Field field : type.getDeclaredFields()) {
                            field.setAccessible(true);
                            String fieldName = field.getName();
                            field.set(paramObject, req.getParameter(fieldName));
                        }
                        actualParameters[index] = paramObject;
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            try {
                Object controller;
                //获得声明当前方法的controller对象
                controller = controllerInstanceMap.get(method.getName());
                //TestController里的方法返回值
                Object returnValue = method.invoke(controller, actualParameters);
                // 判断返回值是否是Void
                if (!method.getReturnType().equals(Void.class)) {
                    ResponseBody annotation = method.getAnnotation(ResponseBody.class);
                    if (annotation != null) {
                        //这里应该处理将对象转化成json字符串的格式,时间有限就暂时省略了,有兴趣的小伙伴可以实现下。
                        resp.getWriter().write(String.valueOf(returnValue));
                    } else {
                        // /page/index.html
                        req.getRequestDispatcher(prefix + returnValue + suffix).forward(req, resp);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            resp.setStatus(404);
        }
    }
}

 

三、测试

1.启动项目

终于到了测试阶段了,由于我们这里采用了tomcat7-maven-plugin,所以可以直接用命令启动Maven项目,而不用去手动配置tomcat了。

我用的是idea,点击这里,进入后:

 点击加号,然后点击maven

然后在Command line中输入:tomcat7:run   然后点击ok,再点击启动按钮就可以启动了。

 

2.代码测试

第一段测试:带@Response注解,返回字符串。

首先,项目启动后,我们可以看到控制台打印结果,证明方法映射成功。

 接着我们在地址栏中输入,

localhost/test/show.do?content=hello&name=evan&age=18&sex=man

可以在日志中看到:

页面中返回: 

测试结果:成功。

 

第二段测试:解析跳转到指定视图

在地址栏中输入:

localhost/test/view.do

我们可以看到页面返回结果:

测试结果:成功。

 

四、总结 

好了我们自己手写模仿的springmvc大功告成了,是不是对springmvc的一些工作原理理解的更透彻了呢^_^。

当然springMVC的设计与处理要远比我们手动模拟的复杂的多得多,就比如说spring和springmvc整合以后,bean的初始化

等等是如何做的呢?如何拓展这些功能呢,都是要学习的。

可不要觉得这样就把springmvc吃透了哦。

学习框架从原理层面去加深理解,学习的主要是框架的思想,这些思想能够帮助我们在面对后续的编程学习和复杂的工作

时,更加游刃有余。

关于手写模拟springmvc就介绍到这里,如果大家有什么好的建议,欢迎提出。

有什么问题,请给我留言,欢迎一切友善的交流。

 

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值