Douyu0.6.1 源码分析 之 MVC篇

5 篇文章 0 订阅
5 篇文章 0 订阅
 继 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.       
  2. public void init(FilterConfig filterConfig) throws ServletException {  
  3.     //------加载基础配置信息-----  
  4.     config.appName = servletContext.getContextPath();  
  5.     //编译编码  
  6.     config.javacEncoding = filterConfig.getInitParameter("javacEncoding");   
  7.     // 源文件目录,默认为 WEB-INF/src 目录  
  8.     config.srcDir = filterConfig.getInitParameter("srcDir");   
  9.     //编译的class文件路径,默认为 WEB-INF/classes  
  10.     config.classesDir = filterConfig.getInitParameter("classesDir");   
  11.   
  12.     //------初始化视图配置信息-----  
  13.     //其配置格式为 视图处理提供类=视图扩展名  
  14.     //如:org.douyu.plugins.velocity.VelocityViewManagerProvider=vm;  
  15.     String vmpConfig = filterConfig.getInitParameter("viewManagerProviderConfig");  
  16.     if (vmpConfig == null)  
  17.         vmpConfig = viewManagerProviderConfig;  
  18.     config.setViewManagerProviderConfig(vmpConfig);  
  19.   
  20.     //------初始化自定义的 ClassLoader 类-----  
  21.     // 以当前 ClassLoader 作为父 Loader,并根据配置信息创建 ResourceLoader 实例。这里采用 Holder模式 设计。  
  22.     holder = ResourceLoader.newHolder(config, getClass().getClassLoader());  
  23. }  
关于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.   
  10.   
  11.     //第一步:从URL 中获截取出调用的 Controller 类名(包括包名) 及 调用的方法名--  
  12.     //path格式: /packageName/controllerClassName.actionName  
  13.     if (path.startsWith("/"))  
  14.         path = path.substring(1);  
  15.     String controllerClassName = path;  
  16.     String actionName = null;  
  17.     int dotPos = path.indexOf('.');//谷歌浏览器(Chrome)不支持'|'字符,所以用'."分隔类名和action名  
  18.     if (dotPos >= 0) {  
  19.         actionName = path.substring(dotPos + 1).trim();  
  20.         controllerClassName = path.substring(0, dotPos);  
  21.     }  
  22.     controllerClassName = controllerClassName.replace('/''.');  
  23.     //##执行到这时,该 actionName 为 soCool,controllerClassName 为 HelloWord  
  24.   
  25.   
  26.     //第二步:为指定的 Controller 加载对应的 Context 实例,具体请往下看对 loadContextClassResource 的分析  
  27.     StringWriter sw = new StringWriter();  
  28.     PrintWriter javacOut = new PrintWriter(sw);  
  29.     ClassResource cr = null;  
  30.     try {  
  31.         cr = holder.get().loadContextClassResource(controllerClassName, javacOut);  
  32.     } catch (JavacException e) {  
  33.         printJavacMessage(sw.toString(), response, e);  
  34.         return;  
  35.     }  
  36. }  
通过 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.   
  6.         //!?? 这里直接判断不是开发模式就 Return 了,应该算不算是一个 Bug?  
  7.         //if(context != null && !config.isDevModel)  这样才合理吧?  
  8.         if (!config.isDevMode)  
  9.             return context;  
  10.   
  11.         //2:加载 controller 的 class 对象,并找到 controller 类的源码,如果源码不存在则直接 return null。当首次请求 controller 时,其 class 文件并不存在,则调用 javac 编译其 controller 类。  
  12.         //带有@Controller标注的类(以下简称:controller类)  
  13.         //注意:事先并不知道controllerClassName是否是一个controller类,  
  14.         //所以先假定它是controller类,  
  15.         //当编译这个假想的controller类后,如果得不到对应的context类,  
  16.         //那么就返回错误(比如返回404 或 返回400(Bad request)  
  17.         ClassResource controller = loadClassResource(controllerClassName, true);  
  18.   
  19.         //3.1:context及controller都未找到,直接返回 null  
  20.         if (context == null && controller == null) {  
  21.             return null;  
  22.         } else { //找到controller类或context类其中之一,或两者都找到了  
  23.   
  24.             //3.2:controller类找不到(对应的java源文件和class文件都找不到)  
  25.             //这可能是由于误删除引起的,所以不管context类是否存在都无意义了,  
  26.             //因为context类总是要引用controller类的.  
  27.             if (controller == null) {  
  28.                 return null;  
  29.             }  
  30.   
  31.             //3.3: 找到了controler类,但是其对应的 context 类未找到  
  32.             //这通常是第一次请求controller类,此时服务器需要尝试编译它,并生成对应的context类  
  33.             else if (controller != null && context == null) {  
  34.                 //未找到controller类的java源文件  
  35.                 //!?? 不知道为什么没找到 Controller 的源码就直接Return Null了,通常正式环境下可能都不存在 源文件的。估计是生成Context时需要解析 Controller 的源码 ?  
  36.                 if (controller.sourceFile == null) {  
  37.                     return null;  
  38.                 } else {  
  39.                     // 调用 javac 编译 controller,并动态生成及编译 context 类  
  40.                     javac.compile(out, controller.sourceFile);  
  41.                     //生成及编译之后及重新调用该方法加载 context 类  
  42.                     //如果这里加载为Null, 则有可能不是效的controller类  
  43.                     return loadClassResource(contextClassName, false);  
  44.                 }  
  45.             } else { //3.4: context 和 controller 都找到了,直接返回context  
  46.                 return context;  
  47.             }  
  48.         }  
  49.     }  
至此,已经完成了 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.   
  11.         printJavacMessage(sw.toString(), response, null);  
  12.     } catch (Exception e) {  
  13.         throw new ServletException(e);  
  14.     } finally {  
  15.         if (ac != null)  
  16.             ac.free();  
  17.     }  
  18. else {  
  19.     chain.doFilter(request, response);  
  20. }  

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

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 进行分析。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值