`
denger
  • 浏览: 359016 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Douyu0.6.1 源码分析 之 MVC篇

阅读更多
      继 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 实例及 视图配置信息:
	
public void init(FilterConfig filterConfig) throws ServletException {
	//------加载基础配置信息-----
	config.appName = servletContext.getContextPath();
	//编译编码
	config.javacEncoding = filterConfig.getInitParameter("javacEncoding"); 
	// 源文件目录,默认为 WEB-INF/src 目录
	config.srcDir = filterConfig.getInitParameter("srcDir"); 
	//编译的class文件路径,默认为 WEB-INF/classes
	config.classesDir = filterConfig.getInitParameter("classesDir"); 

	//------初始化视图配置信息-----
	//其配置格式为 视图处理提供类=视图扩展名
	//如:org.douyu.plugins.velocity.VelocityViewManagerProvider=vm;
	String vmpConfig = filterConfig.getInitParameter("viewManagerProviderConfig");
	if (vmpConfig == null)
		vmpConfig = viewManagerProviderConfig;
	config.setViewManagerProviderConfig(vmpConfig);

	//------初始化自定义的 ClassLoader 类-----
	// 以当前 ClassLoader 作为父 Loader,并根据配置信息创建 ResourceLoader 实例。这里采用 Holder模式 设计。
	holder = ResourceLoader.newHolder(config, getClass().getClassLoader());
}
关于Holder模式介绍

当发起请求时,如访问 http://127.0.0.1:8080/douyu-demo/HelloWorld.soCool,其 HelloWord 类的代码如下:
@Controller
public class HelloWorld {
	public void soCool(ViewManager v) {
		// do something.....             
		v.out("hello.jsp");
	}
}

当请求该 URL 时,则先进入 ControllerFilter.doFilter,其主要代码如下:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
	String path = null;
	//异步 Action 相关
	if (request.getAttribute("javax.servlet.include.request_uri") != null)
		path = request.getAttribute("javax.servlet.include.request_uri").toString();
	// 非异步请求,获取当前请求的 URI 路径
	if (path == null)
		path = hsr.getRequestURI(); //path = hsr.getServletPath();


	//第一步:从URL 中获截取出调用的 Controller 类名(包括包名) 及 调用的方法名--
	//path格式: /packageName/controllerClassName.actionName
	if (path.startsWith("/"))
		path = path.substring(1);
	String controllerClassName = path;
	String actionName = null;
	int dotPos = path.indexOf('.');//谷歌浏览器(Chrome)不支持'|'字符,所以用'."分隔类名和action名
	if (dotPos >= 0) {
		actionName = path.substring(dotPos + 1).trim();
		controllerClassName = path.substring(0, dotPos);
	}
	controllerClassName = controllerClassName.replace('/', '.');
	//##执行到这时,该 actionName 为 soCool,controllerClassName 为 HelloWord


	//第二步:为指定的 Controller 加载对应的 Context 实例,具体请往下看对 loadContextClassResource 的分析
	StringWriter sw = new StringWriter();
	PrintWriter javacOut = new PrintWriter(sw);
	ClassResource cr = null;
	try {
		cr = holder.get().loadContextClassResource(controllerClassName, javacOut);
	} catch (JavacException e) {
		printJavacMessage(sw.toString(), response, e);
		return;
	}
}
通过 ResourceLoader 类加载器动态加载指定 Controller 对应的 AbstractContext 实现类,由于当首次请求 HelloController 时,其所对应的 AbstractContext 子类并不存在,于是便调用 javac 动态生成及编译其 AbstractContext 实现类。另外,如果是开发模式,在每次修改 Controller 类代码后,其 ResourceLoader  便会重新生成编码 Context 类及Controller类,从而实现修改代码后无须重启Server。
	public ClassResource loadContextClassResource(String controllerClassName, PrintWriter out) throws JavacException {
		// Controler 类所对应的AbstractContext实现类名, SUFFIX为 $DOUYU
		String contextClassName = controllerClassName + SUFFIX;
		// 从缓存中获取 context 类的 Class 
		ClassResource resource = classResourceCache.get(contextClassName);
		if (resource == null) {
			// 根据对应  controller 类加载对应 context 类		
			resource = loadContextClassResource(controllerClassName, contextClassName, out);
			if (resource != null) { // 将 context class resource 加入缓存
				classResourceCache.put(contextClassName, resource);
			}
		}
		if (resource != null && config.isDevMode) {
			// 如果修改过 controller 代码,将重新编译controller,并生成重新生成及编译其context类
			if (classResourceModified(out)) { 
				return copy().loadContextClassResource(controllerClassName, out);
			}
		}
		return resource;
	}

当调用 loadContextClassResource 时,首先直接根据 contextClassName 在 classpath 中找 controller的 java 文件,再调用 javac 将其编译,然后再去找该 controller 对应的 context 类,如果没有找到则根据 controller 类调用 javac 动态生成对应的 context 类代码,并将其编译。然后再使用 loadClassResource 来加载编译后的 context class 实例. 代码如下所示:
private ClassResource loadContextClassResource(String controllerClassName, String contextClassName, PrintWriter out) {
		//1:首先根据 context 类名加载 class 对象,当首次请求controller 时,因为其对应的 context java和class文件并没有生成,所以这里可能为 Null
		//带有SUFFIX后缀的类(以下简称:context类),无需加载java源文件
		ClassResource context = loadClassResource(contextClassName, false);

		//!?? 这里直接判断不是开发模式就 Return 了,应该算不算是一个 Bug?
		//if(context != null && !config.isDevModel)  这样才合理吧?
		if (!config.isDevMode)
			return context;

		//2:加载 controller 的 class 对象,并找到 controller 类的源码,如果源码不存在则直接 return null。当首次请求 controller 时,其 class 文件并不存在,则调用 javac 编译其 controller 类。
		//带有@Controller标注的类(以下简称:controller类)
		//注意:事先并不知道controllerClassName是否是一个controller类,
		//所以先假定它是controller类,
		//当编译这个假想的controller类后,如果得不到对应的context类,
		//那么就返回错误(比如返回404 或 返回400(Bad request)
		ClassResource controller = loadClassResource(controllerClassName, true);

		//3.1:context及controller都未找到,直接返回 null
		if (context == null && controller == null) {
			return null;
		} else { //找到controller类或context类其中之一,或两者都找到了

			//3.2:controller类找不到(对应的java源文件和class文件都找不到)
			//这可能是由于误删除引起的,所以不管context类是否存在都无意义了,
			//因为context类总是要引用controller类的.
			if (controller == null) {
				return null;
			}

			//3.3: 找到了controler类,但是其对应的 context 类未找到
			//这通常是第一次请求controller类,此时服务器需要尝试编译它,并生成对应的context类
			else if (controller != null && context == null) {
				//未找到controller类的java源文件
				//!?? 不知道为什么没找到 Controller 的源码就直接Return Null了,通常正式环境下可能都不存在 源文件的。估计是生成Context时需要解析 Controller 的源码 ?
				if (controller.sourceFile == null) {
					return null;
				} else {
					// 调用 javac 编译 controller,并动态生成及编译 context 类
					javac.compile(out, controller.sourceFile);
					//生成及编译之后及重新调用该方法加载 context 类
					//如果这里加载为Null, 则有可能不是效的controller类
					return loadClassResource(contextClassName, false);
				}
			} else { //3.4: context 和 controller 都找到了,直接返回context
				return context;
			}
		}
	}
至此,已经完成了 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 中的方法。
		// 在上面 第二步 中的代码通过 ResourceLoader 中的 loadContextClassResource 加载到 Controller 所对应的 Context 类,其 cr 为返回的ClassResource实现,其中存储着 Context 的 Class 实例。
		if (cr != null) {
			AbstractContext ac = null;
			try {
				// 创建 Context 实例,也就是这里的 HelloWord$DOUYU 类的实例
				ac = (AbstractContext) cr.loadedClass.newInstance();
				ac.init(config, controllerClassName, servletContext, hsr, (HttpServletResponse) response);
				// 执行 Controller 中的方法,这里也就是 soCool 方法
				ac.executeAction(actionName);

				printJavacMessage(sw.toString(), response, null);
			} catch (Exception e) {
				throw new ServletException(e);
			} finally {
				if (ac != null)
					ac.free();
			}
		} else {
			chain.doFilter(request, response);
		}

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

第四步: 以下类为在首次请求 /HelloController 时,会自动根据 HelloConroller 中的代码生成对应的Context 类代码: 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.douyu.mvc.AbstractContext;

public class HelloWorld$DOUYU extends AbstractContext
{
  // 包含当前 Context 类所对应的 Controller 类实例。可见 Douyu  中的 Controller 是多线程单实例滴...
  // 但是当前这个类(Context)是多实例的,因为其 actionName、Request、Reponse等都 是全局变量.
  private static HelloWorld _c = new HelloWorld();


  protected void executeAction() throws Exception
  {

    if (this.actionName == null) this.actionName = "index";
    // 在 Controller 中有一个 soCool 的方法,于是这里便生成了一个if判断  
    if (this.actionName.equals("soCool")) {
      checkHttpMethods(new String[] { "GET", "POST" });
      // 调用 Controller 中的方法          
      _c.soCool(this);
    }
    // 这里的 if判断,是在生成该类代码时,从获取 HelloWord 中的所有方法方法名,如果 HelloWord 这个 Controller 中有多少方法,则生成 else if 根据 actionName 调用具体的 Controller 方法。
   // 不过当 Controller 的方法及参数较多的时候,该类生成的代码极其难看,不过考虑到该类并不需要维护,所以可以理解,只不过执行效率上是否所有影响?这个还需要做进一步探研
    // 至于这些代码是如何生成的,将在下篇文章会专门分析 JAVAC 的相关代码。
    else {
      this.response.sendError(404, this.request.getRequestURI());
    }
  }
}

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

第五步:在 Controller 中的代码 v.out("hello.jsp"); 进行视图熏染,其Context会根据视图的扩展名调用相应的视图处理器,如以下是 JSP 的视图处理器的 out 实现:
@Override
	public void out(String viewFileName) {
		try {
			douyuContext.getHttpServletRequest().getRequestDispatcher(viewFileName).include(douyuContext.getHttpServletRequest(),
					douyuContext.getHttpServletResponse());
		} catch (Throwable t) {
			throw new ViewException(t);
		}
	}

之所以这里使用 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 进行分析。
  • 大小: 14.7 KB
分享到:
评论

相关推荐

    DouYu.zip_douyu___douyu\_jumayumi douyu_yubo.douyu.com

    标题中的"DouYu.zip_douyu___douyu\_jumayumi douyu_yubo.douyu.com"表明这是与斗鱼直播(DouYu)相关的项目,可能涉及到一个名为"DouYu"的软件或应用,其中包含了“jumayumi”和“yubo.douyu.com”的元素,后者是...

    仿斗鱼直播源码DouYu.zip

    通过分析和学习这套源码,开发者可以了解到微信小程序的开发流程,以及如何构建一个高效、流畅的直播应用。 【标签】"小程序"明确了此项目的技术栈,即基于微信小程序的开发框架。微信小程序的开发语言是基于...

    Douyu demo

    为了深入了解"Douyu demo",开发者需要熟悉斗鱼提供的开发者文档,理解其API接口的使用方式,并对压缩包内的源码进行阅读和分析。如果提供了构建脚本,可以运行这些脚本来编译、测试和运行项目,以实际体验其功能。...

    AJAX Douyu_0_1_0.rar

    标题"AJAX Douyu_0_1_0.rar"表明这是一个与AJAX技术相关的源码或工具项目,可能与斗鱼(Douyu)直播平台的某个版本或功能有关。AJAX,全称Asynchronous JavaScript and XML,是一种在无需刷新整个网页的情况下,能够...

    douyu案例后台模拟数据

    【描述】"douyu案例后台模拟数据"的描述虽然简洁,但我们可以从中推测出一些关键信息。首先,它可能包含了一系列用于模拟斗鱼直播后台数据的代码和结构,这些数据可能包括但不限于用户信息、直播间状态、礼物赠送...

    douyu案例demo

    lib目录下存放着源码;test目录用于存放单元测试;pubspec.yaml是Flutter项目的配置文件,列出项目依赖的库和其他元数据。此外,可能还有资源文件(如图片、字体)和构建产出的目录。 通过研究这个Demo,开发者可以...

    python 脚本Douyu.zip

    from scrapy.pipelines.images import ImagesPipeline import scrapy class DouyuPipeline(object): def process_item(self, item, spider): return item class DouyuImagePipeline(ImagesPipeline): ...

    斗鱼直播源数据的获取

    "写入txt"则表明程序会将获取的数据保存为文本文件,例如douyu_kWEG.txt和douyu_Exwh.txt,便于后续分析。 标签"爬虫"提示我们,这个过程可能使用了Python、Java或其他支持网络爬虫的编程语言。爬虫通常包括发送...

    douyu-app:使用斗鱼api和react写的douyu应用

    本项目使用 构建。 说明 本项目使用API来自斗鱼官方论坛。支持响应式。 技术栈 CSS部分:使用styled-components,css in js方案; Javascript框架:React;...使用React Redux实现全部(目前处于分支,实现中) ...

    douyu_spider:斗鱼

    douyu_spider scrapy for douyu

    ps4-irc-douyu:ps4直播douyu.tv弹幕转发服务器

    ps4-irc-douyu ps4直播douyu.tv弹幕转发服务器 把irc.twitch.tv下面4个ip做本地映射 Name: irc.twitch.tv Address: 192.16.64.11 Name: irc.twitch.tv Address: 192.16.64.145 Name: irc.twitch.tv Address: 192.16....

    Douyu-danmu-spark:在Douyu_TV直播节目中抓取主持人的danmu信息,并通过SPARK和一些大数据技术进行相应的统计分析

    斗鱼旦木火花版本3.0 ||最终版本介绍与第一个版本的相比,在此存储库中,对Douyu_TV的danmu的分析基于SPARK而不是MYSQL(Pymysql)。环境: Python 3.6 wordcloud模块解霸斯派德火花(Pyspark) Windows10(64位)...

    【PHP直播导航】最新版PHP聚合直播系统斗鱼直播|虎牙直播|电视台导播|上传即用无后台无数据库

    6. **源码开放**:标签中提到的“源码”表明该系统是开源的,开发者可以深入研究其内部结构,学习PHP编程技巧,甚至根据源码进行二次开发和功能扩展。 在实际使用过程中,用户首先需要下载提供的压缩包【30054】...

    candyjs:斗鱼开源 Node MVC 框架

    这是它的编程哲学CandyJs 采用 MIT 许可 这意味着您可以免费的使用 CandyJs 来开发 WEB 应用文档最新文档请参阅源码的 doc 目录源码 source codeNode 版本8.0.0 +Hello world使用 CandyJs 你只需要从一个入口文件...

    douyu-crawler-demo::smiling_face_with_heart-eyes: Go 开发的 Demo 程序用于演示如何解决字体反爬从而爬取斗鱼主播「关注人数」

    斗鱼关注人数爬虫 Demo,具体可以参考这篇博客 。 注意:爬虫程序有很高的时效性,很快就会过时无法使用。Demo 最后测试时间为 2020-07-02 日。 安装 $ go get -v github.com/cj1128/douyu-crawler-demo 爬取主播...

    douyu:斗鱼SDK golang版本

    斗鱼SDK入门指南package mainimport "github.com/JX3BOX/douyu"func main (){ dy , err := douyu . New ( "AID" , "Key" ) if err != nil { log . Fatalln ( err ) Fatalln } list , err := dy . BatchGetRoomInfo ...

    copy-douyu-jupiter:抄一遍框架

    "copy-douyu-jupiter:抄一遍框架"项目是一个学习性质的开源工程,目标是通过复制 Douyu Jupiter 框架的源代码,帮助开发者深入理解框架的工作原理和设计思想。Jupiter 是斗鱼公司开源的一款轻量级服务治理框架,它...

    douyu_client_120_0v6_3_1_0.exe

    斗鱼客户端,强大而稳定的客户端

    教育学习-抖云先行官方免费版 v1.4.8 安卓版com.douyu.zip

    《抖云先行官方免费版 v1.4.8 安卓版com.douyun.zip》是一款专注于教育学习的安卓应用程序,旨在为用户提供便捷、高效的学习平台。该软件由抖音旗下的团队精心打造,集成了丰富的教育资源,涵盖了多个学科领域,旨在...

Global site tag (gtag.js) - Google Analytics