【快递柜项目】手写MVC框架

为什么要写MVC框架?

先看看,没有MVC框架时,是怎么开发的?

当开发一个项目时,面对项目的多个功能模块,可能要 写多个  xxxServlet

例如:

开发注册功能registerServlet
开发 登录功能loginServlet
开发用户信息获取userInfoServlet

这样需要写的类太多了,非常繁琐,且不好维护。

因此,我们可以写出一个 dispatcherServlet  类,这个“分发类”中注册了(包含了)所有的功能方法。前端不同的请求都去找“分发类”,由它进行分发请求和响应,这样类比较少,便于优化。

怎么写MVC框架?

仿SpringMVC的运行原理(含十多个步骤),便于提前建立Spring MVC框架的认知。所以只写关键的部分,其余弱化 。

 1.所有请求 为“/*.do”的请求都去找 DispatcherServlet这个类

2.在这个servlet中有相应的初始化方法init(),其中包含如下逻辑

2.1.1

a.加载配置文件

b.建立映射地址的池

即说明  xxx.do和 yyy.do 分别对应的是哪个文件的哪个方法,建立一种映射关系

servlet ——>映射器 ——>调用方法——>将结果返回给用户

映射器专门用于映射用户请求的网址,和具体对应的方法,通过映射器,根据用户输入的网址,找到要调用的方法,然后把方法调用结果返回给用户,或者跳转视图。

 2.1.2具体做法

在web.xml中配置映射关系

<servlet>
    <servlet-name>⾃定义名称</servlet-name>
    <servlet-class>处理请求的类的完整路径</servlet-class>
</servlet>
<servlet-mapping><!-- mapping 表示映射 -->
    <servlet-name>⾃定义名称</servlet-name>
    <url-pattern>请求名</url-pattern>
</servlet-mapping>

标签执行顺序:

请求到达——web.xml——servlet-mapping标签中的<url-pattern>标签中的内容和请求名进行匹配——匹配成功以后找到对应的<servlet-mapping> 标签 中的二级标签<servlet-name>的servlet名称,去找上一个一级标签<servlet>中和<servlet-name>相匹配的name值——去找servlet标签中 的 servlet-class中处理类的完整路径

 举例:

<servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>com.mayday.mvc.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contentConfigLocation</param-name>
            <param-value>application.properties</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>*.do</url-pattern>
</servlet-mapping>

当有 任何请求(*.do)过来,都会先解析这个web.xml文件,然后先在servlet-mapping中,根据<url-pattern>和<servlet-name>的映射关系,找到 当前请求 应该 到达的 servlet的名字——即<servlet-name>中的servlet名字。然后去<servlet> 容器中根据名字进行匹配,进一步找到该servlet类所在的项目路径,然后加载初始化参数、配置文件,然后执行。

令 DispatcherServlet 类 继承 Java 的 HttpServlet 方法,并重写 doPost(), doGet(),init()获取配置文件中的每一个用于处理请求的类,每一个类中可能含有0-n个用于处理请求的方法

然后通过类加载器的  init()  

 @Override
    public void init(ServletConfig config) throws ServletException {
        //获取 web.xml 配置文件中的 初始化配置文件
        String path = config.getInitParameter("contentConfigLocation");
        InputStream is = DispatcherServlet.class
                         .getClassLoader().getResourceAsStream(path);
        HandlerMapping.load(is);
    }

注意到:class.getClassLoader().getResourceAsStream(path)得到一个输入流 is

然后生成一个配置文件的对象, 把 application.properties 中描述的所有的类 都加载到 ppt 这个 Map集合中,输入流

Properties ppt = new Properties();

ppt.load(is)                //加载输入流,  那么 application.properties中所有的类都被加载到ppt这个Map集合中

 加载到了这些类以后,通过反射技术去读取  application配置文件 里描述的所有 注解的反射情况

因此 在这之前需要先编写注解

@ResponseBody表示此注解下的方法 应该 返回文字内容给请求者

package com.mayday.mvc;

import java.lang.annotation.*;

@Target(ElementType.METHOD)     //元注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
 * 注解的作用:
 *  被此注解添加的方法, 会被用于处理请求。
 *  方法返回的内容,会以文字形式返回到客户端
 */
