写一个自己的springMVC

今天我们来实现一个简单的springMVC框架,可以理解为 springMVC1.0这个版本,只是功能比较简单而已;

废话不多说,进入正题;先看看springMVC的简单流程;

我们请求的地址会被dispatchservlet这个springMVC的核心拦截器拦截,然后dispatchservlet会找到我们想要的那个controller里的那个方法,并调用。但是dispatchServlet不是人,它没那么智能,看到url就知道是谁了,但是我们可以让它变得智能起来,我们可以利用handlerMapping来告诉

dispatchServlet,它应该调用哪个方法;

为了让这个框架不那么笨,我借用了spring的IOC 思想, 实现一个容器来管理我的bean; 这个框架和你印象中使用springmvc 很相似,你应该能回想起

springMVC的零零点点,然后看看这个简单的框架是如何实现这些功能的;

首先看下项目工程:

首先,我们先看maven 依赖,没错,自己实现的框架当然不用spring的jar包了;

并为了方便大家理解,我的取名和spring原来的风格多少有些类似;

首先看到annotation包,@Autowired、@Controller、@RequestMapping、@service这些注解大家应该再熟悉不过了吧!

然后是 servlet包,模仿的springMVC的核心拦截器 dispatchServlet;

demo包的 controller service  不必解释啦~

这里我们模仿springMVC的调用规则,MyDispatcherServlet负责处理框架的逻辑,调用Controller,Controller调用service;

先看看自定义注解是如何定义的,这里挑选了几个代表;

开始编写核心拦截器 dispatchServilet

我们第一步模仿spring 的思想,先找到我们要扫描哪些类,下面是 spring的做法,

这是我的做法:

为什么我通过  String scanPackage = config.getInitParameter("scanPackage"); 就能找到xml中的配置呢?请参考这里 的初始化细节;

servlet 对象在初始化的时候,容器会给它提供一个 ServletConfig 对象  去读取 web.xml中的配置;

我们得到要扫描的路径后,可以就需要实现spring的IOC了;

我们为了得到所有bean;在拿到项目的包路径后,可以转换为文件路径,然后再从文件路径中得到所有的类名;

得到类名后,就可以通过反射进行实例化了,然后将这些需要管理的东西放到一个容器中管理,要用的时候从容器里拿就可以了;

我这里使用的容器是 Hashmap<String, Object>    类的简称(SimpleName)为key ,类的实例对象为value。

得到了所有的类名后,开始实例化的工作

[java] view plain copy

  1. private void instance(){  
  2. //利用反射机制将扫描到的类名全部实例化
  3. if(classNames.size() == 0){ return; }  
  4. try{  
  5. for (String className : classNames) {  
  6.                     Class<?> clazz = Class.forName(className);  
  7. //没有@Controller、@Service注解标识的类不需要实例化
  8. if(clazz.isAnnotationPresent(LANController.class)){  
  9. //getSimpleName() 除去包名,获取类名的简称  例如: MyAction
  10.                         String beanName = lowerFirstChar(clazz.getSimpleName());  
  11.                         instanceMapping.put(beanName, clazz.newInstance());  
  12.                     }else if(clazz.isAnnotationPresent(LANService.class)){  
  13.                         LANService service = clazz.getAnnotation(LANService.class);  
  14.                         String beanName = service.value();  
  15. if(!"".equals(beanName.trim())){  
  16. //beanName 这里就是aa
  17.                             instanceMapping.put(beanName, clazz.newInstance());  
  18. continue;  
  19.                         }  
  20. //如果自己没有起名字,后面会通过接口自动注入
  21.                         Class<?> [] interfaces = clazz.getInterfaces();  
  22. for (Class<?> i : interfaces) {  
  23.                             instanceMapping.put(i.getName(), clazz.newInstance());  
  24.                         }  
  25.                     }else{  
  26. continue;  
  27.                     }  
  28.             }  
  29.         }catch(Exception e){  
  30.             e.getStackTrace();  
  31.         }  
  32.     }  

实例化以后,就要准备注入了;

