跳转至

SpringMVC底层机制简单实现-01

主要完成:核心分发控制器+Controller和Service注入容器+对象自动装配+控制器方法获取参数+视图解析+返回 JSON格式数据

https://github.com/liyuelian/springmvc-demo.git

1.搭建开发环境

  1. 创建 Maven 项目,File-New-Project-Maven

image-20230209164653089

image-20230209164838732

image-20230209165214529

  1. 将 pom.xml 文件中的编译版本改为1.8

image-20230209165655198

  1. 在 src 目录下创建以下目录:

java 代码放在 java 目录下,相关的资源文件放在 resource 目录下,对 maven 的 web 项目而言,resource 就是类路径。前端页面放在 webapp 下,该目录对应之前的 web 目录。test/java 目录用于存放测试文件,测试需要的资源文件放在 test/resource 目录下。

image-20230209170051719

  1. 在 pom.xml 中引入基本的 jar 包
<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
  </dependency>

  <!--引入原生servlet依赖的jar包-->
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <!--
      1.scope表示引入jar包的作用范围,
      2.provided表示项目在打包放到生产环境时,不需要打上servlet-api.jar
      3.因为 tomcat本身就有该jar包,使用tomcat的即可,防止版本冲突
    -->
    <scope>provided</scope>
  </dependency>

  <!--引入dom4j,用于解析xml-->
  <dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
  </dependency>

  <!--引入常用的工具类jar包,该jar含有很多常用的类-->
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.5</version>
  </dependency>
</dependencies>

2.任务1-开发MyDispatcherServlet

说明:编写 MyDispatcherServlet,充当原生的 DispatcherServlet(即核心控制器)

2.1分析

image-20230209174244370

2.2代码实现

  1. 创建 src/main/java/com/li/myspringmvc/servlet/MyDispatcherServlet.java,充当原生的前端控制器。
package com.li.myspringmvc.servlet;

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

/**
 * @author 李
 * @version 1.0
 * 1.MyDispatcherServlet 充当原生的 DispatcherServlet,它的本质就是一个Servlet
 * 因此继承 HttpServlet
 */
public class MyDispatcherServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("MyDispatcherServlet doGet() 被调用..");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("MyDispatcherServlet doPost() 被调用..");
    }
}
  1. 创建 src/main/resources/myspringmvc.xml,充当原生的 applicationContext-mvc.xml(即 spring 容器配置文件)

  2. 配置 src/main/webapp/WEB-INF/web.xml

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>
    <servlet>
        <servlet-name>MyDispatcherServlet</servlet-name>
        <servlet-class>com.li.myspringmvc.servlet.MyDispatcherServlet</servlet-class>
        <!--给前端控制器指定配置参数,指定要操作的spring容器文件-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:myspringmvc.xml</param-value>
        </init-param>
        <!--要求该对象在tomcat启动时就自动加载-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>MyDispatcherServlet</servlet-name>
        <!--作为前端控制器,拦截所有请求-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>
  1. 配置 Tomcat,进行测试

  2. 浏览器访问 http://localhost:8080/li_springmvc/aaa

image-20230209175857755

3.任务2-实现客户端/浏览器可以请求控制层

3.1分析

image-20230210185548643

任务2的总目标是:

实现自己的 @Controller 注解和 @RequestMapping 注解,当浏览器访问指定的 URL 时,由前端控制器,找到 Controller 的某个方法,然后通过 tomcat 将数据返回给浏览器。

3.2代码实现

image-20230209221006109

步骤一:两个注解和测试Controller

(1)Controller 注解

package com.li.myspringmvc.annotation;

import java.lang.annotation.*;

/**
 * @author 李
 * @version 1.0
 * 该注解用于标识一个控制器组件
 * 1.@Target(ElementType.TYPE) 指定自定义注解可修饰的类型
 * 2.@Retention(RetentionPolicy.RUNTIME) 作用范围,RUNTIME使得可以通过反射获取自定义注解
 * 3.@Documented 在生成文档时,可以看到自定义注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
    String value() default "";
}

(2)RequestMapping 注解

package com.li.myspringmvc.annotation;

import java.lang.annotation.*;

/**
 * @author 李
 * @version 1.0
 * RequestMapping 注解用于指定控制器-方法的映射路径
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
    String value() default "";
}

(3)用于测试的控制器 MonsterController.java

package com.li.controller;

import com.li.myspringmvc.annotation.Controller;
import com.li.myspringmvc.annotation.RequestMapping;

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

/**
 * @author 李
 * @version 1.0
 */
