该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2011-08-02
最后修改:2011-08-03
第二个版本了。其很多方面都有突破性的设计思路和实现方式,如异步 Action、View中读取Controller 中的本地变量、基于 javac 的动态编译、动态代码生成等等之类。正如作者 ZHH2009 所说,先不说该框架的实际发展及今后具体的应用前景如何,但是其不超过1500行的代码实现还是很值得大家去学习的。
继 ZHH2009 从09年11月发布 Douyu 的第一个版本后,至到今年6月已经发布 Douyu 的
对于 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 进行分析。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2011-08-03
分析得不错,
顺便广告一下,Douyu最快在这个月底发布一个新版本: 一些重要特性包括: 1.完全脱离servlet容器 2.一个内置http server,综合了Tomcat/Jetty/Netty的优点 3.支持AJP协议 4.全新的异步模型 @Controller public class AsyncExample { @Async public void asyncAction() { //不用新起线程或生成一个task //invokeLongtimeService } } 5.支持websocket import java.io.IOException; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import douyu.http.WebSocket; import douyu.mvc.Context; import douyu.mvc.Controller; @Controller public class WebSocketExample { public void index(Context c) { c.out("WebSocketExample.html"); } public void join(Context c) { c.setWebSocket(new MyWebSocket()); } public static class MyWebSocket implements WebSocket { private final static Set<MyWebSocket> members = new CopyOnWriteArraySet<MyWebSocket>(); private Outbound outbound; @Override public void onConnect(Outbound outbound) { this.outbound = outbound; members.add(this); } @Override public void onDisconnect() { members.remove(this); } @Override public void onMessage(int type, String data) { for (MyWebSocket member : members) { try { member.outbound.send(type, data); } catch (IOException e) { e.printStackTrace(); } } } @Override public void onMessage(int type, byte[] data) { } } } |
|
返回顶楼 | |
发表时间:2011-08-03
最后修改:2011-08-03
@ZHH2009 支持~ 发布后我将持续跟进其 Douyu 的实现及设计原理。
|
|
返回顶楼 | |
发表时间:2011-08-03
快发布吧,从2009到现在都三年了!!!!!!!!!
|
|
返回顶楼 | |
发表时间:2011-08-03
最后修改:2011-08-03
kjj 写道 快发布吧,从2009到现在都三年了!!!!!!!!!
发布啥? 引用 至到今年6月已经发布 Douyu 的第二个版本了
两个月前作者刚发布了一个版本: http://www.iteye.com/topic/1066808 |
|
返回顶楼 | |
发表时间:2011-08-03
有点意思,思想值得学习
改个名字,叫summer framework多好 |
|
返回顶楼 | |
发表时间:2011-08-03
强烈关注啊。到时候源码好好分析分析。
另外,zhh2009,能不能在douyu里加个动态语言的classloader啊,这样,就更酷了。 我一直用groovy做web,现在不想回到java里了。。。555 |
|
返回顶楼 | |
发表时间:2011-08-03
KimHo 写道 有点意思,思想值得学习
改个名字,叫summer framework多好 这个名字已经被我们公司用了很久了! |
|
返回顶楼 | |
发表时间:2011-08-03
学习了,分析的不错
|
|
返回顶楼 | |
发表时间:2011-08-04
昨天没来得急回复一些细节问题,现在补上
denger 写道 //!?? 这里直接判断不是开发模式就 Return 了,应该算不算是一个 Bug? //if(context != null && !config.isDevModel) 这样才合理吧? if (!config.isDevMode) return context; 在非开发模式(也就是生产模式),如果当前的类是一个Controller,那么它对应的Context类已经被编译好了, 如果没有Context类,说明当前的类不是一个Controller,非Controller是不允许直接从url中访问的, 所以此时context等于null,在非开发模式下就可以直接返回。 比如http://127.0.0.1:8080/java/io/File.list java.io.File类不是一个Controller,所以在非开发模式下就可以立刻返回null,然后发出404响应。 0.6.1这个版本没有提供预编译功能, 也就是在应用上线之前可以通过预编译功能把所有的java代码预编译,Controller对应的Context类也会预编译, 这个if语句也是用于此目的。 denger 写道 //!?? 不知道为什么没找到 Controller 的源码就直接Return Null了,通常正式环境下可能都不存在 源文件的。估计是生成Context时需要解析 Controller 的源码 ? if (controller.sourceFile == null) { return null; 通过上面的if (!config.isDevMode)就可以知道下面的代码都是处于开发模式下的, 一种罕见情况是开发人员只提供了controller的class文件(比如只是通过eclipse编译得来的) 此时controller.sourceFile等于null,如果没有源文件,那就得不到Context类的, 当然这种情况很少发生,不过更友好的方式应该抛出一个异常,告诉开发人员提供controller的源文件. denger 写道 // 不过当 Controller 的方法及参数较多的时候,该类生成的代码极其难看,不过考虑到该类并不需要维护,所以可以理解,只不过执行效率上是否所有影响?这个还需要做进一步探研 只有Controller的非静态public方法才可以直接从url中访问, 现实应用中这种方法的个数达到150个已经算是极端了, 我当初测试过,如果Controller有200个这种非静态public方法, 一种解决方案是用现在这种方式,也就是生成200个if-else语句, 另一种解决方案是用map方式,也就是map的key是方法名,然后把上面if-else语句中的块包装成一个类做为map的value, 当执行action时,map.get(actionName).excute(); 测试的结果发现性能没啥分别,如果方法个数少于100个,第一种方案性能更好, 如果开发人员总是把最常调用的非静态public方法放在最前面声明, 第一种方案性能更好。 当然,不管用哪种方案这里都不是性能瓶颈,因为这里不是通过反射查找action的,所以所花时间正常情况下都不会超过10毫秒。 对于参数较多的情况这个也没啥问题, 在action方法的参数中声明某个参数名和类型,就表示开发人员肯定要取得这个参数的值, 参数总要解析的,要么在action的代码中开发人员自己解析,要么提前自动解析好然后传给action。 |
|
返回顶楼 | |