public @interface ResponseBody {
    String value();
}
以下 三个都是 元注解:
@Target 作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

@Retention  作用:定义被它所注解的注解保留多久

@Documented   作用:只是用来标注生成  javadoc 的时候是否会被记录

 定义 注解  @ResponseView  返回跳转的页面给客户端,

package com.mayday.mvc;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
 * 注解的作用:
 *  被此注解添加的方法, 会被用于处理请求。
 *  方法返回的内容,会直接重定向
 */
public @interface ResponseView {
    String value();
}

在使用这两个注解之前,先写一个枚举类,用于描述 响应是 文字 还是 视图:

package com.mayday.mvc;

public enum ResponseType {
    TEXT,VIEW;
}

2.2映射器

通过映射器去加载相应的关联的方法

在HandlerMapping中包含 N 个 MVCMapping,存放在 Map类型的集合中,用户每次发起请求,就去HandlerMapping下的 Map集合找,匹配 应该用哪个方法来做处理

在 DispatcherServlet 的 init()  初始化加载(load)时,读取配置文件输入流,把配置文件所指向的类里面 (所有用了 这两个注解的类)封装成 Map集合对象。

先写一个  MVCMapping 类 ,这个类包含的属性有对象 Object ,和方法 Method, 枚举类型 ResponseType

package com.mayday.mvc;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 处理程序映射器(包含了大量的网址与方法的对应关系) 即:一个方法用于处理一个 网址的请求
 */
public class HandlerMapping {
    
    /**
     * 映射对象,每一个对象封装了一个方法,用于处理请求
     */
    public static class MVCMapping{
        private Object obj;
        private Method method;
        private ResponseType type;

        public MVCMapping() {
        }

        public MVCMapping(Object obj, Method method, ResponseType type) {
            this.obj = obj;
            this.method = method;
            this.type = type;
        }

       /**..省去 get / set方法..**/
    }
}

下面就写加载 输入流 中的类的 load(),其作用是把添加了 @ResponseView和@ResponseBody的方法 封装到 Map集合中

package com.mayday.mvc;


/**
 * 处理程序映射器(包含了大量的网址与方法的对应关系) 即:一个方法用于处理一个 网址的请求
 */
public class HandlerMapping {
    private static Map<String,MVCMapping> data = new HashMap<>();
    public static MVCMapping get(String uri){
        return data.get(uri);
    }   //这里用到的uri是什么意思?uri:统一资源标识符,用于标识某一互联网资源名称的字符串


