面试中又被问到SpringMVC了?不如自己写一个吧!

前言

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/58/

面试官:用过SpringMVC吗?

我:用过啊(内心OS:不就是Controller之类的吗)

面试官:那可以说一下当一个请求到来时,SpringMVC的处理流程吗?

我:……

真悲伤!

背一万遍概念不如自己亲手写一遍!这里,我带着大家实现一个简单的SpringMVC框架,帮助大家理解SpringMVC的处理流程,使得大家在面试中再遇到类似问题就可以侃侃而谈了。

这次实现的这个SpringMVC,依赖于我们上一篇文章(手撸一个Spring IOC容器——渐进式实现)中的Spring框架,如果还没有看过上篇文章的同学可以去学习一下。

本文项目的完整代码在Github上,地址:https://github.com/CN-GuoZiyang/My-Spring-IOC

SpringMVC原理

要实现我们自己的框架,就必须对原版框架的处理流程了解得清晰透彻,一张图总结:

640.jpg

  1. 用户发送请求至前端控制器DispatcherServlet。
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器。
  3. 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
  4. DispatcherServlet通过HandlerAdapter处理器适配器调用处理器。
  5. 执行处理器(Controller,也叫后端控制器)。
  6. Controller执行完成返回ModelAndView。
  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
  9. ViewReslover解析后返回具体View。
  10. DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)。
  11. DispatcherServlet响应用户。

框架实现

我们都知道,SpringMVC是基于Java的Servelt技术实现的,那么我们就需要导入Servlet的支持包,将这个项目改造成为一个Web项目。

在Maven中添加如下依赖:

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

并且在项目的根目录下建立一个web文件夹,再在web文件夹下建立WEB-INF文件夹,再在其中新建web.xml文件。这就是这个web项目的配置文件,即Servlet的配置文件。内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    <servlet>
        <servlet-name>MySpringMVC</servlet-name>
        <servlet-class>top.guoziyang.springframework.web.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>application.properties</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>MySpringMVC</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

这里和SpringMVC的处理流程一样,就是新建了一个类DispatcherServlet注册为Servlet,并且设置这个Servlet处理所有的URL请求。

在这里我们配置了一个参数contextConfigLocation,参数的值为application.properties。这个文件作为我们的SpringMVC的配置文件,我们的SpringMVC并不需要太多配置,只需要知道Controller的扫描路径就可以了。在resources文件夹下新建这个文件,里面我只写了一行:

scanPackage=top.guoziyang.main.controller

表示我的所有的Controller都会放在top.guoziyang.main.controller包及其子包下,到时候启动时SpringMVC会去扫描这个包。

接着我们去定义三个注解:@Controller@RequestMapping@RequestParam,用过SpringMVC的人应该都知道这三个注解是干嘛的,我就不多说了。

package top.guoziyang.springframework.annotation;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {}
package top.guoziyang.springframework.annotation;

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

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String value() default "";
}
package top.guoziyang.springframework.annotation;

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

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
    String value();
}

接着我们就需要实现在配置文件里写的DispatcherServlet类,这个类需要继承HttpServlet类,才是一个可被使用的Servlet。

这个类需要重写父类中的三个主要的方法,init()doGet()doPost()方法。

init方法如下:

@Override
public void init(ServletConfig config) {
    try {
        xmlApplicationContext = new ClassPathXmlApplicationContext("application-annotation.xml");
    } catch (Exception e) {
        e.printStackTrace();
    }
    doLoadConfig(config.getInitParameter("contextConfigLocation"));
    doScanner(properties.getProperty("scanPackage"));
    doInstance();
    initHandlerMapping();
}

注意这里首先初始化了一个Spring容器。

init方法主要的功能就是读取配置文件,接着扫描目标包下所有的Controller,最后实例化所有的Controller,并且绑定URL路由。对应上面的8、9、10和11行。其中第八行和第九行是把包中所有的类都扫描出来后,存储在classNames这个List里。

doInstance()的实现很简单,如下:

