spring专题---第三部分MVC---一文教你如何手写MVC框架

在这里插入图片描述

本篇总结内容如下:

    前言
    spring mvc实现原理
    初始化工作
    代码实现
    总结
    分享交流

前言

上一篇文章总结了spring MVC框架的使用,为了更好地理解这个框架,本篇我们仿写一个spring MVC框架,用到的技术为xml解析+反射,不需要JDK动态代理。
手写框架之前,我们先来回顾一下spring MVC的实现原理,这样才能更好地写框架。

spring mvc实现原理

    spring MVC的核心组件和工作流程可以参考上一篇文章,通过上一篇的分析,大致可以将spring MVC流程理解如下:
首先需要一个前置控制器DispatcherServlet,它是整个流程的核心,负责调用其他组件,共同完成业务。
    主要组件有两个:一是Controller,调用其他业务方法Method,执行业务逻辑,二是ViewResolver视图解析器,将业务方法的返回值解析为物理视图+模型数据,返回客户端。
我们一起按照上边的思路写框架。

初始化工作

    •根据spring IOC容器的特性,需要将参与业务的对象全部创建并保存到容器中,供流程调用。
    •首先,我们需要创建Controller对象,HTTP请求通过注解找到对应的Controller对象,因此我们需要将 所有的Controller与其对应的注解建立关联。我们可以选择Map集合来保存,因为key-value结构可以key保存注解,value保存Controller对象,这样就模拟了IOC容器。
    •Controller中的Method也是通过Map集合与其注解建立关联,HTTP通过注解找到对应的Method。
    •实例化视图解析器。
初始化工作完成,下面我们来处理HTTP请求,业务流程如下:
(1)DispatcherServlet接收请求,通过映射从IOC容器中获取对应的Controller对象;
(2)根据映射获取Controller对象对应的Method;
(3)调用Method,获取返回值
(4)将返回值传给视图解析器,视图解析器将逻辑视图转换为物理视图,返回物理视图。
(5)完成页面跳转
思路大致如此,接下来就开始写代码了,我们需要创建以下四个类:
(1)MyDispatcherServlet,模拟DispatcherServlet,是MVC框架的中枢,负责接收用户请求,调配其他各个组件;
(2)MyController,模拟Controller注解,负责业务代码部分;
(3)MyRequestMapping,模拟RequestMapping注解,负责映射;
(4)MyViewResolver,模拟ViewResolver视图解析器。

代码实现

(1)创建MyController,MyRequestMapping注解类,注册@Controller和@RequestMapping

//MyController
@Target(ElementType.TYPE)//使注解作用于类
@Retention(RetentionPolicy.RUNTIME)
public @interface MyController {
    String value() default "";
}
//MyRequestMapping
@Target({ElementType.TYPE,ElementType.METHOD})//使注解作用于类和方法
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRequestMapping {
    String value() default "";
}

(2)创建MyViewResolver

//MyViewResolver.java
public class MyViewResolver {
    private String prefix;
    private String suffix;
    public String getPrefix(){
        return prefix;
    }
    public void setPrefix(String prefix){
        this.prefix=prefix;
    }
    public String getSuffix(){
        return suffix;
    }
    public void setSuffix(String suffix){
        this.suffix=suffix;
    }
    public String jspMapping(String value){//进行拼接
        return this.prefix+value+this.suffix;
    }
}

(3)创建MyDispatcherServlet.java

//MyDispatcherServlet.java
import com.southwind.annotation.MyController;
import com.southwind.annotation.MyRequestMapping;
import com.southwind.view.MyViewResolver;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;

public class MyDispatcherServlet extends HttpServlet{

