继 ZHH2009 从09年11月发布 Douyu 的第一个版本后,至到今年6月已经发布 Douyu 的第二个版本了。其很多方面都有突破性的设计思路和实现方式,如异步 Action、View中读取Controller 中的本地变量、基于 javac 的动态编译、动态代码生成等等之类。正如作者 ZHH2009 所说,先不说该框架的实际发展及今后具体的应用前景如何,但是其不超过1500行的代码实现还是很值得大家去学习的。 
     对于 Douyu 的学习,我将从以下三个方面来进行入手。

  • MVC 篇: 也就是本文所需要讲的。主要分析其 MVC 实现及请求处理流程等。
  • javac 篇: 在 Douyu第二个版本中作者提到了其最炫的功能。在 View 中直接访问 Action 中的本地变量,Modle注入等,所以我将从源码分析该实现方式。并且会结合其"无需打包、部署,无需重启Servlet容器"的实现原理进行分析。
  • 异步Acton篇:将结合 Tomcat7.0 及 Servlet3.0 对Douyu的异步Action的实现机制进行分析。

当然,如果你觉Douyu的哪些地方还可以值得学习分析的欢迎评论中补充。

源码分析 
首先是当服务器启动时,初始化ControllerFilter ,调用 init 方法,主要用于初始化一些参数信息、ResourceLoader 实例及 视图配置信息:

Java代码  
  1. public void init(FilterConfig filterConfig) throws ServletException {
  2. //------加载基础配置信息-----
  3. config.appName = servletContext.getContextPath();
  4. //编译编码
  5. config.javacEncoding = filterConfig.getInitParameter("javacEncoding");
  6. // 源文件目录,默认为 WEB-INF/src 目录
  7. config.srcDir = filterConfig.getInitParameter("srcDir");
  8. //编译的class文件路径,默认为 WEB-INF/classes
  9. config.classesDir = filterConfig.getInitParameter("classesDir");
  10. //------初始化视图配置信息-----
  11. //其配置格式为 视图处理提供类=视图扩展名
  12. //如:org.douyu.plugins.velocity.VelocityViewManagerProvider=vm;
  13. String vmpConfig = filterConfig.getInitParameter("viewManagerProviderConfig");
  14. if (vmpConfig == null)
  15. vmpConfig = viewManagerProviderConfig;
  16. config.setViewManagerProviderConfig(vmpConfig);
  17. //------初始化自定义的 ClassLoader 类-----
  18. // 以当前 ClassLoader 作为父 Loader,并根据配置信息创建 ResourceLoader 实例。这里采用 Holder模式 设计。
  19. holder = ResourceLoader.newHolder(config, getClass().getClassLoader());
  20. }

关于Holder模式介绍

当发起请求时,如访问 http://127.0.0.1:8080/douyu-demo/HelloWorld.soCool,其 HelloWord 类的代码如下:

Java代码  
  1. @Controller
  2. public class HelloWorld {
  3. public void soCool(ViewManager v) {
  4. // do something.....
  5. v.out("hello.jsp");
  6. }
  7. }

当请求该 URL 时,则先进入 ControllerFilter.doFilter,其主要代码如下:

Java代码  
  1. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
  2. String path = null;
  3. //异步 Action 相关
  4. if (request.getAttribute("javax.servlet.include.request_uri") != null)
  5. path = request.getAttribute("javax.servlet.include.request_uri").toString();
  6. // 非异步请求,获取当前请求的 URI 路径
  7. if (path == null)
  8. path = hsr.getRequestURI(); //path = hsr.getServletPath();
  9. //第一步:从URL 中获截取出调用的 Controller 类名(包括包名) 及 调用的方法名--
  10. //path格式: /packageName/controllerClassName.actionName
  11. if (path.startsWith("/"))
  12. path = path.substring(1);
  13. String controllerClassName = path;
  14. String actionName = null;
  15. int dotPos = path.indexOf('.');//谷歌浏览器(Chrome)不支持'|'字符,所以用'."分隔类名和action名
  16. if (dotPos >= 0) {
  17. actionName = path.substring(dotPos + 1).trim();
  18. controllerClassName = path.substring(0, dotPos);
  19. }
  20. controllerClassName = controllerClassName.replace('/', '.');
  21. //##执行到这时,该 actionName 为 soCool,controllerClassName 为 HelloWord
  22. //第二步:为指定的 Controller 加载对应的 Context 实例,具体请往下看对 loadContextClassResource 的分析
  23. StringWriter sw = new StringWriter();
  24. PrintWriter javacOut = new PrintWriter(sw);
  25. ClassResource cr = null;
  26. try {
  27. cr = holder.get().loadContextClassResource(controllerClassName, javacOut);
  28. } catch (JavacException e) {
  29. printJavacMessage(sw.toString(), response, e);
  30. return;
  31. }
  32. }

