模仿 Struts2 框架自己写一个 MVC 框架来深入理解 MVC

一、什么是MVC?

1. MVC 概念

MVC 即是Model View Controller 的缩写,Model 即模型,View 即视图,Controller 即控制器。

MVC 是一种非常流行的软件设计模式,把代码分根据功能为视图、模型、控制器三个部分。

M : Model 模型,主要用于业务处理逻辑及数据存取,表示数据的状态。

V: View 视图,主要用于展现数据(列表数据,详细数据等),收集数据(注册,调查报告等)。体现在和用户的交互界面。

C:Controller,主要用于接收客户端请求及根据请求调用响应的Model。并根据 Model 执行的结果来跳转到不同的视图。

2.MVC 作用

2.1 解耦合

通过控制层把视图层和业务层分离,使业务层代码和视图层代码分离,有利于扩展和维护。

2.2 有利于分工开发和管理

分为三层,这样有利于前后端分离,前端开发人员只专注视图层,后端开发人员专注业务逻辑及数据处理。各司其职,互不干涉。

2.3 重用性

可以提高代码可重用性,控制层 可以根据需要可以重复的调用 Model 来完成数据加工处理。

3.MVC 框架

目前 Java 方向比较流行的 MVC 框架主要是 SpringMVC 、Struts2 等

Struts2 目前已经有点过时,用的越来越少了

但是 Struts2 框架曾经是非常经典的流行的 MVC 框架

二、自定义 MVC 框架意义?

​ 本 Chat 主要目的,不是解释什么 MVC ,而是要带着大家一起来写一个仿照 Struts2 框架的山寨版的自定义MVC 框架。

​ 主要目的是通过自己的手写一个 MVC 框架,来加深对 Struts2 框架及 SpringMVC 框架的理解。这样有助于我们更灵活的使用框架。也有助于在面试当中被问及框架执行流程及原理时,能尽可能的回答完整其原理。

​ 当然在一定程度上,也有助于你站在架构师的角度去理解框架的实现思路。

​ 接下来我们先来了解下Struts2的原理图(来自 Struts2 官网):

​编辑

对于官网的原理图,很多人看上去就蒙了。我用下图来解释下:

​编辑

从上图中可以看到 Struts2 的执行过程:

1.客户端发出请求,经过一系列的滤器链。完成 request 数据到 上下文中数据映射及属性对应。保证了一系列的Filter访问正确的ActionContext。

2.调用 ActionMapper 来判定是否为正确的 Struts 的 Action 请求。

3.如果是 Struts 请求,则创建 ActionProxy 代理。

4.并根据之前加载的 struts.xml 来确定来调用哪一个Action及对应的拦截器。

5.调用 拦截器栈,拦截器完成了参数到Action的封装、国际化等功能。

6.最后一个拦截器调用 Action 。这里其实是一种代理模式。通过拦截器实现了功能增强,在执行Action前做一些其他的处理,比如:计时统计,权限验证等。拦截器最后调用对应的 Action。

7.根据 Action 结果调用对应 Result 结果视图。结果视图确定了逻辑名和物理路径映射关系及跳转方式等

8.跳转到视图进行视图渲染。说白了就是执行 JSP 或其他的页面组件进行显示结果数据。

9.再执行拦截器中剩下的部分代码。就是像又从拦截器栈中逐一相反方向退出拦截器栈。

10.准备响应结果。

11.再返回到开始经过的一系列过滤器链。

12.把结果页面响应到客户端显示。

上面的概念有些还是比较抽象,但是我们还得返回到我们今天的主要话题:自己写一个框架来理解框架!

为了方便理解,我们对 Struts 的自定义做出了简化。但是基本能达到 Struts2 框架的效果。

三、自己框架的结构图

​编辑

接下来我们按照请求的依次调用流程来逐一介绍每个类的执行时机及作用。

1.核心控制器类

