基于NodeJS的前后端分离时代的到来,传统的后端Freemarker是否可能做得更好呢?这篇文章主要介绍作者基于Spring MVC对BigPipe的实践,下面直接进入正题。
普通方式
对于复杂页面我们常常会将页面分成若干模块,每个模块对应的业务代码耦合相对较少,这样易于开发和维护。为了简化模型,我们举个简单的例子,比如下面这个页面。
这个页面很简单,主要包含模块1和模块2这两个页面,我们假设除了模块1和模块2都是静态DOM,并且假设模块1为获取数据所需要的准备时间是1000ms,模块2为获取数据所需要的准备时间为800ms,那么一次请求过来,我们得到下面这张执行图:
显然,当用户请求这个页面的时候需要至少等待1000ms+800ms=1800ms的白屏时间,这里没有计算其他很少的耗时,原因是当服务器使用contentLength方式来决定是否将内容刷新到客户端的时候,客户端在所有数据准备好之前不会得到任何内容。而chunked编码方式方式允许服务器端分块将内容返回到客户端。
基于Spring MVC的实现
上面提到的模型基于Spring MVC实现只需要一个Controller加上一小块模板就可以表达了。我们姑且叫它OldController:
@Controller @RequestMapping("old") public class OldController { @RequestMapping("index") public String index(Model model, String name){ model.addAttribute("index", name); //返回数据和一个模板统一渲染,然后返回 model.addAttribute("module1", module1(name)); model.addAttribute("module2", module2(name)); return "old/index"; } public String module1(String name){ try { Thread.sleep(1000);//业务逻辑 } catch (Exception e) { } //返回数据 return name; } public String module2(String name){ try { Thread.sleep(800);//业务逻辑 } catch (Exception e) { } //返回数据 return name; } }
Controller的实现很简单,我们姑且用sleep来模拟数据的准备时间,至于模板呢,也很简单:
${index!} 模块1的内容 ${module1!} 模块2的内容 ${module2!}
三块内容,我们启动服务测试一下:
借助浏览器工具我们看下请求时间相关的数据:
发送请求花了0.22ms,我们关心的是客户端等待时间Waiting(TTFB)为1.89s,内容回传用了2.65ms,总耗时1.92秒。
如何改进呢,我想用下面的图解释一下我对Spring MVC的改进:
这幅图左边是老的Spring MVC渲染方式,右边是新的模型,我们把每个模块对应的业务逻辑提取为一个RequestMapping,让这些RequestMapping并发的去准备数据和渲染,结果用chunked方式刷回客户端。
基于Spring MVC CO的BigPipe方式的实现
在完成了Spring MVC CO这个工具之后我们的这个页面对应的Controller和模板可以像下面这样写:
Controller:
/** * @author float.lu */ @Controller @RequestMapping("co") public class CoController { @RequestMapping("index") public String index(Model model, String name){ model.addAttribute("index", name); //返回数据和模板,同步返回 return "co/index"; } //ModelAndView并发执行和渲染 @RequestMapping("module1") public String module1(Model model, String name){ model.addAttribute("module1", name); try { //业务逻辑 Thread.sleep(1000); } catch (Exception e) { } //模块模板 return "co/module1"; } //ModelAndView并发执行和渲染 @RequestMapping("module2") public String module2(Model model, String name){ model.addAttribute("module2",name); try { //业务逻辑 Thread.sleep(800); } catch (Exception e) { } //模块模板 return "co/module2"; } }
解释下,之前这个页面整体渲染的时候只需要一个RequestMapping,而这个时候我们除了为主页面配置一个RequestMapping之外还需要给每一个模块配置一个RequestMapping,每个RequestMapping的写法和原生Spring MVC的写法并没有什么不同,每个RequestMapping返回的ModelAndView也和原生的Spring MVC写法没有不同。不同的是这些RequestMapping对应的是页面上的模块,和主页面的请求共享一个Request,而不是来源于客户端的真正的Request。下面来看看如何编写主请求对应的模板:
其实很简单:
<#assign co=JspTaglibs["http://www.springframework.org/co"]> <@co.config timeout=3000/> ${index!} 模块1: <@co.module mapping="/co/module1"/> 模块2: <@co.module mapping="/co/module2" />
第一行<#assign co=JspTaglibs["http://www.springframework.org/co"]>是引入JSP标签库,接着<@co.config timeout=3000/>是设置模块的超时时间,如果超过这个时间我们可以放弃这个模块了以便释放连接。<@co.module mapping="/co/module1"/>这句是引入模块,其中/co/module1是和Controller中的RequestMapping对应的。令人兴奋的是我们也支持PathVariable方式传参,但是一个请求上下文我们不能有同名的PathVariable参数。
同时Spring MVC配置文件可能需要改动一下:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc/co" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc/co http://www.springframework.org/schema/mvc/spring-mvc-co.xsd"> <mvc:annotation-driven> </mvc:annotation-driven></beans>
仅仅是将MVC的命名空间地址换了一下,其他不需要任何改动。
启动服务器请求一个我们再看看响应时间数据:
从上面的数据我们可以看出来服务器的响应时间变成了13.02ms,中间的1.01s包括第一个响应chunk到最后一个chunk下载完的总时间,而页面的总响应时间为1.04秒,几乎也近处理最慢的那个模块的时间了,而这个时候其实前端的样式准备从13.02ms就已经开始了,而不是让用户一直面对白屏。
关于BigPipe
BigPipe概念很早就出现了,目前业界也有很多种实现,它带来的好处主要是通过长连接方式让先完成的数据返回给用户以优化用户体验,同时减少请求次数,达到本来多次请求合并成一个请求的效果。作者仅仅是在Spring MVC的基础之上实现这一功能,感谢Spring开源的支持。原理落地,没有最好的设计,只有最好的实践,也希望以后能有更多的实践。
项目地址:http://git.oschina.net/floatlu/spring-mvc-co
欢迎一起改进。