通过 ResourceLoader 类加载器动态加载指定 Controller 对应的 AbstractContext 实现类,由于当首次请求 HelloController 时,其所对应的 AbstractContext 子类并不存在,于是便调用 javac 动态生成及编译其 AbstractContext 实现类。另外,如果是开发模式,在每次修改 Controller 类代码后,其 ResourceLoader  便会重新生成编码 Context 类及Controller类,从而实现修改代码后无须重启Server。

Java代码  
  1. public ClassResource loadContextClassResource(String controllerClassName, PrintWriter out) throws JavacException {
  2. // Controler 类所对应的AbstractContext实现类名, SUFFIX为 $DOUYU
  3. String contextClassName = controllerClassName + SUFFIX;
  4. // 从缓存中获取 context 类的 Class
  5. ClassResource resource = classResourceCache.get(contextClassName);
  6. if (resource == null) {
  7. // 根据对应  controller 类加载对应 context 类
  8. resource = loadContextClassResource(controllerClassName, contextClassName, out);
  9. if (resource != null) { // 将 context class resource 加入缓存
  10. classResourceCache.put(contextClassName, resource);
  11. }
  12. }
  13. if (resource != null && config.isDevMode) {
  14. // 如果修改过 controller 代码,将重新编译controller,并生成重新生成及编译其context类
  15. if (classResourceModified(out)) {
  16. return copy().loadContextClassResource(controllerClassName, out);
  17. }
  18. }
  19. return resource;
  20. }

当调用 loadContextClassResource 时,首先直接根据 contextClassName 在 classpath 中找 controller的 java 文件,再调用 javac 将其编译,然后再去找该 controller 对应的 context 类,如果没有找到则根据 controller 类调用 javac 动态生成对应的 context 类代码,并将其编译。然后再使用 loadClassResource 来加载编译后的 context class 实例. 代码如下所示:

Java代码  
  1. private ClassResource loadContextClassResource(String controllerClassName, String contextClassName, PrintWriter out) {
  2. //1:首先根据 context 类名加载 class 对象,当首次请求controller 时,因为其对应的 context java和class文件并没有生成,所以这里可能为 Null
  3. //带有SUFFIX后缀的类(以下简称:context类),无需加载java源文件
  4. ClassResource context = loadClassResource(contextClassName, false);
  5. //!?? 这里直接判断不是开发模式就 Return 了,应该算不算是一个 Bug?
  6. //if(context != null && !config.isDevModel)  这样才合理吧?
  7. if (!config.isDevMode)
  8. return context;
  9. //2:加载 controller 的 class 对象,并找到 controller 类的源码,如果源码不存在则直接 return null。当首次请求 controller 时,其 class 文件并不存在,则调用 javac 编译其 controller 类。
  10. //带有@Controller标注的类(以下简称:controller类)
  11. //注意:事先并不知道controllerClassName是否是一个controller类,
  12. //所以先假定它是controller类,
  13. //当编译这个假想的controller类后,如果得不到对应的context类,
  14. //那么就返回错误(比如返回404 或 返回400(Bad request)
  15. ClassResource controller = loadClassResource(controllerClassName, true);
  16. //3.1:context及controller都未找到,直接返回 null
  17. if (context == null && controller == null) {
  18. return null;
  19. } else { //找到controller类或context类其中之一,或两者都找到了
  20. //3.2:controller类找不到(对应的java源文件和class文件都找不到)
  21. //这可能是由于误删除引起的,所以不管context类是否存在都无意义了,
  22. //因为context类总是要引用controller类的.
  23. if (controller == null) {
  24. return null;
  25. }
  26. //3.3: 找到了controler类,但是其对应的 context 类未找到
  27. //这通常是第一次请求controller类,此时服务器需要尝试编译它,并生成对应的context类
  28. else if (controller != null && context == null) {
  29. //未找到controller类的java源文件
  30. //!?? 不知道为什么没找到 Controller 的源码就直接Return Null了,通常正式环境下可能都不存在 源文件的。估计是生成Context时需要解析 Controller 的源码 ?
  31. if (controller.sourceFile == null) {
  32. return null;
  33. } else {
  34. // 调用 javac 编译 controller,并动态生成及编译 context 类
  35. javac.compile(out, controller.sourceFile);
  36. //生成及编译之后及重新调用该方法加载 context 类
  37. //如果这里加载为Null, 则有可能不是效的controller类
  38. return loadClassResource(contextClassName, false);
  39. }
  40. } else { //3.4: context 和 controller 都找到了,直接返回context
  41. return context;
  42. }
  43. }
  44. }