@Controller
public class MonsterController {
    //编写方法,可以列出妖怪列表
    //springmvc支持原生的servlet api,为了看到底层机制,这里直接放入两个参数
    @RequestMapping(value = "/monster/list")
    public void listMonster(HttpServletRequest request, HttpServletResponse response) {
        //设置编码
        response.setContentType("text/html;charset=utf-8");
        //获取writer,返回提示信息
        try {
            PrintWriter printWriter = response.getWriter();
            printWriter.print("<h1>妖怪列表信息</h1>");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

步骤二:配置容器文件 springmvc.xml,指定扫描的包

指定扫描的包是为了之后使用注解获取需要反射的类

如果需要添加新的扫描包,在base-package添加包路径,用逗号表示间隔。

<?xml version="1.0" encoding="utf-8" ?>
<beans>
    <!--指定要扫描的包及其子包的java类-->
    <component-scan base-package="com.li.controller,com.li.service"/>
</beans>

步骤三:编写 XMLParse 工具类,用于解析 springmvc.xml,得到要扫描的包

package com.li.myspringmvc.xml;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;

/**
 * @author 李
 * @version 1.0
 * XMLParse用于解析spring配置文件
 */
public class XMLParse {
    public static String getBasePackage(String xmlFile) {
        SAXReader saxReader = new SAXReader();

        //maven的类路径是在target/li-springmvc/WEB-INF/classes/目录下
        //通过类的加载路径-->获取到spring配置文件[对应的资源流]
        InputStream inputStream =
                XMLParse.class.getClassLoader().getResourceAsStream(xmlFile);
        try {
            //得到配置文件的文档
            Document document = saxReader.read(inputStream);
            Element rootElement = document.getRootElement();
            Element componentScanElement = rootElement.element("component-scan");
            Attribute attribute = componentScanElement.attribute("base-package");
            String basePackage = attribute.getText();
            return basePackage;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
}

步骤四:开发MyWebApplicationContext,充当原生Spring容器,得到扫描类的全路径列表

即把指定目录包括子目录下的 java 类的全路径扫描到 ArrayList 集合中,以便之后反射。是否需要反射,还要取决于类中是否添加了@Controller注解

(1)MyWebApplicationContext.java 实现自定义的 spring 容器,目前先完成扫描工作

package com.li.myspringmvc.context;

import com.li.myspringmvc.xml.XMLParse;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * @author 李
 * @version 1.0
 * MyWebApplicationContext 是我们自定义的spring容器
 */
public class MyWebApplicationContext {
    //属性classFullPathList用于保存扫描包/子包的类的全路径
    private List<String> classFullPathList = new ArrayList<>();

    //该方法完成对自己的 spring容器的初始化
    public void init() {
        //返回的是我们在容器文件中配置的base-package的value
        String basePackage = XMLParse.getBasePackage("myspringmvc.xml");
        //这时你的 basePackage是像 com.li.controller,com.li.service 这样子的
        //通过逗号进行分割包
        String[] basePackages = basePackage.split(",");
        if (basePackages.length > 0) {
            //遍历这些包
            for (String pack : basePackages) {
                scanPackage(pack);
            }
        }
        System.out.println("扫描后的路径classFullPathList=" + classFullPathList);
    }

    /**
     * 该方法完成对包的扫描
     * @param pack 表示要扫描的包,如 "com.li.controller"
     */
    public void scanPackage(String pack) {
        //得到包所在的工作路径[绝对路径]
        // (1)通过类的加载器,得到指定包的工作路径[绝对路径]
        // (2)然后用斜杠代替点=>如 com.li.controller=>com/li/controller
        URL url =
                this.getClass().getClassLoader()
                        .getResource("/" + pack.replaceAll("\\.", "/"));
        // url=file:/D:/IDEA-workspace/li-springmvc/target/li-springmvc
        // /WEB-INF/classes/com/li/controller/
        //System.out.println("url=" + url);

        //根据得到的路径,对其进行扫描,把类的全路径保存到 classFullPathList属性中
        String path = url.getFile();
        System.out.println("path=" + path);
        //在io中,把目录也视为一个文件
        File dir = new File(path);
        //遍历 dir目录,因为可能会有[多个文件/子目录]
        File[] files = dir.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                //如果是目录,需要递归扫描
                //pack加上下一级的目录名继续下一层的扫描
                scanPackage(pack + "." + file.getName());
            } else {
                //这时得到的文件可能是.class文件,也可能是其他文件
                //就算是class文件,还需要考虑是否要注入到容器的问题
                //目前先把所有文件的全路径都保存到集合中,后面注入对象到spring容器时再考虑过滤
                String classFullPath =
                        pack + "." + file.getName().replaceAll(".class", "");
                classFullPathList.add(classFullPath);
            }
        }
    }
}

(2)通过 MyDispatcherServlet 前端控制器来调用并初始化 spring 容器

//添加init方法,用于初始化spring容器
@Override
public void init() throws ServletException {
    MyWebApplicationContext myWebApplicationContext = new MyWebApplicationContext();
    myWebApplicationContext.init();
}

(3)启动tomcat,后台成功获取到了路径,测试成功。

tomcat启动--加载了MyDispatcherServlet--通过该Servlet的init()生命周期方法初始化自定义的 spring 容器,同时调用自定义 spring 容器的 init 方法去扫描包

image-20230209215714429

image-20230209215753556

步骤五:完善MyWebApplicationContext(自定义 spring 容器),实例化对象到容器中

将扫描到的类,在满足添加了注解的情况下,通过反射注入到 ioc 容器

(1)部分代码:在MyWebApplicationContext中添加新属性 ioc 和新方法 executeInstance,修改init方法。

//定义属性ioc,用于存放反射生成的 bean对象(单例的)
public ConcurrentHashMap<String, Object> ioc = new ConcurrentHashMap<>();

/**
 * 该方法完成对自己的 spring容器的初始化
 */
public void init() {
    //返回的是我们在容器文件中配置的base-package的value
    String basePackage = XMLParse.getBasePackage("myspringmvc.xml");
    //这时你的 basePackage是像 com.li.controller,com.li.service 这样子的
    //通过逗号进行分割包
    String[] basePackages = basePackage.split(",");
    if (basePackages.length > 0) {
        //遍历这些包
        for (String pack : basePackages) {
            scanPackage(pack);
        }
    }
    System.out.println("扫描后的路径classFullPathList=" + classFullPathList);
    //将扫描到的类反射到ioc容器
    executeInstance();
    System.out.println("扫描后的ioc容器=" + ioc);
}

//...

/**
 * 该方法将扫描到的类,在满足条件的情况下进行反射,并放入到ioc容器中
 */
public void executeInstance() {
    //是否扫描到了类
    if (classFullPathList.size() == 0) {//没有扫描到类
        return;
    }
    //遍历 classFullPathList,进行反射
    try {
        for (String classFullPath : classFullPathList) {
            Class<?> clazz = Class.forName(classFullPath);
            //判断是否要进行反射(即是否添加了注解)
            if (clazz.isAnnotationPresent(Controller.class)) {
                Object instance = clazz.newInstance();
                //获取该对象的id,默认情况下为类名(首字母小写)
                String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase()
                        + clazz.getSimpleName().substring(1);
                String value = clazz.getAnnotation(Controller.class).value();
                if (!"".equals(value)) {//如果注解的value指定了id
                    beanName = value;
                }
                ioc.put(beanName, instance);
            }//如果有其他注解,可以进行扩展
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

}

(2)启动tomcat,反射成功。

image-20230209224358698

步骤六:完成请求URL和控制器方法的映射关系

image-20230210180607737

(1)MyHandler 保存url,控制器,控制器找中的方法,三者对应的映射关系。一个 MyHandler 对象就是一个映射关系。

package com.li.myspringmvc.handler;

import java.lang.reflect.Method;

/**
 * @author 李
 * @version 1.0
 * MyHandler对象用于记录 请求的url 和 控制器方法的映射关系
 */
public class MyHandler {
    private String url;//正确的url
    private Object controller;//需要调用的控制器
    private Method method;//控制器中url对应的方法

    public MyHandler(String url, Object controller, Method method) {
        this.url = url;
        this.controller = controller;
        this.method = method;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Object getController() {
        return controller;
    }

    public void setController(Object controller) {
        this.controller = controller;
    }

    public Method getMethod() {
        return method;
    }

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

    @Override
    public String toString() {
        return "MyHandler{" +
                "url='" + url + '\'' +
                ", controller=" + controller +
                ", method=" + method +
                '}';
    }
}

(2)MyDispatcherServlet.java 增加集合 handlerList,用于保存映射关系。 initHandlerMapping() 方法将映射保存到集合 handlerList 中。

这里的 initHandlerMapping() 对应原生的 HandlerMapping 映射处理器,只是将其简化为了一个方法

package com.li.myspringmvc.servlet;

import com.li.myspringmvc.annotation.Controller;
import com.li.myspringmvc.annotation.RequestMapping;
import com.li.myspringmvc.context.MyWebApplicationContext;
import com.li.myspringmvc.handler.MyHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;

/**
 * @author 李
 * @version 1.0
 * 1.MyDispatcherServlet充当原生的 DispatcherServlet,它的本质就是一个Servlet
 * 因此继承 HttpServlet
 */
public class MyDispatcherServlet extends HttpServlet {
    //定义属性 handlerList,保存 MyHandler对象[url和控制器方法的映射关系]
    private ArrayList<MyHandler> handlerList = new ArrayList<>();

    //定义ioc容器
    MyWebApplicationContext myWebApplicationContext = null;

    @Override
    public void init() throws ServletException {
        //初始化ioc容器
        myWebApplicationContext = new MyWebApplicationContext();
        myWebApplicationContext.init();
        //调用 initHandlerMapping(),完成url和控制器方法的映射
        initHandlerMapping();
        //测试输出 handlerList
        System.out.println("handlerList输出的结果=" + handlerList);
    }

    //doGet方法...

    //doPost方法...

    //该方法完成url和控制器方法的映射关联
    private void initHandlerMapping() {
        //判断当前的ioc容器是否为空
        if (myWebApplicationContext.ioc.isEmpty()) {
            //如果为空,就退出
            return;
        }
        //如果不为空,就遍历ioc容器的 bean对象,进行url映射处理
        //map的遍历
        Set<Map.Entry<String, Object>> entries = myWebApplicationContext.ioc.entrySet();
        for (Map.Entry<String, Object> entry : entries) {
            //先取出 bean的clazz对象
            Class<?> clazz = entry.getValue().getClass();
            //如果bean对象是一个Controller
            if (clazz.isAnnotationPresent(Controller.class)) {
                //取出所有的方法
                Method[] declaredMethods = clazz.getDeclaredMethods();
                //遍历所有的方法
                for (Method declaredMethod : declaredMethods) {
                    //如果该方法有 @RequestMapping注解
                    if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {
                        //如果有,就取出@RequestMapping注解的value值,即该方法的映射路径
                        String url = 
                            declaredMethod.getAnnotation(RequestMapping.class).value();
                        //创建 MyHandler对象,一个 MyHandler 对象就是一个映射关系
                        MyHandler myHandler = 
                            new MyHandler(url, entry.getValue(), declaredMethod);
                        //handlerList 集合保存映射关系
                        handlerList.add(myHandler);
                    }
                }
            }
        }
    }

}

(3)启动 tomcat,输出如下,测试成功。

handlerList输出的结果=[MyHandler{url='/monster/list', controller=com.li.controller.MonsterController@d76112f, method=public void com.li.controller.MonsterController.listMonster(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)}]

步骤七:完成 MyDispatcherServlet 分发请求到对应的控制器方法

(1)MyDispatcherServlet

增加方法 getMyHandler(),该方法根据用户的请求 url 找到对应的控制器方法,并返回对应的MyHandler对象(即映射关系)

增加 executeDispatch(),该方法完成分发请求。然后 doGet 或 doPost 方法调用 executeDispatch()

部分代码:

package com.li.myspringmvc.servlet;

//import ....

/**
 * @author 李
 * @version 1.0
 * 1.MyDispatcherServlet充当原生的 DispatcherServlet,它的本质就是一个Servlet
 * 因此继承 HttpServlet
 */
public class MyDispatcherServlet extends HttpServlet {
    //定义属性 handlerList,保存 MyHandler对象[url和控制器方法的映射关系]
    private ArrayList<MyHandler> handlerList = new ArrayList<>();

   //init方法..略..

    @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 {
        //System.out.println("MyDispatcherServlet doPost() 被调用..");
        //调用方法,完成分发请求
        executeDispatch(req, resp);
    }

    //initHandlerMapping方法..略..

    /**
     * 通过request对象的url匹配 MyHandler对象的url,如果没有就返回404
     * 如果匹配就反射调用对应的方法
     */
    public MyHandler getMyHandler(HttpServletRequest request) {
        //获取用户请求 url
        //这里的 requestURL为 /web工程路径/xxx 形式的
        String requestURL = request.getRequestURI();
        //方案一:切割掉前面的 web工程路径
        int length = getServletContext().getContextPath().length();
        requestURL = requestURL.substring(length);
        System.out.println("requestURL=" + requestURL);
        //方案二:tomcat直接配置项目工程路径为 /
        //方案三:保存 MyHandler对象时的 url 连项目工程路径一起保存

        //遍历 handlerList
        for (MyHandler myHandler : handlerList) {
            //如果 requestURI和集合中的某个url相等
            if (requestURL.equals(myHandler.getUrl())) {
                //返回这个对象
                return myHandler;
            }
        }
        //如果没有匹配,即返回null
        return null;
    }

    //编写方法,完成分发请求
    private void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
        MyHandler myHandler = getMyHandler(request);
        try {
            //如果 myHandler为 null,说明请求 url没有匹配的方法,即用户请求的资源不存在
            if (myHandler == null) {
                response.getWriter().print("<h1>404 NOT FOUND</h1>");
            } else {//匹配成功,就反射调用控制器的方法
                myHandler.getMethod()
                        .invoke(myHandler.getController(), request, response);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

(2)启动tomcat,浏览器中分别访问:

image-20230210195533003

image-20230210195552419

测试成功。至此,任务2的目标功能实现完毕。