[java] view plain copy

  1. private void autowired(){  
  2. if(instanceMapping.isEmpty()){ return; }  
  3. for (Entry<String, Object> entry : instanceMapping.entrySet()) {  
  4. //getDeclaredFields()获取自己声明的所有字段
  5.             Field [] fields = entry.getValue().getClass().getDeclaredFields();  
  6. for (Field field : fields) {  
  7. if(!field.isAnnotationPresent(LANAutowired.class)){ continue; }  
  8.                 LANAutowired autowired = field.getAnnotation(LANAutowired.class);  
  9. //如果是私有属性,设置可以访问的权限
  10.                 field.setAccessible(true);  
  11. //自己取的名字   获取注解的值
  12.                 String beanName = autowired.value().trim();  
  13.                 System.out.println("beanName=="+beanName);  
  14. //如果没有自己取名字
  15. if("".equals(beanName)){  
  16. //getType()获取该字段声明时的     类型对象   根据类型注入
  17.                     beanName = field.getType().getName();  
  18.                 }  
  19. try {  
  20.                     System.out.println("field.getName()***"+field.getName());  
  21. // 注入接口的实现类,  
  22.                      System.out.println("entry.getValue()======"+entry.getValue());  
  23.                      System.out.println("instanceMapping.get(beanName)---------"+instanceMapping.get(beanName));  
  24. //将Action 这个 类的 IModifyService 字段设置成为   aa 代表的实现类  ModifyServiceImpl
  25.                     field.set(entry.getValue(),instanceMapping.get(beanName));  
  26.                 } catch (Exception e) {  
  27.                     e.printStackTrace();  
  28. continue;  
  29.                 }  
  30.             }  
  31.         }  
  32.     }  

[java] view plain copy

  1. field.set(entry.getValue(),instanceMapping.get(beanName));  

这一行代码是关键,这就是为什么我们在注入接口,就能找到实现类的根本所在。

这里的field,就是我们注入的那个接口, entry.getValue() 得到的是接口所在的类,instanceMapping.get(beanName)是这个接口对应的

那个实现类,   意思就是:在 运行阶段, 将 controller中 的某个service接口字段 替换成 这个service的实现类;

这样我们在编写代码的时候是用使用接口调用方法,但实际运行时,就是它的实现类在调用这个方法了;

不得不感叹,反射的强大。

完成注入后,开始处理handlermapping上的value与之对应的method 的映射关系了

[java] view plain copy

  1. public void handlerMapping(){  
  2. if(instanceMapping.isEmpty()){ return; }  
  3. for (Entry<String, Object> entry : instanceMapping.entrySet()) {  
  4.             Class<?> clazz = entry.getValue().getClass();  
  5. //RequestMapping只在 Controller中
  6. if(!clazz.isAnnotationPresent(LANController.class)){ continue; }  
  7.             String url = "";  
  8. if(clazz.isAnnotationPresent(LANRequestMapping.class)){  
  9.                 LANRequestMapping requstMapping = clazz.getAnnotation(LANRequestMapping.class);  
  10. //得到RequstMapping的value(/web)准备与 方法上的 RequstMapping(search/add/remove)进行拼接;
  11.                 url = requstMapping.value();//(/web)
  12.             }  
  13.             Method [] methods = clazz.getMethods();  
  14. for (Method method : methods) {  
  15. if(!method.isAnnotationPresent(LANRequestMapping.class)){ continue; }  
  16.                 LANRequestMapping requstMapping = method.getAnnotation(LANRequestMapping.class);  
  17.                 String regex =  url + requstMapping.value();  
  18. // regex = /web/add.json
  19.                 regex = regex.replaceAll("/+", "/").replaceAll("\\*", ".*");  
  20.                 System.out.println("regex: "+regex);  
  21.                 Map<String,Integer> pm = new HashMap<String,Integer>();  
  22. //因为每个参数可能有多个注解,所以会是个二维数组
  23.                 Annotation [] [] pa = method.getParameterAnnotations();  
  24. for(int i = 0; i < pa.length; i ++){  
  25. for (Annotation a : pa[i]){  
  26. if(a instanceof LANRequestParam){  
  27.                             String paramName = ((LANRequestParam) a).value();  
  28. if(!"".equals(paramName.trim())){  
  29. //方法参数的名字(值)  name/addr,下标
  30.                                 pm.put(paramName, i);  
  31.                             }  
  32.                         }  
  33.                     }  
  34.                 }  
  35. //提取Request和Response的索引
  36.                 Class<?> [] paramsTypes = method.getParameterTypes();  
  37. for(int i = 0 ; i < paramsTypes.length; i ++){  
  38.                     Class<?> type = paramsTypes[i];  
  39. if(type == HttpServletRequest.class ||  type == HttpServletResponse.class){  
  40.                         pm.put(type.getName(), i);  
  41.                     }  
  42.                 }  
  43.                 handlerMapping.add(new Handler(Pattern.compile(regex),entry.getValue(),method, pm));  
  44.             }  
  45.         }  
  46.     }  