至此,已经完成了 Context 类的加载。当首次请求完之后,则可以看到 WEB-INF/classes 中生成三个文件: 
 
其中的 HelloController.class 为一开始自己写的 Controller 类,其HelloWorld$DOUYU.java 及 HelloWorld$DOUYU.class 为该 Controller 对应的 Context 类。至于 Context 中的代码及作用在下面会进行分析。另外,关于 Douyu 如何调用javac实现自动生成 Context 的代码,其具体分析会在下篇文章中,也就是上面说的 javac 篇。有兴趣的可以去看看: com.sun.tools.javac.processing.ControllerProcessor 代码。 
到这里,值的一提的是,虽然已经完成 Context 代码生成及编译,但是这里最终返回的是 ClassResource 对象,该对象通过 Class<?> loadClass 变量存储其 Context 的 Class 实例。于是,这便意味着需要在运行时加载 Context 的class字节码,为其生成 Class 对象。 
Douyu 对动态 Class 加载,主要通过 org.douyu.core.ResourceLoader.findClassOrClassResource(String name, boolean resolve, boolean findJavaSourceFile) 方法实现。如果你了解 ClassLoader 机制话,你并不会陌生其实现机制,并且该方法的注释也非常详细。

第三步:到这步为止,其 Context 、Controller都已经生成并编译完成,并且已经获取 Context 的 Class 实例。继续回到 org.douyu.mvc.ControllerFilter.doFilter 的代码分析。这里先是获取 Context 实例,调用其中的 executAction 执行 Controller 中的方法。 

Java代码  
  1. // 在上面 第二步 中的代码通过 ResourceLoader 中的 loadContextClassResource 加载到 Controller 所对应的 Context 类,其 cr 为返回的ClassResource实现,其中存储着 Context 的 Class 实例。
  2. if (cr != null) {
  3. AbstractContext ac = null;
  4. try {
  5. // 创建 Context 实例,也就是这里的 HelloWord$DOUYU 类的实例
  6. ac = (AbstractContext) cr.loadedClass.newInstance();
  7. ac.init(config, controllerClassName, servletContext, hsr, (HttpServletResponse) response);
  8. // 执行 Controller 中的方法,这里也就是 soCool 方法
  9. ac.executeAction(actionName);
  10. printJavacMessage(sw.toString(), response, null);
  11. } catch (Exception e) {
  12. throw new ServletException(e);
  13. } finally {
  14. if (ac != null)
  15. ac.free();
  16. }
  17. } else {
  18. chain.doFilter(request, response);
  19. }

可以看到,上面Filter 中代码从来都没有调用过 Controller 中的方法,也就是这里的 HelloController.soCool方法,而调用的是 Context 类的 executeAction 然后传入需要调用的方法,也就是这里的 HelloController$DOUYU.executeAction 方法。 
OK,那接着看第四步对 Context 的代码的分析。

第四步: 以下类为在首次请求 /HelloController 时,会自动根据 HelloConroller 中的代码生成对应的Context 类代码:  

