自己实现SpringMVC 底层机制

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 自己实现SpringMVC 底层机制

自己实现SpringMVC 底层机制[一]


搭建SpringMVC 底层机制开发环境


  1. 1创建Maven 项目
  2. 2对Myspringmvc 进行配置: 修改my-springmvc\pom.xml , 将1.7修改成1.8.
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>


  1. 3对my-springmvc 进行配置: 创建main 和test 相关的源码目录和资源目录和测试目录

a509e4637de440c39df0bc384aaf9e81.png


  1. 4引入需要的基本的jar 包, 修改my-springmvc\pom.xml
 <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本身是有servlet的jar, 到时直接使用tomcat本身的servlet-api.jar,防止版本冲突
            -->
            <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>


自己实现SpringMVC 底层机制【核心分发控制器+ Controller 和Service 注入容器+ 对象自动装配+ 控制器方法获取参数+ 视图解析+ 返回JSON 格式数据】


实现任务阶段1- 开发myDispatcherServlet


编写myDispatcherServlet 充当原生的DispatcherServlet(即核心控制器)


73f4227b26de48b8ab495a102c18e51e.png


  1. 1创建src\main\java\com\myspringmvc\servlet\myDispatcherServlet.java
public class myDispatcherServlet 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);
        }
}


  1. 2创建src\main\resources\myspringmvc.xml, 充当原生的applicationContext-mvc.xml 文件

(就是spring 的容器配置文件, 比如指定要扫描哪些包下的类), 先创建个空的文件


3修改src\main\webapp\WEB-INF\web.xml, 完成myDispatcherServlet 的配置

<!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>
  <!--配置myDispatcherServlet, 作为我们自己的前端控制器-->
  <servlet>
    <servlet-name>myDispatcherServlet</servlet-name>
    <servlet-class>com.myspringmvc.servlet.myDispatcherServlet</servlet-class>
    <!--给myDispatcherServlet配置参数,指定要操作的spring容器配置文件-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:myspringmvc.xml</param-value>
    </init-param>
    <!--myDispatcherServlet在tomcat启动时,就自动加载-->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>myDispatcherServlet</servlet-name>
    <!--因为myDispatcherServlet作为前端控制器,所以需要拦截所有请求,url-pattern配置 /-->
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>


配置Tomcat, 完成测试


修改my-springmvc\src\main\java\com\servlet\myDispatcherServlet.java

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()被调用");
        }
}

启动Tomcat,完成测试.


实现任务阶段2- 完成客户端/浏览器可以请求控制层


创建自己的Controller 和自定义注解

创建my-springmvc\src\main\java\com\controller\MonsterController.java

public class MonsterController {
    public void listMonsters(HttpServletRequest request, HttpServletResponse response) {
        response.setContentType("text/html;charset=utf-8");
        try {
                PrintWriter printWriter = response.getWriter();
                printWriter.write("<h1>妖怪列表</h1>");
        } catch (IOException e) {
          e.printStackTrace();
        }
    }
}


创建自定义注解,my-springmvc\src\main\java\com\myspringmvc\annotation\Controller.java

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
      String value() default "";
}


创建自定义注解,my-springmvc\src\main\java\com\myspringmvc\annotation\RequestMapping.java

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
  String value() default "";
}


配置myspringmvc.xml


在该文件指定,我们的springmvc 要扫描的包

<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<component-scan base-package="com.controller"></component-scan>
</beans>


编写XMLParser 工具类,可以解析myspringmvc.xml,得到要扫描的包


创建my-springmvc\src\main\java\com\myspringmvc\xml\XMLPaser.java