完成所有的初始化工作后,当然就等着用户发过来的请求咯。

那自然是调用servlet的 doPost 和 doGet方法了,

为了简单点,我在doGet中调用doPost

[java] view plain copy

  1. @Override
  2. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  3. this.doPost(req, resp);  
  4.     }  
  5. @Override
  6. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  7. //System.out.println(req.getRequestURI());
  8. try{  
  9. //从上面已经初始化的信息中匹配
  10. //拿着用户请求url去找到其对应的Method
  11. boolean isMatcher = pattern(req,resp);  
  12. if(!isMatcher){  
  13.             resp.getWriter().write("对不起,你遇到了  404 Not Found");  
  14.         }  
  15.         }catch(Exception e){  
  16.             resp.getWriter().write("500 Exception,Details:\r\n" +   
  17.             e.getMessage() + "\r\n" +  
  18.             Arrays.toString(e.getStackTrace()).replaceAll("\\[\\]", "")  
  19.             .replaceAll(",\\s", "\r\n"));  
  20.         }  
  21.     }  

如果如果没有匹配成功就返回404,说明用户的路径输错了,

发送异常就报500;

如果匹配成功怎么办? 当然是调用controller里的方法咯;

怎么调用?还是通过反射~通过方法的反射~

public boolean pattern(HttpServletRequest req, HttpServletResponse resp) throws Exception{   if(handlerMapping.isEmpty()){  return false; }  

  1. //获取请求的url
  2.         String url = req.getRequestURI();  
  3. //获取容器路径
  4.         String contextPath = req.getContextPath();  
  5.         url = url.replace(contextPath, "").replaceAll("/+", "/");     
  6. for (Handler handler : handlerMapping) {  
  7. try{  
  8.                 Matcher matcher = handler.pattern.matcher(url);  
  9. //如果没匹配上就跳出
  10. if(!matcher.matches()){ continue ;}  
  11. //按照声明顺序返回  Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。
  12.                 Class<?> [] paramTypes = handler.method.getParameterTypes();  
  13. //里面存放 反射是需要的具体参数
  14.                 Object [] paramValues = new Object[paramTypes.length];  
  15. //获取前端请求参数和请求参数值的映射关系
  16.                 Map<String,String[]> params = req.getParameterMap();  
  17. for (Entry<String, String []> param : params.entrySet()) {  
  18.                     String value = Arrays.toString(param.getValue()).replaceAll("\\]|\\[", "").replaceAll(",\\s", ",");  
  19. if(!handler.paramMapping.containsKey(param.getKey())){ continue;}  
  20. //如果匹配,则获取该参数在方法中的下标;
  21. int index = handler.paramMapping.get(param.getKey());  
  22. //涉及到类型转换
  23.                     paramValues[index] = castStringValue(value,paramTypes[index]);  
  24.                 }  
  25. //
  26. int reqIndex = handler.paramMapping.get(HttpServletRequest.class.getName());  
  27.                 paramValues[reqIndex] = req;  
  28. int respIndex = handler.paramMapping.get(HttpServletResponse.class.getName());  
  29.                 paramValues[respIndex] = resp;  
  30. //方法的反射   需要对象.方法
  31.                 handler.method.invoke(handler.controller, paramValues);  
  32. return true;  
  33.             }catch(Exception e){  
  34. throw e;  
  35.             }  
  36.         }  
  37. return false;  
  38.     }  