Java代码  
  1. import javax.servlet.http.HttpServletRequest;
  2. import javax.servlet.http.HttpServletResponse;
  3. import org.douyu.mvc.AbstractContext;
  4. public class HelloWorld$DOUYU extends AbstractContext
  5. {
  6. // 包含当前 Context 类所对应的 Controller 类实例。可见 Douyu  中的 Controller 是多线程单实例滴...
  7. // 但是当前这个类(Context)是多实例的,因为其 actionName、Request、Reponse等都 是全局变量.
  8. private static HelloWorld _c = new HelloWorld();
  9. protected void executeAction() throws Exception
  10. {
  11. if (this.actionName == null) this.actionName = "index";
  12. // 在 Controller 中有一个 soCool 的方法,于是这里便生成了一个if判断
  13. if (this.actionName.equals("soCool")) {
  14. checkHttpMethods(new String[] { "GET", "POST" });
  15. // 调用 Controller 中的方法
  16. _c.soCool(this);
  17. }
  18. // 这里的 if判断,是在生成该类代码时,从获取 HelloWord 中的所有方法方法名,如果 HelloWord 这个 Controller 中有多少方法,则生成 else if 根据 actionName 调用具体的 Controller 方法。
  19. // 不过当 Controller 的方法及参数较多的时候,该类生成的代码极其难看,不过考虑到该类并不需要维护,所以可以理解,只不过执行效率上是否所有影响?这个还需要做进一步探研
  20. // 至于这些代码是如何生成的,将在下篇文章会专门分析 JAVAC 的相关代码。
  21. else {
  22. this.response.sendError(404, this.request.getRequestURI());
  23. }
  24. }
  25. }

Ok, 至于,已经完成从 请求至执行 Controller 中的方法代码分析,接下来最后一步便是关于视图的处理。

第五步:在 Controller 中的代码 v.out("hello.jsp"); 进行视图熏染,其Context会根据视图的扩展名调用相应的视图处理器,如以下是 JSP 的视图处理器的 out 实现:

Java代码  
  1. @Override
  2. public void out(String viewFileName) {
  3. try {
  4. douyuContext.getHttpServletRequest().getRequestDispatcher(viewFileName).include(douyuContext.getHttpServletRequest(),
  5. douyuContext.getHttpServletResponse());
  6. } catch (Throwable t) {
  7. throw new ViewException(t);
  8. }
  9. }

之所以这里使用 JSP 的 include 而不是 forward,其目的之一是,作者所说的:

引用
3. 支持Velocity、FreeMaker,集成其他模板引擎也是非常简单,多种模板引擎可以在同一个应用中同时使用。

另外,对于视图中的变量处理,同样会在 javac 文章进行分析。 
其Douyu对 视图处理是整个框架不错的设计地方之一,很大程度上解决 View 、Model、Servlet、 Controller 之间的耦合度,还提供了 View 扩展的API,对于增加新的视图实现也极其简单。 并且对视图管理类的创建采用了  Provider 设计模式,从而可以同一类视图,支持多种处理模式。

目前该框好象没有对 Session 及 Application 作用域进行考虑,应该需要结合 AbstarctContext 设计,不过目前对于框架本身的设计,解决这个也不是什么问题。

OK,至此已经完成了Douyu 整个 MVC 请求的处理流程的代码分析,其分析程度还较浅。如果觉得有什么问题或想讨论该框架的设计,欢迎下面评论进行讨论。因目前还在对Douyu源码进行Debug,所以将下一文章将会结合 javac 实现更深一步对 Douyu 进行分析。