//XMLParser 用于解析spring配置文件
public class XMLParser {
    public static String getBasePackage(String xmlFile) {
        SAXReader saxReader = new SAXReader();
        //通过得到类的加载路径-》获取到spring配置文件.[对应的资源流]
        InputStream inputStream =  XMLParser.class.getClassLoader().getResourceAsStream(xmlFile);
        try {
            //得到xmlFile文档
            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 "";
    }
}


创建my-springmvc\src\test\java\com\test\mySpringMvcTest.java 完成测试

public class mySpringMvcTest {
    @Test
    public void readXML() {
        String basePackage = XMLPaser.getbasePackage("myspringmvc.xml");
        System.out.println("basePackage= " + basePackage);
    }
}


开发myWebApplicationContext,充当Spring 容器-得到扫描类的全路径列表


即把指定的目录包括子目录下的java 类的全路径扫描到集合中,比如ArrayList。

扫描后的classFullPathList= 
    [com.controller.MonsterController,
     com.service.impl.MonsterServiceImpl,
     com.service.MonsterService]


创建my-springmvc\src\main\java\com\myspringmvc\context\myWebApplicationContext.java

public class myWebApplicationContext {
    private ArrayList<String> classFullPathList = new ArrayList<>();
  // 初始化自定义的spring 容器, 目标就是把要@Controller 等等初始化到容器中
    public void init() {
        //这里是写的固定的spring容器配置文件=>做活
        //String basePackage = XMLParser.getBasePackage("myspringmvc.xml");
        String basePackage =   XMLParser.getBasePackage(configLocation.split(":")[1]);
        //这时basePackage => com.controller,com.service
        String[] basePackages = basePackage.split(",");
        //遍历basePackages, 进行扫描
        if (basePackages.length > 0) {
            for (String pack : basePackages) {
                scanPackage(pack);//扫描
            }
        }
        //测试
        System.out.println("扫描后的= classFullPathList=" + classFullPathList);
    }
    //创建方法,完成对包的扫描
     //@param pack 表示要扫描的包,比如"com.controller"
    public void scanPackage(String pack) {
        //得到包所在的工作路径[绝对路径]
        //下面这句话的含义是 通过类的加载器,得到你指定的包对应的 工作路径[绝对路径]
        //比如 "com.controller" => url 是 D:\my-springmvc\target\my-springmvc\WEB-INF\classes\com\controller
        //细节说明: 1. 不要直接使用Junit测试, 否则 url null
        //             2. 启动tomcat来测试
        URL url =
                this.getClass().getClassLoader()
                        .getResource("/" + pack.replaceAll("\\.", "/"));
        //System.out.println("url=" + url);
        //根据得到的路径, 对其进行扫描,把类的全路径,保存到classFullPathList
        String path = url.getFile();
        System.out.println("path= " + path);
        //在io中,把目录,视为一个文件
        File dir = new File(path);
        //遍历dir[文件/子目录]
        for (File f : dir.listFiles()) {
            if (f.isDirectory()) {//如果是一个目录,需要递归扫描
                scanPackage(pack + "." + f.getName());
            } else {
                //说明:这时,你扫描到的文件,可能是.class, 也可能是其它文件
                //就算是.class, 也存在是不是需要注入到容器
                //目前先把文件的全路径都保存到集合,后面在注入对象到容器时,再处理
                String classFullPath =
                        pack + "." + f.getName().replaceAll(".class", "");
                classFullPathList.add(classFullPath);
            }
        }
    }
}


修改com\myspringmvc\servlet\MyWebApplicationContext.java , 增加

@Override
public void init(ServletConfig config) throws ServletException {
    MyWebApplicationContext myWebApplicationContext = new   myWebApplicationContext();
    myWebApplicationContext.init();
}

启动Tomcat 完成测试,看看扫描是否成功. 需要使用Tomcat 启动方式完成测试,直接用Junit 测试URL 是null


完善myWebApplicationContext,充当Spring 容器-实例化对象到容器中。


将扫描到的类, 在满足条件的情况下(即有相应的注解@Controller @Service…), 反射注入到ioc 容器。

扫描后的ioc={monsterController=com.controller.MonsterController@690aac1e}。


修改com\myspringmvc\context\MyWebApplicationContext.java

public class MyWebApplicationContext {
private ArrayList<String> classFullPathList = new ArrayList<>();
//ioc 用于存放反射后的bean 对象.
private ConcurrentHashMap<String, Object> ioc =  new ConcurrentHashMap<>();
  // 初始化自定义的spring 容器, 目标就是把要@Controller 等等初始化到容器中
    public void init() {
        //这里是写的固定的spring容器配置文件.?=>做活
        //String basePackage = XMLParser.getBasePackage("Myspringmvc.xml");
        String basePackage =
                XMLParser.getBasePackage(configLocation.split(":")[1]);
        //这时basePackage => com.controller,com.service
        String[] basePackages = basePackage.split(",");
        //遍历basePackages, 进行扫描
        if (basePackages.length > 0) {
            for (String pack : basePackages) {
                scanPackage(pack);//扫描
            }
        }
        System.out.println("扫描后的= classFullPathList=" + classFullPathList);
        //将扫描到的类, 反射到ico容器
        executeInstance();
        System.out.println("扫描后的 ioc容器= " + ioc);
        //完成注入的bean对象,的属性的装配
        executeAutoWired();
        System.out.println("装配后 ioc容器= " + ioc);
    }
    //创建方法,完成对包的扫描
    /**
     * @param pack 表示要扫描的包,比如"com.controller"
     */
    public void scanPackage(String pack) {
        //得到包所在的工作路径[绝对路径]
        //下面这句话的含义是 通过类的加载器,得到你指定的包对应的 工作路径[绝对路径]
        //比如 "com.controller" => url 是 D:\My-springmvc\target\My-springmvc\WEB-INF\classes\com\controller
        //细节说明: 1. 不要直接使用Junit测试, 否则 url null
        //             2. 启动tomcat来吃测试
        URL url = this.getClass().getClassLoader()
                        .getResource("/" + pack.replaceAll("\\.", "/"));
        //System.out.println("url=" + url);
        //根据得到的路径, 对其进行扫描,把类的全路径,保存到classFullPathList
        String path = url.getFile();
        System.out.println("path= " + path);
        //在io中,把目录,视为一个文件
        File dir = new File(path);
        //遍历dir[文件/子目录]
        for (File f : dir.listFiles()) {
            if (f.isDirectory()) {//如果是一个目录,需要递归扫描
                scanPackage(pack + "." + f.getName());
            } else {
                //说明:这时,你扫描到的文件,可能是.class, 也可能是其它文件
                //就算是.class, 也存在是不是需要注入到容器
                //目前先把文件的全路径都保存到集合,后面在注入对象到容器时,再处理
                String classFullPath =
                        pack + "." + f.getName().replaceAll(".class", "");
                classFullPathList.add(classFullPath);
            }
        }
    }
}


完成测试(提示: 启动tomcat 来加载MyDispatcherServlet 的方式测试)


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


示意图分析


9954f8c60506417a9a489908b8b600d2.png


-将配置的@RequestMapping 的url 和对应的控制器-方法映射关系保存到集合中

如图

handlerList= [MyHandler{url='/monster/list',
controller=com.controller.MonsterController@714dab1, method=public void
com.controller.MonsterController.listMonsters(javax.servlet.http.HttpServletReques
t,javax.servlet.http.HttpServletResponse)}]


创建src\main\java\com\myspringmvc\handler\MyHandler.java

//MyHandler 对象记录请求的 url 和 控制器方法映射关系
public class MyHandler {
    private String url;
    private Object controller;
    private Method method;
    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 +
                '}';
    }
}