private void doInstance() {
    if (classNames.isEmpty()) {
        return;
    }
    for (String className : classNames) {
        try {
            //把类搞出来,反射来实例化(只有加@Controller需要实例化)
            Class clazz = Class.forName(className);
            if (clazz.isAnnotationPresent(Controller.class)) {
                classes.add(clazz);
                BeanDefinition definition = new BeanDefinition();
                definition.setSingleton(true);
                definition.setBeanClassName(clazz.getName());
                xmlApplicationContext.addNewBeanDefinition(clazz.getName(), definition);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    try {
        xmlApplicationContext.refreshBeanFactory();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

主要就是把上一步中包下的所有类遍历一下,找到加上了Controller注解的类,添加到Spring容器里就行了。

这里有人就会问了,唉我Spring容器已经初始化完成了,怎么还能往里添加Bean呢?原理很简单,我们手动刷新下不就行了。这里给XmlApplicationContext类添加了一个refreshBeanFactory()方法,手动刷新Bean的配置,如果遇到没有初始化的(刚添加进去的)就会初始化。方法实现非常简单:

public void refreshBeanFactory() throws Exception {
    prepareBeanFactory((AbstractBeanFactory) beanFactory);
}

注意这里我们还把符合条件的类(Controller)放在了classes里,这是一个HashSet,后续在绑定URL的时候要用。

在initHandlerMapping()方法中,我们将扫描对应的Controller,找出某个URL应当由哪个类的哪个方法进行处理。如下:

private void initHandlerMapping() {
    if (classes.isEmpty()) return;
    try {
        for (Class<?> clazz : classes) {
            String baseUrl = "";
            if (clazz.isAnnotationPresent(RequestMapping.class)) {
                baseUrl = clazz.getAnnotation(RequestMapping.class).value();
            }
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                if (!method.isAnnotationPresent(RequestMapping.class)) continue;
                String url = method.getAnnotation(RequestMapping.class).value();
                url = (baseUrl + "/" + url).replaceAll("/+", "/");
                handlerMapping.put(url, method);
                controllerMap.put(url, xmlApplicationContext.getBean(clazz));
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

由于我们已经把符合条件的Controller都放在了classes中,只要遍历这个Set就行了。对每个类遍历方法,获取RequestMapping这个注解的值,并且拼接出完整的URL,将URL与方法的映射存储在handlerMapping这个map中,将URL与类的映射存储在controllerMap中。

那么最终,一个请求到来时,是到达doGet()和doPost()方法的。我们自己实现一个doDispatch()方法来进行自定义处理。

doDispatch()方法首先需要分离出请求的URL和请求参数,找到对应的方法后通过反射调用。如下:

    public void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (handlerMapping.isEmpty()) return;
        String url = request.getRequestURI();
        String contextPath = request.getContextPath();
        url = url.replace(contextPath, "").replaceAll("/+", "/");
        if (!handlerMapping.containsKey(url)) {
            response.getWriter().write("404 NOT FOUND!");
            return;
        }
        Method method = handlerMapping.get(url);
        Class<?>[] parameterTypes = method.getParameterTypes();
        Map<String, String[]> parameterMap = request.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] = request;
                continue;
            }
            if (requestParam.equals("HttpServletResponse")) {
                paramValues[i] = response;
                continue;
            }
            if (requestParam.equals("String")) {
                for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
                    String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "").replaceAll(",\\s", ",");
                    paramValues[i] = value;
                }
            }
        }
        method.invoke(controllerMap.get(url), paramValues);
    }

反射调用方法传参的方式,是通过一个Object数组的方式传入参数的,按照方法定义参数的顺序,将值存放在数组中,在反射调用时将数组传入即可。

测试

这里我们写了一个Controller:

@Controller
@RequestMapping("/test")
public class TestController {

    @Autowired
    private HelloWorldService helloWorldService;

    @RequestMapping("/test1")
    public void test1(HttpServletRequest request, HttpServletResponse response,
                      @RequestParam("param") String param) {
        try {
            String text = helloWorldService.getString();
            response.getWriter().write(text + " and the param is " + param);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

这里我们同时还注入了一个对象,HelloWorldService,来看一看和Spring的耦合是否成功。这个test1方法还需要传入一个参数param,用于测试传参。

当把项目通过Tomcat启动在8080端口后,访问http://localhost:8080/test1?param=abc,出现如下结果:

Hello world and the param is abc

成功!

猜你喜欢

转载自blog.csdn.net/qq_40856284/article/details/106622595