本篇总结内容如下:
前言
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框架。
如果你在写项目的时候,遇到一些不寻常的问题,也可以关注我的博客园,上边会发布一些我在写项目中遇到的各种问题以及解决方式。