我的架构梦:(八)手写SpringMVC框架

手写SpringMVC框架

    • 一、SpringMVC的请求处理流程
    • 二、Spring MVC 高级技术
    • 三、手写 SpringMVC 框架
    • 四、代码仓库

手写SpringMVC框架之前呢,我觉得有必要先了解SpringMVC的请求处理流程以及高级特性。

一、SpringMVC的请求处理流程

1、请求处理流程

我的架构梦:(八)手写SpringMVC框架_第1张图片

流程说明:

第一步:用户发送请求至前端控制器DispatcherServlet

第二步DispatcherServlet收到请求调用HandlerMapping处理器映射器。

第三步:处理器映射器根据请求Url找到具体的Handler(后端控制器),生成处理器对象及处理器拦截器(如果有则生成)一并返回DispatcherServlet

第四步DispatcherServlet调用HandlerAdapter处理器适配器去调用Handler

第五步:处理器适配器执行Handler

第六步:Handler执行完成给处理器适配器返回ModelAndView

第七步:处理器适配器向前端控制器返回 ModelAndViewModelAndViewSpringMVC 框架的一个 底层对 象,包括 ModelView

第八步:前端控制器请求视图解析器去进行视图解析,根据逻辑视图名来解析真正的视图。

第九步:视图解析器向前端控制器返回View

第十步:前端控制器进行视图渲染,就是将模型数据(在 ModelAndView 对象中)填充到 request 域。

第十一步:前端控制器向用户响应结果。

2、Spring MVC 九大组件

  • HandlerMapping(处理器映射器)

    HandlerMapping 是用来查找 Handler 的,也就是处理器,具体的表现形式可以是类,也可以是方法。比如,标注了@RequestMapping的每个方法都可以看成是一个HandlerHandler负责具体实际的请求处理,在请求到达后,HandlerMapping 的作用便是找到请求相应的处理器 HandlerInterceptor

  • HandlerAdapter(处理器适配器)

    HandlerAdapter 是一个适配器。因为 Spring MVCHandler 可以是任意形式的,只要能处理请求即可。但是把请求交给 Servlet 的时候,由于 Servlet 的方法结构都是 doService(HttpServletRequest req,HttpServletResponse resp)形式的,要让固定的 Servlet 处理方法调用 Handler 来进行处理,便是 HandlerAdapter 的职责。

  • HandlerExceptionResolver

    HandlerExceptionResolver 用于处理 Handler 产生的异常情况。它的作用是根据异常设置
    ModelAndView,之后交给渲染方法进行渲染,渲染方法会将 ModelAndView 渲染成⻚面。

  • ViewResolver

    ViewResolver即视图解析器,用于将String类型的视图名和Locale解析为View类型的视图,只有一 个resolveViewName()方法。从方法的定义可以看出,Controller层返回的String类型视图名 viewName 最终会在这里被解析成为ViewView是用来渲染⻚面的,也就是说,它会将程序返回的参数和数据填入模板中,生成html文件。ViewResolver 在这个过程主要完成两件事情: ViewResolver 找到渲染所用的模板(第一件大事)和所用的技术(第二件大事,其实也就是找到视图的类型,如JSP)并填入参数。默认情况下,Spring MVC会自动为我们配置一个 InternalResourceViewResolver,是针对 JSP 类型视图的。

  • RequestToViewNameTranslator

    RequestToViewNameTranslator 组件的作用是从请求中获取 ViewName,因为 ViewResolver 根据 ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName, 便要通过这个组件从请求中查找 ViewName

  • LocaleResolver

    ViewResolver 组件的 resolveViewName 方法需要两个参数,一个是视图名,一个是 LocaleLocaleResolver 用于从请求中解析出 Locale,比如中国 Localezh-CN,用来表示一个区域。这 个组件也是 i18n 的基础。

  • ThemeResolver

    ThemeResolver 组件是用来解析主题的。主题是样式、图片及它们所形成的显示效果的集合。 Spring MVC 中一套主题对应一个 properties文件,里面存放着与当前主题相关的所有资源,如图片、CSS样式等。创建主题非常简单,只需准备好资源,然后新建一个“主题名.properties”并将资源设置进去,放在classpath下,之后便可以在⻚面中使用了。SpringMVC中与主题相关的类有 ThemeResolverThemeSourceThemeThemeResolver负责从请求中解析出主题名, ThemeSource根据主题名找到具体的主题,其抽象也就是Theme,可以通过Theme来获取主题和具体的资源。

  • MultipartResolver

    MultipartResolver 用于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实现。MultipartHttpServletRequest 可以通过 getFile() 方法 直接获得文件。如果上传多个文件,还可以调用 getFileMap()方法得到Map这样的结构,MultipartResolver 的作用就是封装普通的请求,使其拥有文件上传的功能。

  • FlashMapManager

    FlashMap 用于重定向时的参数传递,比如在处理用户订单时候,为了避免重复提交,可以处理完 post请求之后重定向到一个get请求,这个get请求可以用来显示订单详情之类的信息。这样做虽然可以规避用户重新提交订单的问题,但是在这个⻚面上要显示订单的信息,这些数据从哪里来获得呢?因为重定向时么有传递参数这一功能的,如果不想把参数写进URL(不推荐),那么就可以通过FlashMap来传递。只需要在重定向之前将要传递的数据写入请求(可以通过
    ServletRequestAttributes.getRequest()方法获得)的属性OUTPUT_FLASH_MAP_ATTRIBUTE 中,这样在重定向之后的HandlerSpring就会自动将其设置到Model中,在显示订单信息的⻚面 上就可以直接从Model中获取数据。FlashMapManager 就是用来管理 FalshMap 的。