修改src\main\java\com\myspringmvc\servlet\MyDispatcherServlet.java

public class MyDispatcherServlet extends HttpServlet {
    //存放url 和控制器-方法的映射关系
    private ArrayList<MyHandler> handlerList =
    new ArrayList<>();
    private MyWebApplicationContext myWebApplicationContext;
    @Override
    public void init(ServletConfig config) throws ServletException {
        myWebApplicationContext = new MyWebApplicationContext();
        myWebApplicationContext.init();
        //调用initHandlerMapping()
        搞定完成控制器层url---> Controller ---> 方法的映射关系
        initHandlerMapping();
        //测试(启动Tomcat 装载MyDispatcherServlet 对象方式来测试)
        System.out.println("handlerList= " + handlerList);
    }
    @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. 完成控制器层url---> Controller ---> 方法的映射关系(该关系封装到 MyHandler 对象)
    * 2. 并放入到handlerList 集合中
    * 3. 后面我们可以通过handlerList 结合找到某个url 请求对应的控制器的方法(!!!)
    */
    private void initHandlerMapping() {
        if(myWebApplicationContext.ioc.isEmpty()) {
          throw new RuntimeException("spring ioc 容器为空");
       }
        for(Map.Entry<String,Object> entry:
        myWebApplicationContext.ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();
            if(clazz.isAnnotationPresent(Controller.class)) {
                    Method[] declaredMethods = clazz.getDeclaredMethods();
                        for (Method declaredMethod : declaredMethods) {
                                if(declaredMethod.isAnnotationPresent(RequestMapping.class)) {
                                        RequestMapping requestMappingAnnotation =
                                        declaredMethod.getAnnotation(RequestMapping.class);
                                        String url = requestMappingAnnotation.value();
                                        handlerList.add(new   MyHandler(url,entry.getValue(),declaredMethod));
                                }
                        }
                 }
            }
    }
}


完成测试(启动Tomcat , 加载MyDispatcherServlet 方式), 注意看后台输出。


完成MyDispatcherServlet 分发请求到对应控制器方法


当用户发出请求,根据用户请求url 找到对应的控制器-方法, 并反射调用。

如果用户请求的路径不存在,返回404。

修改com\myspringmvc\servlet\MyDispatcherServlet.java

/**
 * 1. MyDispatcherServlet充当原生DispatcherServlet
 * 2. 本质是一个Servlet, 继承HttpServlet
 */