Douyu0.6.1 源码分析 之 MVC篇相关推荐

  1. Spring 源码分析(四) ——MVC(二)概述

    随时随地技术实战干货,获取项目源码.学习资料,请关注源代码社区公众号(ydmsq666) from:Spring 源码分析(四) --MVC(二)概述 - 水门-kay的个人页面 - OSCHINA ...

  2. hadoop作业初始化过程详解(源码分析第三篇)

    (一)概述 我们在上一篇blog已经详细的分析了一个作业从用户输入提交命令到到达JobTracker之前的各个过程.在作业到达JobTracker之后初始化之前,JobTracker会通过submit ...

  3. Kubernetes Node Controller源码分析之配置篇

    2019独角兽企业重金招聘Python工程师标准>>> Author: xidianwangtao@gmail.com Kubernetes Node Controller源码分析之 ...

  4. JUC源码分析-线程池篇(五):ForkJoinPool - 2

    通过上一篇(JUC源码分析-线程池篇(四):ForkJoinPool - 1)的讲解,相信同学们对 ForkJoinPool 已经有了一个大概的认识,本篇我们将通过分析源码的方式来深入了解 ForkJ ...

  5. photoshop-v.1.0.1源码分析第三篇–FilterInterface.p

    photoshop-v.1.0.1源码分析第三篇–FilterInterface.p 总体预览 一.源码预览 二.语法解释 三.结构预览 四:语句分析 五:思维导图 六:疑留问题 一.源码预览 {Ph ...

  6. hadoop之MapReduce框架TaskTracker端心跳机制分析(源码分析第六篇)

    1.概述 MapReduce框架中的master/slave心跳机制是整个集群运作的基础,是沟通TaskTracker和JobTracker的桥梁.TaskTracker周期性地调用心跳RPC函数,汇 ...

  7. 源码分析Dubbo前置篇-寻找注册中心、服务提供者、服务消费者功能入口

    本节主要阐述如下两个问题:  1.Dubbo自定义标签实现.  2.dubbo通过Spring加载配置文件后,是如何触发注册中心.服务提供者.服务消费者按照Dubbo的设计执行相关的功能.  所谓的执 ...

  8. Vue.js 源码分析(九) 基础篇 生命周期详解

    先来看看官网的介绍: 主要有八个生命周期,分别是: beforeCreate.created.beforeMount.mounted.beforeupdate.updated   .beforeDes ...

  9. Vue.js 源码分析(五) 基础篇 方法 methods属性详解

    methods中定义了Vue实例的方法,官网是这样介绍的: 例如:: <!DOCTYPE html> <html lang="en"> <head&g ...

  10. v74.01 鸿蒙内核源码分析(编码方式篇) | 机器指令是如何编码的 | 百篇博客分析OpenHarmony源码

    Python微信订餐小程序课程视频 https://blog.csdn.net/m0_56069948/article/details/122285951 Python实战量化交易理财系统 https ...

最新文章

  1. 局域网怎样自动安装FLASH插件(浏览器不安装flashplayer都可以浏览.swf文件)
  2. github+picGo+jsDelivr构建图床
  3. 聊聊kafka client chunkQueue 与 MaxLag值
  4. rabbitmq集群报错
  5. python中如果要多次输入文本,关于文本游戏:文本游戏 – 如果语句基于输入文本 – Python...
  6. 容联CTO许志强:AI、5G让通讯更智能、更高效
  7. Thymeleaf select 使用 和多select 级联选择
  8. 1小时搞懂设计模式之工厂模式(简单工厂)
  9. 如何在小程序里面放入企业官网
  10. C#实现图像下一张上一张
  11. OpenLayers 6 实现仿Echarts风格的动态迁徙图/航班图
  12. kali自定义分辨率
  13. Android Glide清除缓存图片 你可能不知道
  14. 如何更改python界面颜色_pycharm修改界面主题颜色的方法 pycharm怎么恢复默认设置...
  15. 什么是二极管钳位电路
  16. adobe illustrator软件能做什么
  17. python 爬去拉钩测试招聘信息
  18. css-图片模糊处理-blur
  19. Matplotlib-散点图详解
  20. Java操作Redis客户端

热门文章

  1. css文字跑马灯,css3实现文字跑马灯(css3跑马灯demo) - 全文
  2. 何为自动化测试?(纯干货)
  3. 【推荐系统】推荐算法系列之DSSM双塔模型:Deep Structured Semantic Models for Web Search using Clickthrough Data
  4. jQuery的五种初始化加载写法
  5. 51单片机学习历程——建立新的工程
  6. 计算机教育课题申请报告,课题结项申请报告
  7. 目标检测 YOLO 系列:快速迭代 YOLO v5
  8. 云原生小课堂 | Envoy请求流程源码解析(一):流量劫持
  9. python pygame字体设置_2015/11/3用Python写游戏,pygame入门(3):字体模块、事件显示和错误处理...
  10. 阿里云服务器验证码不能显示解决办法java.lang.Error: Probable fatal error:No fonts found