二、Spring MVC 高级技术

监听器、过滤器和拦截器对比

  • Servlet:处理Request请求和Response响应。

  • 过滤器(Filter):对Request请求起到过滤的作用,作用在Servlet之前,如果配置为/*可以对所有的资源访问(servlet、js/css静态资源等)进行过滤处理。

  • 监听器(Listener):实现了javax.servlet.ServletContextListener 接口的服务器端组件,它随 Web应用的启动而启动,只初始化一次,然后会一直运行监视,随Web应用的停止而销毁。

    作用一:做一些初始化工作,web应用中spring容器启动ContextLoaderListener
    作用二:监听web中的特定事件,比如HttpSession,ServletRequest的创建和销毁;变量的创建、 销毁和修改等。可以在某些动作前后增加处理,实现监控,比如统计在线人数,利用 HttpSessionLisener等。

  • 拦截器(Interceptor):是SpringMVC、Struts等表现层框架自己的,不会拦截 jsp/html/css/image的访问等,只会拦截访问的控制器方法(Handler)

    从配置的⻆度也能够总结发现:serlvet、filter、listener是配置在web.xml中的,而interceptor是配置在表现层框架自己的配置文件中的。

    Handler业务逻辑执行之前拦截一次
    Handler逻辑执行完毕但未跳转⻚面之前拦截一次
    在跳转⻚面之后拦截一次

关于它们更详细的区别,可以看下这篇博文: SpringBoot项目中自定义Filter过滤器、Listener监听器、Interceptor拦截器和Servlet容器

三、手写 SpringMVC 框架

好了,回顾完请求处理流程与一些高级特性后,我们开始来手写 SpringMVC 框架了。

我们来梳理下流程,为了更加清晰手写 SpringMVC 框架的思路,我画了下面这张图:

我的架构梦:(八)手写SpringMVC框架_第2张图片

1、自定义注解类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {

    String value() default "";

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {

    String value() default "";

}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {

    String value() default "";

}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

    String value() default "";

}

2、DispatcherServlet 最核心的类

public class DispatcherServlet extends HttpServlet {

    private Properties properties = new Properties();

    private List<String> classNames = Lists.newArrayList(); // 缓存扫描

    private Map<String, Object> ioc = Maps.newHashMap(); // ioc容器

    // handlerMapping
    // private Map handlerMapping = Maps.newHashMap(); // 存储url和method之间的映射关系
    private List<Handler> handlerMapping = Lists.newArrayList();

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

        // 2.扫描相关的类,扫描注解。
        doScan(properties.getProperty("scanPackage"));

        // 3.初始化bean对象(实现ioc容器,基于注解)
        doInstance();

        // 4.实现依赖注入
        doAutowired();

        // 5.构造一个HandlerMapping处理器映射器,将配置好的url和Method建立映射关系
        initHandlerMapping();

        System.out.println("riemann mvc init success...");

        // 6.等待请求进入,处理请求。
    }

    /**
     * 构造一个HandlerMapping处理器映射器
     * 最关键的步骤
     * 目的:将url和method建立关联
     */
    private void initHandlerMapping() {
        if (ioc.isEmpty()) return;
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            // 获取ioc容器中当前遍历的对象的class类型
            Class<?> clazz = entry.getValue().getClass();
            if (!clazz.isAnnotationPresent(Controller.class)) continue;

            String baseUrl = "";
            if (clazz.isAnnotationPresent(RequestMapping.class)) {
                RequestMapping annotation = clazz.getAnnotation(RequestMapping.class);
                baseUrl = annotation.value(); // 等同于 /riemann
            }

            // 获取方法
            Method[] methods = clazz.getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                // 方法没有标识RequestMapping,就不处理
                if (!method.isAnnotationPresent(RequestMapping.class)) continue;
                // 如果标识则处理
                RequestMapping annotation = method.getAnnotation(RequestMapping.class);
                String methodUrl = annotation.value(); // 等同于 /query
                String url = baseUrl + methodUrl; // 计算出来的url /riemann/query

                // 把method所有信息及url封装为一个Handler
                Handler handler = new Handler(entry.getValue(), method, Pattern.compile(url));

                // 计算方法的参数位置信息 // query(HttpServletRequest request, HttpServletResponse response, String name)
                Parameter[] parameters = method.getParameters();
                for (int j = 0; j < parameters.length; j++) {
                    Parameter parameter = parameters[j];
                    if (parameter.getType() == HttpServletRequest.class || parameter.getType() == HttpServletResponse.class) {
                        // 如果是request和response对象,那么参数名称写HttpServletRequest和HttpServletResponse
                        handler.getParamIndexMapping().put(parameter.getType().getSimpleName(), j);
                    } else {
                        handler.getParamIndexMapping().put(parameter.getName(), j); // 
                    }
                }

                // 建立url和method之间的映射关系(map缓存起来)
                // handlerMapping.put(url, method);

                handlerMapping.add(handler);
            }

        }
    }

    /**
     * 实现依赖注入
     */
    private void doAutowired() {
        if (ioc.isEmpty()) return;
        // 有对象,再进行依赖注入处理
        // 遍历ioc中所有对象,查看对象中的字段,是否有@Autowired注解,如果有需要维护依赖注入的关系
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            // 获取bean对象中的字段信息
            Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();
            // 遍历判断处理
            for (int i = 0; i < declaredFields.length; i++) {
                Field declaredField = declaredFields[i]; // @Autowired private RiemannService riemannService;
                if (!declaredField.isAnnotationPresent(Autowired.class)) continue;
                // 有该注解
                Autowired annotation = declaredField.getAnnotation(Autowired.class);
                String beanName = annotation.value(); // 需要注入的bean的id
                if ("".equals(beanName.trim())) {
                    // 没有配置具体的bean id,那就需要根据当前字段类型注入(接口注入)RiemannService
                    beanName = declaredField.getType().getName();
                }
                // 开启赋值
                declaredField.setAccessible(true);
                try {
                    declaredField.set(entry.getValue(), ioc.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * ioc容器
     * 基于className缓存的类的全限定类名,以及反射技术,完成对象创建和管理。
     */
    private void doInstance() {
        if (classNames.size() == 0) return;
        try {
            for (int i = 0; i < classNames.size(); i++) {
                String className = classNames.get(i); // com.riemann.controller.RiemannController
                // 反射
                Class<?> clazz = Class.forName(className);
                // 区分controller,区分service
                if (clazz.isAnnotationPresent(Controller.class)) {
                    // controller的id不做过多处理,不取value了,就拿类的首字母小写作为id,保存到ioc中
                    String simpleName = clazz.getSimpleName(); // RiemannController
                    String lowerLetterSimpleName = lowerLetterFirst(simpleName); // riemannController
                    Object o = clazz.newInstance();
                    ioc.put(lowerLetterSimpleName, o);
                } else if (clazz.isAnnotationPresent(Service.class)) {
                    Service annotation = clazz.getAnnotation(Service.class);
                    // 获取注解的值
                    String beanName = annotation.value();
                    // 如果指定了id,就以指定的为准
                    if (!"".equals(beanName.trim())) {
                        ioc.put(beanName, clazz.newInstance());
                    } else {
                        // 如果没有指定,就以类名首字母小写
                        beanName = lowerLetterFirst(clazz.getSimpleName());
                        ioc.put(beanName, clazz.newInstance());
                    }

                    // service层往往是有接口的,面向接口开发,此时再以接口名为id,放入一份对象到ioc容器中,便于后期根据接口类型注入
                    Class<?>[] interfaces = clazz.getInterfaces();
                    for (int j = 0; j < interfaces.length; j++) {
                        Class<?> anInterface = interfaces[j];
                        // 以接口的全限定类名作为id放入
                        ioc.put(anInterface.getName(), clazz.newInstance());
                    }

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

    public String lowerLetterFirst(String str) {
        char[] chars = str.toCharArray();
        if ('A' <= chars[0] && chars[0] <= 'Z') {
            chars[0] += 32;
        }
        return String.valueOf(chars);
    }

    /**
     * 扫描类
     * scanPackage:com.riemann ---> 磁盘上的文件夹(File) com/riemann
     * @param scanPackage
     */
    private void doScan(String scanPackage) {
        String scanPackagePath = Thread.currentThread().getContextClassLoader().getResource("").getPath() +
                scanPackage.replaceAll("\\.", "/");
        File packageName = new File(scanPackagePath);
        for (File file : packageName.listFiles()) {
            if (file.isDirectory()) { // 子package
                // 递归
                doScan(scanPackage + "." + file.getName()); // com.riemann.controller
            } else if (file.getName().endsWith(".class")) {
                String className = scanPackage + "." + file.getName().replaceAll(".class", "");
                classNames.add(className);
            }
        }
    }

    /**
     * 加载配置文件
     * @param contextConfigLocation
     */
    private void doLoadConfig(String contextConfigLocation) {
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try {
            properties.load(resourceAsStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @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 {
        // 处理请求,根据url,找到对应的Method方法,进行调用。
        // 获取uri
        // String requestURI = req.getRequestURI();
        // Method method = handlerMapping.get(requestURI); // 获取到一个反射的方法
        // 反射调用,需要传入对象,需要传入参数,此处无法完成调用,没有把对象缓存起来,也没有参数!!!需要改造initHandlerMapping()
        // method.invoke();

        // 根据uri获取到我们能够处理当前请求的handler(从handlerMapping中(List))
        Handler handler = getHandler(req);
        if (handler == null) {
            resp.getWriter().write("404 not found");
            return;
        }

        // 参数绑定
        // 获取所有参数类型数组,这个数组的长度就是我们最后要传入的args数组的长度
        Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();

        // 根据上述数组长度创建一个新的数组(参数数组,是要传入反射调用的)
        Object[] paramValues = new Object[parameterTypes.length];

        // 以下就是为了向参数数组中塞值,而且还得保证参数的顺序和方法中形参顺序一致
        Map<String, String[]> parameterMap = req.getParameterMap();

        // 遍历request中所有参数(填充除了request、response之外的)
        for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
            // name=1&name=2 name [1,2]
            String value = StringUtils.join(param.getValue(), ","); // 如同 1,2

            // 如果参数和方法中的参数匹配上了,填充数据。
            if (!handler.getParamIndexMapping().containsKey(param.getKey())) continue;

            // 方法形参确实有该参数,找到它的索引位置,对应的把参数值放入paramValues
            Integer index = handler.getParamIndexMapping().get(param.getKey()); // name在第2个位置

            paramValues[index] = value; // 把前台传递过来的参数值填充到对应的位置去
        }

        int requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName()); // 0
        paramValues[requestIndex] = req;

        int responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName()); // 1
        paramValues[responseIndex] = resp;


        // 最终调用handler的method属性
        try {
            handler.getMethod().invoke(handler.getController(), paramValues);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private Handler getHandler(HttpServletRequest req) {
        if (handlerMapping.isEmpty()) return null;
        String requestURI = req.getRequestURI();

        for (Handler handler : handlerMapping) {
            Matcher matcher = handler.getPattern().matcher(requestURI);
            if (!matcher.matches()) continue;
            return handler;
        }
        return null;
    }

}

3、pojo类Handler

/**
 * 封装handler方法相关的信息
 */
@Data
public class Handler {

    private Object controller; // method.invoke(obj,);

    private Method method;

    private Pattern pattern; // spring中url是支持正则的

    private Map<String, Integer> paramIndexMapping; // 参数的顺序,是为了进行参数绑定。key是参数名,value是第几个参数

    public Handler(Object controller, Method method, Pattern pattern) {
        this.controller = controller;
        this.method = method;
        this.pattern = pattern;
        paramIndexMapping = Maps.newHashMap();
    }

}

4、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>riemannmvc</servlet-name>
    <servlet-class>com.riemann.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>springmvc.properties</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>riemannmvc</servlet-name>
    <url-pattern>/*
  


5、RiemannController.java

@Controller
@RequestMapping("/riemann")
public class RiemannController {

    @Autowired
    private RiemannService riemannService;

    /**
     * URL: /riemann/query
     * @param request
     * @param response
     * @param name
     * @return
     */
    @RequestMapping("/query")
    public String query(HttpServletRequest request, HttpServletResponse response, String name) {
        return riemannService.get(name);
    }

}

6、测试结果

浏览器输入:http://localhost:8888/riemann/query?name=riemann

riemann mvc init success...
RiemannService 实现类中的name参数:riemann

ok,测试成功,这样就完成了手写SpringMVC框架的简易版了。

四、代码仓库

https://github.com/riemannChow/perseverance/tree/master/handwriting-framework/springmvc

你可能感兴趣的:(我的架构梦)