一,看源码目的是为了什么,看源码目的是为了理解别人思想的过程,然后进行高度抽象,转变成自己的。
程序员在看别人写的代码非常吃力,那么我们反过来试一下,先把整体思想告诉你,然后再看源码会产生什么不一样的结果。
我们手写一个SpringMVC框架前先梳理一下整体代码设计思路,然后再具体写代码。
首先先建一个普通的maven项目,pom文件不要引入任何Jar包。开始!!!
第一步,SpringMVC有@Controller,@Service,@RequestMapping,@Autowired,@RequestParam注解对不对?
先盘一下这几个代码怎么写。
建一个标签类@interface,然后就是固定格式,程序员看家的本领(C,V)
package com.spring.jxd.annotation; import java.lang.annotation.*; @Target(java.lang.annotation.ElementType.FIELD)//FIELD表示这个注解只能在属性上使用 @Retention(RetentionPolicy.RUNTIME)//表示在运行的时候可以通过反射获取(90%的注解都是RUNTIME注解只是一个载体) @Documented//编译的时候放到javadoc public @interface JxdAutowired { String value() default "";//表示注解后边可以跟字符串例如@JxdController("/test") }
import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(java.lang.annotation.ElementType.TYPE)//TYPE表示这个注解只能在类上使用 @Retention(RetentionPolicy.RUNTIME)//表示再运行的时候可以通过反射获取(90%的注解都是RUNTIME注解只是一个载体) @Documented//编译的时候放到javadoc public @interface JxdController { String value() default "";//表示注解后边可以跟字符串例如@Controller("/test") }
package com.spring.jxd.annotation; import java.lang.annotation.*; @Target({java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.TYPE})//METHOD表示在方法上边 @Retention(RetentionPolicy.RUNTIME)//表示再运行的时候可以通过反射获取(90%的注解都是RUNTIME注解只是一个载体) @Documented//编译的时候放到javadoc public @interface JxdRequestMapping { String value() default "";//表示注解后边可以跟字符串例如@Controller("/test") }
package com.spring.jxd.annotation; import java.lang.annotation.*; @Target(java.lang.annotation.ElementType.PARAMETER)//TYPE这个注解表示在参数上使用 @Retention(RetentionPolicy.RUNTIME)//表示再运行的时候可以通过反射获取(90%的注解都是RUNTIME注解只是一个载体) @Documented//编译的时候放到javadoc public @interface JxdRequestParam { String value() default "";//表示注解后边可以跟字符串例如@Controller("/test") }
package com.spring.jxd.annotation; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(java.lang.annotation.ElementType.TYPE)//TYPE表示只能在类上使用 @Retention(RetentionPolicy.RUNTIME)//表示再运行的时候可以通过反射获取(90%的注解都是RUNTIME注解只是一个载体) @Documented//编译的时候放到javadoc public @interface JxdService { String value() default "";//表示注解后边可以跟字符串例如@Controller("/test") }
上边我们建立了自己的标签分别为@JxdController,@JxdService,@JxdRequestMapping,@JxdAutowired,@JxdRequestParam 很形象直观易懂是干什么的,不多解释,我名字的首字母Jxd你也可以用你名字来命名。然后建一个包名字叫annotation把这些注解类放到里边(就类似于把工具类方到util包里边一样)。
第二步,你想啊,Tomcate在启动的时候Spring是不是需要扫描一下你写的项目,然后把项目中所有的类文件都扫描拿出来放到一个集合里边。就是你Spring xml配置文件里的ScanPackage你输入一个包名例如"com.demo"扫描这个文件下的所有文件,文件夹对不对。
我们定义一个doScanPackage方法,这个方法的目的是把所有的类文件取名称以字符串形式加载的一个叫classNames的List
//扫描文件目的就是拿到com.spring下的所有calss并且保存到classNames集合中以便以后调用 public void doScanPackage(String basePackage){ // URL url=this.getClass().getClassLoader().getResource("/"+basePackage.replaceAll("\\.","/")); String fileStr=url.getFile();//转换成字符串地址 File file =new File(fileStr); String [] filesStr=file.list(); for (String path:filesStr) { File filePath=new File(fileStr+path); if(filePath.isDirectory()){//判断是否是文件夹 doScanPackage(basePackage+"."+path); }else{ classNames.add(basePackage+"."+path);//basePackage+"."+path=com.spring.jxd.controller.jxdController类保存进来 } } }
说明一下com.spring需要转换成com/spring这种形式,因为file只识别“/”这种形式。然后找到之后再转回“.”这种形式保存起来。为什么继续往下看。
第三步,你再想啊Spring IOC是什么,IOC是一个容器对不对,这个容器盛着你所用到的bean,你标注的@Controller,@Service等注解都会加载到这个容器里边。这个容器底层就是一个Map,key就是这个bean名字首字母小写,value就是这个bean的实例。
我们定义一个doInstance方法,这个方法就是把所有包含@JxdController注解的@JxdService注解的类加载到IOC容器中(Map集合中)。
public void doInstance(){ for (String className:classNames) { //com.spring.jxd.controller.jxdController.class遍历出的类 String cn= className.replace(".class","");//去掉.class后缀 try { Class> clazz=Class.forName(cn);//先把路径变成class对象再说 if(clazz.isAnnotationPresent(JxdController.class)){//用于判断类上边的注解是否用到JxdController注解 //是Controller注解类 Object instance1= clazz.newInstance();//实例化这个类 //把是Controller注解类放到IOC容器中 JxdRequestMapping map= clazz.getAnnotation(JxdRequestMapping.class); String key=map.value();//控制类拿到JxdRequestMapping注解中的值当作key注入容器 beansIco.put(key.replace("/",""),instance1);//把Controller类中的/去掉 }else if (clazz.isAnnotationPresent(JxdService.class)){ //是Service注解的类 Object instance= clazz.newInstance();//实例化这个类 //把Service类放到IOC容器中 JxdService map= clazz.getAnnotation(JxdService.class); String key=map.value();//控制类拿到JxdRequestMapping注解中的值当作key注入容器 beansIco.put(key,instance); }else{ continue;//Spring底层代码就这样做的 } } catch (ClassNotFoundException e) { e.printStackTrace(); }catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
第四步,我们的@JxdController类里边是不是有一些@JxdAutowired注解的属性,要求你给他注入。我们用反射机制给他注入实例。
//对容器中业务实例 public void doAutowired(){ for (Map.Entryentry:beansIco.entrySet()) { System.out.println(entry.getKey()+"mapKey"); Object instance =entry.getValue(); Class> clazz =instance.getClass();//把类变成Class方便拿其中的属性 if(clazz.isAnnotationPresent(JxdController.class)){//目前只判断JxdController类的是否注入实例 //是Controller类所以拿成员变量 //getDeclaredField("name")这个方法可以拿name的属性,做一个扩展实际没有用到 Field[] fileds = clazz.getDeclaredFields();//拿到这个类中所有的属性万物皆对象属性值也是对象 //判断属性是否有JxdAutowired注解 for (Field filed:fileds) { if (filed.isAnnotationPresent(JxdAutowired.class)){ JxdAutowired ea= filed.getAnnotation(JxdAutowired.class);//拿属性中的value值 String key =ea.value(); Object obj=beansIco.get(key); filed.setAccessible(true);//因为该变量为private先要打开权限 try { filed.set(instance,obj);//你要告诉它是哪一个类的set方法然后反射赋值 } catch (IllegalAccessException e) { e.printStackTrace(); } }else{ continue; } } }else{ continue; } } }
我们IOC容器里边包含着所有的@JxdController注解的类所以直接遍历它就可以。
第五步,当我们的SpringMVC再接收到一个请求访问到我们@Controller类时,他是怎么知道到底进入哪个方法的,
localhost:8080/demo/query 比如这个请求,他是怎么能准确定位到哪个方法的,是不是靠@RequestMapping()这个注解来定义的(我们这里叫@JxdRequestMapping),我们思路是这样的,先把类上边的@RequestMapping("/demo")这个value值取出来,在把方法上的@RequestMapping("/query")value值取出来,然后做拼接字符串,拼成"/demo/query",然后创建一个Map,key为这个拼接好的字符串,value为这个方法(方法也是类万物皆对像)。这样一个请求过来之后我就知道你要进哪个方法了。
//把跳转路径和方法对象一一映射起来用map保存 public void urlMapping(){ for (Map.Entryentry:beansIco.entrySet()) { Object instance=entry.getValue(); Class> clazz =instance.getClass(); if(clazz.isAnnotationPresent(JxdController.class)){ //是控制类先拿类的路径在拿方法路径 JxdRequestMapping map=clazz.getAnnotation(JxdRequestMapping.class); String classPath= map.value();//拿类的路径 /jxd Method[] methods =clazz.getMethods();//先拿到类下边所有方法 for (Method method: methods) { if(method.isAnnotationPresent(JxdRequestMapping.class)){ //方法上用到JxdRequestMapping注解 JxdRequestMapping mapMethod= method.getAnnotation(JxdRequestMapping.class); String methodPath =mapMethod.value();//拿到方法上的注解value值 query handlerMap.put(classPath+methodPath,method); }else { continue; } } } } }
最后一步,以上五步要求再tomcate启动的时候全部完成,我们定一个JxdDispatcherServlet 继承HttpServlet ,把他们都写到init()方法里边就可以了。
public class JxdDispatcherServlet extends HttpServlet { ListclassNames =new ArrayList ();//用于保存class类文件 Map beansIco=new HashMap ();//定义IOC容器 Map handlerMap =new HashMap ();//路径跳转Map; //扫描当前所有路径名找这些类创建ico容器 //实例化(ICO容器 放到MAP中) //url-method路径跳转 public void init(ServletConfig config) throws ServletException{ doScanPackage("com.spring");//扫描com.spring下的所有包,文件 doInstance();//实例化把Controller,Service注解声明类放到IOC容器中 doAutowired();//注解自动注入 urlMapping();//把跳转路径和方法一一映射起来用map保存 }
}
JxdDispatcherServlet 这个就是SpringMVC源码中的DispatcherServlet分发类,我知道你们没有时间把这些都写一遍,但是我的目的就是为了让你们了解一下,理解思想之后再看源码,会比直接看源码大不相同,后期我会把这个完整的手写SpringMVC小Demo发gitHub上边,需要的可以自行下载测试。