我们来看看controller的代码,准备测试:

[java] view plain copy

  1. public class MyController {  
  2. public  MyController() {  
  3.         System.out.println("初始化了"+this);  
  4.     }  
  5. @LANAutowired
  6. private searchService searchService;  
  7. @LANAutowired("aa")   
  8. private modifyService modifyService;  
  9. @LANRequestMapping("/search/*.json")  
  10. public void search(HttpServletRequest request,HttpServletResponse response,  
  11. @LANRequestParam("name") String name){  
  12.         String result = searchService.search(name);  
  13.         System.out.println("查询成功");  
  14.         out(response,result);  
  15.     }  
  16. @LANRequestMapping("/add.json")  
  17. public void add(HttpServletRequest request,HttpServletResponse response,  
  18. @LANRequestParam("name") String name,  
  19. @LANRequestParam("addr") String addr){  
  20.         System.out.println("添加成功");  
  21.         String result = modifyService.add(name,addr);  
  22.         out(response,result);  
  23.     }  
  24. @LANRequestMapping("/delete.json")  
  25. public void remove(HttpServletRequest request,HttpServletResponse response,  
  26. @LANRequestParam("id") Integer id){  
  27.         System.out.println("删除成功");  
  28.         String result = modifyService.remove(id);  
  29.         out(response,result);  
  30.     }  

是不是和你刚学springMVC时的一致?

赶紧启动项目在地址栏输入:http://localhost:8080/LanSpringMVC/web/add.json?name=witt&addr=shenzhen

出现:

[java] view plain copy

  1. add name = witt,addr=shenzhen  

说明大工告成~

想要源码的小伙伴点这里:源码

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏偏前端工程师的驿站

编译期类型检查 in ClojureScript

1032
来自专栏编舟记

Java内部类的异常处理

最近遇到一个问题,使用Java写某个DSL标记语言X的parser(解析器)Maven插件的时候,对外暴露一个名为Callback的接口和一个待实现的方法get...

772
来自专栏青青天空树

java导出Excel文件

  最近在java上做了一个EXCEL的导出功能,写了一个通用类,在这里分享分享,该类支持多sheet,且无需手动进行复杂的类型转换,只需提供三个参数即可:

3321
来自专栏一个会写诗的程序员的博客

《Kotlin 程序设计》第十二章 Kotlin的多线程

Kotlin 1.1 introduced coroutines, a new way of writing asynchronous, non-blockin...

1041
来自专栏游戏杂谈

JavaScript立即调用的函数表达式

主要参考知乎上这个问题:javascript 匿名函数有哪几种执行方式 长天之云的回答。

1202
来自专栏圣杰的专栏

线程安全知多少

1. 如何定义线程安全 线程安全,拆开来看: 线程:指多线程的应用场景下。 安全:指数据安全。 多线程就不用过多介绍了,相关类型集中在System.Thread...

3435
来自专栏difcareer的技术笔记

JNI实现源码分析【四 函数调用】正文0x01:dvmCallMethodV0x02:nativeFunc0x03: 何时赋值

有了前面的铺垫,终于可以说说虚拟机是如何调用JNI方法的了。JNI方法,对应Java中的native方法,所以我们跟踪对Native方法的处理即可。

964
来自专栏屈定‘s Blog

Java8 Lambda(二)-Stream原理

推荐一篇博文,很好的介绍了Stream的原理.本文对其进行一些补充更加详细的讲解.

3302
来自专栏我就是马云飞

Retrofit源码模拟

如果要进行网络请求,你可能会这样写一个简单的OKHttp请求 public class CallExector { public static fin...

22510
来自专栏java 成神之路

URI 源码分析

37915

扫码关注云+社区

领取腾讯云代金券