StrutsPrepareAndExecuteFilter

本类为一个过滤器组件 Filter,主要用来拦截 *.action 的 Struts 请求。首先定义类及对类在web.xml中进行配置。

先来编写核心控制器 StrutsPrepareAndExecuteFilter 代码如下:

java 复制代码
package com.framework;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 核心控制器
 * 拦截所有的*.action请求
 */
@WebFilter("*.action")//如果使用的web3.0则可以省略web.xml配置,在此处使用注解来完成映射
public class StrutsPrepareAndExecuteFilter implements Filter {
    //参数拦截器,这是只是模拟了一个拦截器,在struts中实际是多个拦截器的栈列表,包括自定义拦截器
    private ParameterInterceptor paramInterceptor;

    private ActionMapper actionMapper;
    @Override
    public void init(FilterConfig arg0) throws ServletException {
        paramInterceptor = new ParameterInterceptor();
        //加载配置文件 并获取配置文件信息
        actionMapper = new ActionMapper();
        System.out.println("%%%%%%%%%%%%启动 加载完成!%%%%%%%%%%");
    }
    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse resp = (HttpServletResponse) response;

        //根据请求获取action映射信息
        ActionMapping actionMapping = actionMapper.getMapping(req);

        try {
            DefaultActionInvocation invocation = new DefaultActionInvocation(actionMapping, req, resp);
            //通过拦截调用action 这里只模拟了参数拦截器
            String resultname = paramInterceptor.intercept(invocation);
                //根据action执行后返回逻辑名来获取Result视图对象
            Result result = actionMapping.getResultMap().get(resultname);
            //逻辑名换为了真是物理路径s
            String goUrl = result.getPath();
            //取出跳转的类型 转发或重定向
            String type = result.getType();
            if(type==null || type.equals(Result.TYPE_DISPATHER)){
                request.getRequestDispatcher(goUrl).forward(request, response);
            }else{
                resp.sendRedirect(goUrl);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

如果使用 web3.0以上版本,Tomcat7 及以上版本,则可以省略下面的 web.xml配置。否则需要如下配置:

xml 复制代码
  <filter>
    <filter-name>struts2</filter-name>
    <filter-class>com.framework.StrutsPrepareAndExecuteFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>*.action</url-pattern>
  </filter-mapping>

2.ActionMapper

在上面的核心控制器 StrutsPrepareAndExecuteFilter 中可以看到在初始化 init() 方法中调用了实例化了 ActionMapper 类对象。而 init() 方法是在服务器启动时就会执行的方法。那这里到底做了什么事情?

先来看下 ActionMapper 中都做干了什么?

( * 注意:Struts2 框架中真正加载配置文件的是ConfigurationManager类来完成的,这里简化放到这里 *)

ini 复制代码
package com.framework;

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

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
/**
 * 根据请求的URL从加载的配置文件中获取action的映射信息
 *
 */
public class ActionMapper {
    private static Map<String, ActionMapping> actionMap;

    private static String defaultName = "/struts.xml";
    /**
     * static块 只加载一次
     * 加载配置文件,为了简化,这里省略了ConfigurationManager类的加载。直接在此完成了Conf类的任务
     */
    static{        
        try {
            actionMap = new HashMap<String, ActionMapping>();
            SAXReader reader = new SAXReader();
            InputStream is = ActionMapper.class.getResourceAsStream(defaultName);

            Document doc = reader.read(is);
            //获取根节点mystruts节点
            Element root = doc.getRootElement();
            //获取package节点
            Element pack = root.element("package");
            List<Element> actionList = pack.elements("action");

            for (Element element : actionList) {
                String name = element.attributeValue("name");
                String className = element.attributeValue("class");
                String method = element.attributeValue("method");

                //取出action 下的result 孩子节点
                List<Element> resultList = element.elements("result");


                //实例化ActionMapping
                ActionMapping actionBean = new ActionMapping();
                actionBean.setName(name);
                actionBean.setClassName(className);
                actionBean.setMethod(method);

                //把resultList 转为Map
                Map<String, Result> resultMap = new HashMap<String, Result>(); 
                for (Element element2 : resultList) {
                    String resultName = element2.attributeValue("name");
                    String path = element2.attributeValue("path");
                    String type = element2.attributeValue("type");

                    //实例化Result
                    Result result = new Result();
                    result.setName(resultName);
                    result.setPath(path);
                    result.setType(type);

                    //放入resultMap
                    resultMap.put(resultName, result);
                }

                actionBean.setResultMap(resultMap);

                //把actionBean 放入actionMap
                actionMap.put(name, actionBean);
            }

            System.out.println("***********加载完成!*******"+actionMap);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据请求获取对应的Action对象
     * @param request
     * @param configManager
     * @return
     */
    public ActionMapping getMapping(javax.servlet.http.HttpServletRequest request){
        String actionName = this.getActionName(request);
        return actionMap.get(actionName);
    }
    /**
     * 根据URL来截取出action的name
     * @param request
     * @return
     */
    private String getActionName(javax.servlet.http.HttpServletRequest request){
        String url = request.getRequestURI();
        String actionName = url.substring(url.lastIndexOf("/")+1, url.lastIndexOf("."));
        return actionName;
    }
}

上面可以看到,上面类中就一个方法是公共的可以被外界调用的getMapping(javax.servlet.http.HttpServletRequest request) 方法,它主要功能是通过request 请求获取 URL,t通过 URL 来获取对应的 .action 请求,来获取要调用的 ActionMapping (下面介绍此类作用)。

类中在静态块中加载了 Struts.xml 文件并进行了解析,把解析出的映射关系放入了 Map 对象。

Map<String, ActionMapping> actionMap。

struts.xml 配置如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<mystruts>
    <package>
        <action name="add" class="com.action.CalAction" method="add">
            <result name="success" path="result.jsp"></result>
            <result name="error" path="error.jsp" type="redirect"></result>
        </action>
    </package>
</mystruts>

加载上面配置文件后,会把 action 中的 name、class、method 属性 及 下面所属的 result 节点相关属性提取,分别封装到 ActionMapping 及 Result 两个 Bean 中,用来保存映射信息在内存。

Result.java 如下:

typescript 复制代码
package com.framework;

public class Result {
    public static String TYPE_DISPATHER = "dispatcher";
    public static String TYPE_REDIRECT = "redirect";
    private String name;
    private String path;
    private String type;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPath() {
        return path;
    }
    public void setPath(String path) {
        this.path = path;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    @Override
    public String toString() {
        return "Result [name=" + name + ", path=" + path + ", type=" + type + "]";
    }

}

ActionMapping.java 代码如下:

typescript 复制代码
package com.framework;

import java.util.HashMap;
import java.util.Map;

/**
 * 本类用来存储从struts.xml解析出来的配置信息
 * 
 */
public class ActionMapping {

    private String name; // Action的name
    private String className;
    private String method;
    private Map<String, Result> resultMap = new HashMap<String, Result>();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public Map<String, Result> getResultMap() {
        return resultMap;
    }

    public void setResultMap(Map<String, Result> resultMap) {
        this.resultMap = resultMap;
    }

    public String getMethod() {
        return method;
    }

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

    @Override
    public String toString() {
        return "ActionMapping [name=" + name + ", className=" + className
                + ", method=" + method + ", resultMap=" + resultMap + "]";
    }

}

通过上面的加载及封装,就完成了 Action 映射信息的读取,做好了后面根据 url 创建及调用 Action 的准备。

3.参数拦截器

ParameterInterceptor 为我们自定义的一个参数拦截器,主要作用是用来完成客户端页面提交过来的参数到 Action 中的属性自动封装 及 常用类型的转换。这里需要 Action 中提供和页面表单元素名相对应的属性。这样此拦截器即可自动完成 参数到 Action 的封装。

在核心控制器 StrutsPrepareAndExecuteFilter 的初始化方法 init() 方法中,还有一行代码:

paramInterceptor = new ParameterInterceptor();

csharp 复制代码
    public void init(FilterConfig arg0) throws ServletException {
        paramInterceptor = new ParameterInterceptor();
        //加载配置文件 并获取配置文件信息
        actionMapper = new ActionMapper();
        System.out.println("%%%%%%%%%%%%启动 加载完成!%%%%%%%%%%");
    }

例如页面有如下表单:

xml 复制代码
    <form action="add.action" method="post">
        <p>
        第一个数:<input name="num1">
        </p>
        <p>
        第二个数:<input name="num2">
        </p>
        <input type="submit" value="计算">
    </form>

上面需要请求的 add.action 对应的 AddAction 也需要定义两个属性 num1 和 num2 来接收表单提交的参数:

csharp 复制代码
package com.action;

public class CalAction {
    private int num1;
    private int num2;
    private int result;
    /**
     * add.action请求
     */
    public String add(){
        System.out.println(num1+"============="+num2);
        this.result = num1+num2;
        return "success";
    }
    public int getNum1() {
        return num1;
    }
    public void setNum1(int num1) {
        this.num1 = num1;
    }
    public int getNum2() {
        return num2;
    }
    public void setNum2(int num2) {
        this.num2 = num2;
    }
    public int getResult() {
        return result;
    }
    public void setResult(int result) {
        this.result = result;
    }

}

表单提交的数据到服务器端,通过 request 得到的都为 String 字符串类型,并且也需要逐一提取并 set 到 Action 对象,set 之前还需要类型转换 String -> int 。这些工作全部由参数拦截器来完成!

参数拦截器 实现的接口 Interceptor.java 及 ParameterInterceptor.java 代码如下:

java 复制代码
package com.framework;
public interface Interceptor { 
    public String intercept(DefaultActionInvocation invocation) throws Exception;
}
scss 复制代码
package com.framework;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import javax.servlet.http.HttpServletRequest;

/**
 * 参数拦截器:实现参数到Action对象参数的封装
 * 实际的Struts2框架中,可以有自定义拦截器及内置的拦截器或拦截器栈。
 * 本案例为了简化流程,省略了真实拦截action并调用action的过程。
 *
 */
public class ParameterInterceptor implements Interceptor {

    /**
     * 请求中参数自动封装到action
     * @param action
     * @param request
     */
    private void requestToAction(Object action,HttpServletRequest request){
        //取出Action对象中所有属性
                Field[] field = action.getClass().getDeclaredFields();
                for (Field field2 : field) {
                    //属性名
                    String name = field2.getName();
                    //属性类型
                    Class<?> type = field2.getType();

                    //从request中获取参数
                    String val = request.getParameter(name);

                    if(val==null){
                        continue;
                    }

                    //获取对应属性的setter方法
                    String methodName = "set"+name.substring(0, 1).toUpperCase()+name.substring(1);
                    try {
                        Method method = action.getClass().getMethod(methodName, type);//使用属性类型

                        if(type==int.class || type==Integer.class){
                            //执行方法
                            method.invoke(action, Integer.parseInt(val));
                        }else if(type==float.class || type==Float.class){
                            //执行方法
                            method.invoke(action, Float.parseFloat(val));
                        }else if(type==double.class || type==Double.class){
                            //执行方法
                            method.invoke(action, Double.parseDouble(val));
                        }else{
                            method.invoke(action, val);
                        }


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

    /**
     * 把执行action后的action中的属性,放入到request中转发给视图
     * @param action
     * @param request
     */
    private void actionToRequest(Object action,HttpServletRequest request){
        Field[] fields = action.getClass().getDeclaredFields();
        for (Field field : fields) {
            String name = field.getName();

            //getter方法
            String methodName = "get"+name.substring(0, 1).toUpperCase()+name.substring(1); ///getNum1();
            try{
                Method method = action.getClass().getMethod(methodName);
                Object val = method.invoke(action);

                //放入request
                request.setAttribute(name, val);
            }catch(Exception e) {
                //e.printStackTrace();
            }
        }
    }

    /**
     * 拦截方法
     * 类似代理模式
     * DefaultActionInvocation包含了action对象的引用,
     * 所以可以在执行前做参数,执行后再放入request,类似Filter,起到了拦截增强功能的作用
     */
    @Override
    public String intercept(DefaultActionInvocation invocation)
            throws Exception {
        //获取action对象引用
        Object action = invocation.getAction();
        //调用action前先把参数封装到action对象中
        this.requestToAction(action, invocation.getRequest());

        //调用action 
        String result = invocation.invoke();

        //把执行后结果放入request中
        this.actionToRequest(action, invocation.getRequest());
        return result;
    }
}

参数拦截器中主要有三个方法:

requestToAction(Object action,HttpServletRequest request) :

用来完成提交的请求 request 参数封装到 action 对象的工作。

actionToRequest(Object action,HttpServletRequest request):

用来在 action 方法被调用后,把 action 中更新后的属性 再放入 request 请求,从而转发带到视图页面中显示。

intercept(DefaultActionInvocation invocation):

此方法为拦截器中核心方法,用来通过代理模式调用 action 对象的方法。

arduino 复制代码
    String result = invocation.invoke(); //调用action 

在此方法中,可以看到,在执行 action 方法调用前执行了requestToAction(action, invocation.getRequest());

完成了参数到 action 自动封装。action 调用后,又执行了actionToRequest(action, invocation.getRequest());

完成了action 到 request 的赋值。

上面代码中 invocation.invoke(); 又是什么?

**public String intercept(DefaultActionInvocation invocation) ** 中的 DefaultActionInvocation 又是用来做什么的呢?

4. DefaultActionInvocation

DefaultActionInvocation 也是我们自定义的类,是真正执行 Action 方法的地方!里面也封装了拦截器需要的 request 对象和 response 对象。

(但是注意:Struts框架不是如此实现的,Struts 中有 ActionContext 来保存了和 request 打交道的对象和数据,所以不需要传参 request 给拦截器,拦截器使用request时,直接从上下文中获取即可。我们这里是简化了此步骤)

DefaultActionInvocation.java 代码如下:

java 复制代码
package com.framework;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DefaultActionInvocation {
    //保留请求和响应引用,用来提前参数及跳转
    private HttpServletRequest request;
    private HttpServletResponse response;

    protected ActionMapping actionMapping;
    protected Object actionBean;//action对象

    public DefaultActionInvocation(ActionMapping actionMapping,HttpServletRequest request,HttpServletResponse response) throws InstantiationException, IllegalAccessException, ClassNotFoundException{
        this.request = request;
        this.response = response;
        this.actionMapping = actionMapping;
        createAction();
    }

    private void  createAction() throws InstantiationException, IllegalAccessException, ClassNotFoundException{
        String className = actionMapping.getClassName();
        this.actionBean = Class.forName(className).newInstance();
    }
    //获取action对象
    public Object getAction(){
        return actionBean;
    }

    public HttpServletResponse getResponse(){
        return response;
    }
    public HttpServletRequest getRequest(){
        return request;
    }
    /**
     * 执行action方法
     */
    public String invoke() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ServletException, IOException {
        //获得action的method
        String methodName = actionMapping.getMethod();
        //如果没有配置method,则默认execute方法
        methodName = methodName==null?"execute":methodName;
        //反射出method对象
        Method method = actionBean.getClass().getMethod(methodName);
        //执行action的方法 获得跳转的逻辑名
        String resultname = (String)method.invoke(actionBean);
        return resultname;
    }
}

从上面代码中可以看出,类中只提供了一个 public 的方法 invoke() 。方法中主要逻辑是使用从核心控制器传入的参数 ActionMapping 对象中获取请求要执行的方法,然后通过反射调用执行后,获取 action 执行返回的结果视图的逻辑名字符串。用于过滤器跳转。

到此为止,我们自定义框架中的所有类已经分别介绍完毕。

本框架其实就是模仿 struts2 框架做了一套简易版的 MVC 框架。

5. 工作流程原理

通过 3.1 部分核心控制器中代码工作流程如下:

1.服务器启动时,会根据 web.xml 配置,先实例化及初始化过滤器 StrutsPrepareAndExecuteFilter。在初始化方法中完成对 struts.xml 文件的加载。(此动作是通过 new ActionMapper(); 来完成的,因为加载代码在 ActionMaper 类的static 代码段中)。并保存在过滤器属性中,缓存起来。

2.浏览器发出 add.action 的请求,被核心控制器 StrutsPrepareAndExecuteFilter 拦截获取请求。过滤器从而调用 actionMapper.getMapping(req); 来获取ActionMapping 对象,从而获取请求的 Action 的配置信息。

3.上一步获取到的 actionMapping 对象再封装到 DefaultActionInvocation 对象中,然后调用参数拦截器并传入 DefaultActionInvocation 对象。

4.在参数拦截器中完成了 Action 对象的获取,并把请求 request 对象中客户端提交过来的参数 num1 和 num2 封装到 action 对象。再调用 DefaultActionInvocation 的 invoke() 方法,在此方法中执行 action 的方法调用。最后在此拦截器中再把 action 执行后的结果放入request 对象。并返回执行后的返回参数 ( 结果视图逻辑名 )。

5.在过滤器调用参数拦截器执行结束后,获取到结果为String 的要跳转的逻辑名称。然后通过

Result result = actionMapping.getResultMap().get(resultname); 把逻辑名转为 Result 对象,Result 对象中封装了要跳转的物理 url 及跳转的类型(转发或重定向)。

6.然后再从 result 对象获取路径,根据类型跳转:

ini 复制代码
        //获取跳转路径url
        String goUrl = result.getPath();
        //取出跳转的类型 转发或重定向
        String type = result.getType();
        if(type==null || type.equals(Result.TYPE_DISPATHER)){
            request.getRequestDispatcher(goUrl).forward(request, response);
        }else{
            resp.sendRedirect(goUrl);
        }

7.最后响应到页面结束。

简单流程图如下:

​编辑

到此本 Chat 介绍结束,有问题可以加微信 zp11481062 交流!

相关推荐
V+zmm101341 天前
基于小程序宿舍报修系统的设计与实现ssm+论文源码调试讲解
java·小程序·毕业设计·mvc·ssm
鲨鱼辣椒不吃辣c2 天前
拦截器魔法:Spring MVC中的防重放守护者
java·spring·mvc
goTsHgo2 天前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
李小白663 天前
Spring MVC(上)
java·spring·mvc
王ASC3 天前
SpringMVC的URL组成,以及URI中对/斜杠的处理,解决IllegalStateException: Ambiguous mapping
java·mvc·springboot·web
撒呼呼3 天前
# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)
数据库·spring boot·spring·mvc·springboot
zybishe4 天前
免费送源码:Java+ssm++MVC+HTML+CSS+MySQL springboot 社区医院信息管理系统的设计与实现 计算机毕业设计原创定制
java·hadoop·sql·zookeeper·html·json·mvc
nie66688885 天前
springmvc的拦截器,全局异常处理和文件上传
spring·mvc
王ASC5 天前
Springboot访问到Controller中不存在的接口BUG
spring boot·后端·mvc
de之梦-御风5 天前
【进阶编程】MVVM的物理架构目录
架构·mvc·.net