public class MyDispatcherServlet extends HttpServlet {
    //定义属性 handlerList , 保存MyHandler[url和控制器方法的映射]
    private List<MyHandler> handlerList =
            new ArrayList<>();
    //定义属性myWebApplicationContext,自己的spring容器
    MyWebApplicationContextmyWebApplicationContext = null;
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        //获取到web.xml中的 contextConfigLocation
        /*
         <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath:myspringmvc.xml</param-value>
        </init-param>
         */
        String configLocation =
                servletConfig.getInitParameter("contextConfigLocation");
        //创建自己的spring容器
       myWebApplicationContext =
                new MyWebApplicationContext(configLocation);
       myWebApplicationContext.init();
        //调用 initHandlerMapping , 完成url和控制器方法的映射
        initHandlerMapping();
        //输出handlerList
        System.out.println("handlerList初始化的结果= " + handlerList);
    }
    @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);
    }
    //编写方法,完成url 和 控制器方法的映射
    private void initHandlerMapping() {
        if (myWebApplicationContext.ioc.isEmpty()) {
            //判断当前的ioc容器是否为null
            return;
        }
        //遍历ioc容器的bean对象,然后进行url映射处理
        //java基础 map的遍历
        for (Map.Entry<String, Object> entry :myWebApplicationContext.ioc.entrySet()) {
            //先取出注入的Object的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值->就是映射路径
                        RequestMapping requestMappingAnnotation =
                                declaredMethod.getAnnotation(RequestMapping.class);
                        //这里可以把工程路径+url
                        //getServletContext().getContextPath()
                        // /springmvc/monster/list
                        String url = requestMappingAnnotation.value();
                        //创建MyHandler对象->就是一个映射关系
                        MyHandlermyHandler = new MyHandler(url, entry.getValue(), declaredMethod);
                        //放入到handlerList
                        handlerList.add(myHandler);
                    }
                }
            }
        }
    }
}

完成测试(启动Tomcat)。


注意

因为getRequestURL返回的是主机后的所有内容,为了和handlermapping存放的URL一致,需要把tomcat配置的application context修改成 /,


否则匹配不上,因为按照原来的得到的是my_springmvc/monster/list.


增加方法和控制器


创建my-springmvc\src\main\java\com\controller\OrderController.java

@Controller
public class OrderController {
    @RequestMapping(value = "/order/list")
    public void listOrder(HttpServletRequest request,  HttpServletResponse response)  {
        //设置编码和返回类型
        response.setContentType("text/html;charset=utf-8");
        //获取writer返回信息
        try {
            PrintWriter printWriter = response.getWriter();
            printWriter.write("<h1>订单列表信息</h1>");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @RequestMapping(value = "/order/add")
    public void addOrder(HttpServletRequest request,  HttpServletResponse response)  {
        //设置编码和返回类型
        response.setContentType("text/html;charset=utf-8");
        //获取writer返回信息
        try {
            PrintWriter printWriter = response.getWriter();
            printWriter.write("<h1>添加订单...</h1>");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


相关文章
|
8月前
|
设计模式 前端开发 Java
【深入浅出Spring原理及实战】「夯实基础系列」360全方位渗透和探究SpringMVC的核心原理和运作机制(总体框架原理篇)
【深入浅出Spring原理及实战】「夯实基础系列」360全方位渗透和探究SpringMVC的核心原理和运作机制(总体框架原理篇)
80 0
|
XML JSON 前端开发
SpringMVC系列(六)之JSON数据返回以及异常处理机制
SpringMVC系列(六)之JSON数据返回以及异常处理机制
|
JSON 前端开发 Java
SpringMVC之JSON数据返回&异常处理机制
SpringMVC之JSON数据返回&异常处理机制
105 0
|
JSON 前端开发 Java
【SpringMVC】JSON注解&全局异常处理机制(二)
【SpringMVC】JSON注解&全局异常处理机制(二)
76 0
|
存储 JSON 前端开发
SpringMVC之JSON返回&异常处理机制(带你学习新的SpringMVC武功秘籍)
SpringMVC之JSON返回&异常处理机制(带你学习新的SpringMVC武功秘籍)
223 0
|
XML JSON 开发框架
【推荐】SpringMVC与JSON数据返回及异常处理机制的使用
【推荐】SpringMVC与JSON数据返回及异常处理机制的使用
125 0
|
17天前
|
存储 前端开发 Java
【SpringMVC】——Cookie和Session机制
获取URL中参数@PathVarible,上传文件@RequestPart,HttpServerlet(getCookies()方法,getAttribute方法,setAttribute方法,)HttpSession(getAttribute方法),@SessionAttribute
|
3月前
|
XML 存储 前端开发
手动开发-实现SpringMVC底层机制--小试牛刀
手动开发-实现SpringMVC底层机制--小试牛刀
26 0
|
JSON 前端开发 Java
SpringMVC之JSON返回及异常处理机制
SpringMVC之JSON返回及异常处理机制
69 0
|
8月前
SpringMVC异常处理机制
SpringMVC异常处理机制
48 0