    public static void load(InputStream is ){    //在DispatcherServlet类中已获取了输入流
        //加载配置文件
        Properties ppt = new Properties();
        try {
            ppt.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //获取配置文件中描述的一个个的类
        Collection<Object> values = ppt.values();   //取出properties中所有的值(类)
        for (Object cla:values) {
            String className = (String) cla;    //拿到对象类的名称后,下面需要对其进行反射的处理,
//通过反射创建对象,进而获取对象类中的方法
//反射:1.动态获取 类的信息(属性,方法),2.动态调用 对象的 方法、属性
            try {
                //加载配置文件中描述的每一个类
                Class c = Class.forName(className);//加载完毕后通过反射加载这个类
                //创建这个类的对象
                Object obj = c.getConstructor().newInstance();
                //获取这个类的所有方法
                Method[] methods = c.getMethods();
                for (Method m:methods) {
                    Annotation[] as = m.getAnnotations();
                    if(as != null){
                        for(Annotation annotation:as){
                            if(annotation instanceof ResponseBody){
                                //说明此方法,用于返回字符串给客户端
                                MVCMapping mapping = new MVCMapping(obj,m,ResponseType.TEXT);
                                Object o = data.put(((ResponseBody) annotation).value(),mapping);
                                if(o != null){
                                    //存在了重复的请求地址
                                    throw new RuntimeException("请求地址重复:"+((ResponseBody) annotation).value());
                                }
                            }else if(annotation instanceof ResponseView){
                                //说明此方法,用于返回界面给客户端
                                MVCMapping mapping = new MVCMapping(obj,m,ResponseType.VIEW);
                                Object o =  data.put(((ResponseView) annotation).value(),mapping);
                                if(o != null){
                                    //存在了重复的请求地址
                                    throw new RuntimeException("请求地址重复:"+((ResponseView) annotation).value());
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

  
}

当服务器启动时, DispatcherServlet 就被加载了, 就会在 init() 中通过 HandlerMapping的 load()方法 加载配置文件application.properties  (注意,需要使用类加载器,将配置文件转化为 输入流 才能进行文件间的交流 )。在load()中,我们读取了这个application.properties配置文件的输入流InputStream is,然后 将输入流保存到配置文件对象中【ppt.load()】,并拿到配置文件中描述的一个个的类,并且遍历,对每一个类使用反射技术 加载 类的信息,创建类对象,获取类的方法,获取方法的注解信息,根据注解类型判断返回 文字还是视图

上面提到,我们自定义的两个注解中, @ResponseBody含有 值: String value();

 我们需要获取到这个注解的 value值 ,作为 HashMap的 key, 而 MVCMapping对象的 mapping 作为HashMap的 value

data.put(((ResponseBody) annotation).value(), mapping)

 为什么要封装在HashMap中?

是先通过提前初始化 并记录所有的注解和方法的对应关系,(记录到HandlerMapping里面的HashMap)这样便于后续,响应 处理客户端的请求。(否则服务器会处理不了路径请求,报404)

以上是通过 init() 中调用 load() 获取了所有类的方法映射关系,

下面需要写一个方法,对这些映射关系进行匹配(使用uri匹配 )获取(调用)相关方法

 public static MVCMapping get(String uri){
        return data.get(uri);
    } 

到这里,我们的MVC框架就快写完了

当用户发出请求, doGet(), doPost() , 为什么能执行,是因为, 当用户请求时,其实Tomcat 主动调用的是Service(),

而Service ()中代码第一行就是获取请求的方式,如果请求是 “GET”,就在Service()中调用 doGet()

如果请求是 "POST", 就在Service()调用 doPost() ,所以这两个方法可直接在 Service()中进行处理:

package com.mayday.mvc;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;

public class DispatcherServlet extends javax.servlet.http.HttpServlet {
    @Override
    public void init(ServletConfig config) throws ServletException {
        //获取 web.xml 配置文件中的 初始化配置文件
        String path = config.getInitParameter("contentConfigLocation");
        InputStream is = DispatcherServlet.class.getClassLoader().getResourceAsStream(path);
        HandlerMapping.load(is);
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.    获取用户请求的uri   /xx.do
        String uri = req.getRequestURI();
        HandlerMapping.MVCMapping mapping = HandlerMapping.get(uri);
        if( mapping == null){
            resp.sendError(404,"MVC:映射地址不存在:"+uri);
            return;
        }
        Object obj = mapping.getObj();
 //反射技术中的  Method类,可通过 Method调用对象的方法:Method.invoke(Object obj1, Object obj2..)
        Method method = mapping.getMethod();   
        Object result = null;
        //相当于SpringMVC中的 Handler
        try {
            //使用反射技术 动态调用 指定obj对象的 方法: mothod.invoke()
            result = method.invoke(obj, req, resp);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        //判断Mapping的类型是什么,如果是 对应类型,就向前台回复(写入)处理的结果
        //相当于 SpringMVC的HandlerAdapter
        switch (mapping.getType()){
            case TEXT:
                resp.getWriter().write((String)result);
                break;
            case VIEW:
                resp.sendRedirect((String)result);
                break;
        }
    }

}

下面再来梳理一下逻辑:

当服务器启动时,DispatcherServlet就会加载 application.properties 配置文件,通过反射技术,根据配置文件中的类的全限定名,找到所有类下的方法,并存储到HashMap中。其中HashMap中的 Key是通过反射技术得到的方法名上 注解的 url 值。value是 反射技术得到的 ——由 Object,和 Method, 枚举类ResponseType组成的——封装对象 MVCMapping。

当用户请求DispatcherServlet的时候,就会通过  req.getRequestURI()  先获取用户请求的 URI。

然后拿着这个URI去 HandlerMapping 的HashMap去找有无对应的可用来处理这个请求的方法

如果没有对应方法,则报404,映射地址不存在。

如果有对应方法,则去调用相应方法。

注意:方法一定是要加入了注解才会在HashMap中的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值