    //创建Map集合用于存放Controller实例对象
    private Map<String,Object> iocContainer=new HashMap<String, Object>();
    //创建Map集合用于存放handler对象,handler可以理解为有 @controller 的类里边的 有@RequestMapping 的方法
    private Map<String, Method> handlerMapping=new HashMap<String, Method>();
    //自定义视图解析器,这个是咱们自己写的类,在后边有它的具体业务代码
    private MyViewResolver myViewResolver;
    //init完成初始化工作
    public void init(ServletConfig config) throws ServletException {
        //扫描@Controller,创建实例对象,并存入iocController
        scanController(config);
        //扫描@RequestMapping,将符合的handler存入handlerMapping, handler我上边已经解释过了
        initHandlerMapping();
        //加载视图解析器,可以理解为将返回的字符串进行拼接,例如返回index,处理后为/index.jsp
        //扫描springmvc.xml,读取配置文件中的prefix和suffix,获取咱们自己写的类,这个类作用就是拼接并返回一个返回路径,不懂的看下边会慢慢明白的
        loadViewResolver(config);
    }
    //扫描@Controller
    public void scanController(ServletConfig config){
        //SAXReader是dom4j里的,专门用于解析xml配置文件,dom4j需要我们引入依赖,maven工程可以在pom.xml引入dom4j
        SAXReader reader=new SAXReader();
        try {
            //解析springmvc.xml , path是它的完整路径
            String path=config.getServletContext().getRealPath("")+"\\WEB-INF\\classes\\"+config.getInitParameter("contextConfigLocation");
            Document document=reader.read(path);//通过所给的路径去寻找xml文件,document代表整个xml文件
            Element root=document.getRootElement();//获取根节点,任何xml解析都是从根节点开始的
            Iterator iter=root.elementIterator();//获取到跟节点接着使用elementIterator获取它的子节点,Iterator是迭代器,迭代遍历根节点的每一个子节点
            while(iter.hasNext()){
                Element ele=(Element)iter.next();//获取每一个子节点的对象,将每一个子节点对象转化为Element类型,这里运用到了强转
                if(ele.getName().equals("component-scan")){//判断该子节点标签名字是不是component-scan,也就是看是不是<context:component-scan base-package="com.summer.shh.controller"/>这个标签
                    String packageName=ele.attributeValue("base-package");//能够走这一步说明该子节点对象确实是component-scan标签,这一步是获取标签中base-package
                    List<String> list=getClassNames(packageName);//获取base-package包下所有类名,getClassName是咱们自己写的类,这个类作用就是遍历这个包下所有的类,然后获取每一个类的完整名(包名+类名)并存入List集合中
                    for(String str:list){//进行for增强,遍历值给str
                        Class clazz=Class.forName(str);//根据str获取每一个类的类名
                        if(clazz.isAnnotationPresent(MyController.class)){//获取该类是否有MyController注解
                            MyRequestMapping annotation=(MyRequestMapping)clazz.getAnnotation(MyRequestMapping.class);//能够进入这一步说明确实有MyController注解,这一步是获取@MyController中@MyRequestMapping注解类
                            String value=annotation.value().substring(1);//获取该注解的value值   注意annotation.value().substring(1)是为了截掉/
                            iocContainer.put(value,clazz.newInstance());//创建iocController类实例化对象并以MyRequestMapping的value(@RequestMapping(value="/admin"))作为Map的键
                        }
                    }
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
     }
     //获取base-package中指定包下所有类的类名,这个类是服务于上边的,在上边我也已说明
     public List<String> getClassNames(String packageName){
        List<String> classNameList=new ArrayList<String>();//List集合存放包下所有类名
        String packagePath=packageName.replace(".","/");//将base-package声明的.换成/,从而变成目录路径进行遍历
        ClassLoader loader=Thread.currentThread().getContextClassLoader();
        URL url=loader.getResource(packagePath);
        if(url!=null){
            File file=new File(url.getPath());
            File[] childFiles=file.listFiles();
            for(File childFile: childFiles){
                String className=packageName+"."+childFile.getName().replace(".class","");//base-package包路径+包下各个路径,将.class去掉
                classNameList.add(className);//获得的包下所有的类路径(包名+类名),存放于List集合中
            }
        }
        return classNameList;//返回这个拥有base-package包下所有类的集合
     }
     //初始化handler映射,作用上边我也说明了
     public void initHandlerMapping(){
        for(String str:iocContainer.keySet()){//遍历iocContainer集合的key
            Class clazz=iocContainer.get(str).getClass();//获得每一个key对应的class
            Method[] methods=clazz.getMethods();//获取每一个带有@MyController注解的类下所有方法存放在这个数组里边
            for(Method method:methods){//遍历数组中每一个方法
                if(method.isAnnotationPresent(MyRequestMapping.class)){//判断每一个方法是否有@MyRequestMapping注解
                    MyRequestMapping annotation=method.getAnnotation(MyRequestMapping.class);//获取@MyRequestMapping注解的运行时类
                    String value=annotation.value().substring(1);//获取@MyRequestMapping注解的value值,例如@RequestMapping(value="/list")  annotation.value().substring(1)是为了截掉/
                    handlerMapping.put(value,method);//将method放入Map集合中,handlerMapping是我们刚开始创建的Map集合,目的存放每个有@MyRequestMapping的方法
                }
            }
        }
     }
     //加载自定义视图解析器,作用上边我也说明了
     public void loadViewResolver(ServletConfig config){
        SAXReader reader=new SAXReader();//解析xml,专门用于解析xml配置文件的
        try{
            String path=config.getServletContext().getRealPath("")+"\\WEB-INF\\classes\\"+config.getInitParameter("contextConfigLocation");//解析springmvc.xml , path是它的完整路径
            Document document=reader.read(path);//通过所给的路径去寻找xml文件,document代表整个xml文件
            Element root=document.getRootElement();//获取根节点,任何xml解析都是从根节点开始的
            Iterator iter=root.elementIterator();//获取到跟节点接着使用elementIterator获取它的子节点,Iterator是迭代器,迭代遍历根节点的每一个子节点
            while(iter.hasNext()){
                Element ele=(Element)iter.next();//获取每一个子节点的对象,将每一个子节点对象转化为Element类型,这里运用到了强转
                if(ele.getName().equals("bean")){//获取每个子节点是否是bean标签,比如<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                    String className=ele.attributeValue("class");//获取标签的class值,如果我们用spring mvc官方框架的话,我们会发现其实它的class是固定的
                    Class clazz=Class.forName(className);//根据class值获得该类名
                    Object obj=clazz.newInstance();//创建该类对象
                    Method prefixMethod=clazz.getMethod("setPrefix",String.class);//获得该类setPrefix方法
                    Method suffixMethod=clazz.getMethod("setSuffix",String.class);//获得该类setSuffix方法
                    Iterator beanIter=ele.elementIterator();//遍历每一个bean标签子标签
                    Map<String,String> propertyMap=new HashMap<String, String>();//Map集合用于存放bean标签的子标签属性
                    while(beanIter.hasNext()){//遍历
                        Element beanEle=(Element)beanIter.next();//获取每一个子节点的对象,将每一个子节点对象转化为Element类型,这里运用到了强转
                        String name=beanEle.attributeValue("name");//子节点对象获取property标签的name值,比如<property name="prefix" value="/WEB-INF/pages/"/>
                        String value=beanEle.attributeValue("value");//子节点对象获取property标签的value值
                        propertyMap.put(name,value);//将name值和value值存入Map集合
                    }
                    for(String str:propertyMap.keySet()){//获得Map集合的key值
                        if(str.equals("prefix")){//判断是否是prefix
                            prefixMethod.invoke(obj,propertyMap.get(str));//执行setPrefix方法,第一个参数是该类对象,第二个参数是要set的值
                        }
                        if(str.equals("suffix")){//判断是否是prefix
                            suffixMethod.invoke(obj,propertyMap.get(str));//执行setSuffix方法,第一个参数是该类对象,第二个参数是要set的值
                        }
                    }
                    myViewResolver=(MyViewResolver)obj;//将该类对象强转为MyViewResolver类型
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
     }

     protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException,IOException{
        this.doPost(req,resp);
     }

     protected void doPost(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
        String handlerUri=req.getRequestURI().split("/")[1];//获取请求路径剪切第二个,对应@Controller的@RequestMapping的value
        Object obj=iocContainer.get(handlerUri);
        String methodUri=req.getRequestURI().split("/")[2];//获取请求路径剪切第三个,对应@Controller下有@RequestMapping(value="")的方法value
         Method method=handlerMapping.get(methodUri);
        try{
            String value=(String)method.invoke(obj);//执行method方法,传入参数为@Controller对象
            String result=myViewResolver.jspMapping(value);//拼接,这个方法在MyViewResolver中
            req.getRequestDispatcher(result).forward(req,resp);//请求转发,转发路径为result,result是已经拼接好的返回路径
        }catch(Exception e){
            e.printStackTrace();
        }
     }

}

(4)创建TestController.java

//TestController.java
@MyController
@MyRequestMapping(value = "/testController") //  "/"一定不能省
public class TestController {

    @MyRequestMapping(value = "/test")  //  "/"一定不能省
    public String Test(){
        System.out.println("doing Test");
        return "index";
    }
}

    •以上便是框架代码,我在代码中都做了详细的注释,还有不明白的地方可以留言或私信我。
项目运行成功如图:
在这里插入图片描述
在这里插入图片描述

总结

    sprin MVC框架用到了IOC容器这一部分,大家可以看一下我前边写的自写IOC组件内容,手写框架目的在于能够让我们明白框架是如何运行的。spring MVC框架重点在于解析xml配置文件并依据注解寻找对应的Controller和Method,进行映射并返回,依据返回值进行视图解析返回给用户,过程虽然有些难理解,但重在坚持,相信你会有所收获的。

分享交流

    以上便是我对这一部分的理解,如果有错误或者你有其他疑惑都可以留言给出,我都会一一进行回复,希望对你有所帮助,如果写的不好也请您多多包涵。欢迎在下方补充留言哟。
    对SSM框架感兴趣的童鞋,可以移步这里,在这里你可以快速的搭建好一个SSM框架。
    如果你在写项目的时候,遇到一些不寻常的问题,也可以关注我的博客园,上边会发布一些我在写项目中遇到的各种问题以及解决方式。

发布了21 篇原创文章 · 获赞 27 · 访问量 4283

猜你喜欢

转载自blog.csdn.net/llllxxxxyyyy/article/details/104101589