Struts 标签 BigPipe 专业技术实现.docx
- 文档编号:8622255
- 上传时间:2023-02-01
- 格式:DOCX
- 页数:21
- 大小:93.50KB
Struts 标签 BigPipe 专业技术实现.docx
《Struts 标签 BigPipe 专业技术实现.docx》由会员分享,可在线阅读,更多相关《Struts 标签 BigPipe 专业技术实现.docx(21页珍藏版)》请在冰豆网上搜索。
Struts标签BigPipe专业技术实现
引言
Facebook创立了一项技术名为BigPipe。
该技术改善了Facebook用户的用户体验,减少页面加载等待时间,它的原理简单、合理。
本文借鉴BigPipe的思想,针对Struts2和JSP技术的特点,实现了单线程与多线程版的BigPipe。
两种版本的实现各有优缺点,它们与Facebook的BigPipe不尽相同,其中多线程版的BigPipe实现与Facebook较为类似。
单线程与多线程实现方式都可以明显改善用户体验,文章之所以要介绍两种实现,是笔者认为二者都有更加适用的情境,在很多情况下,单线程的使用情况更多、更简单。
文章将用实际的使用实例,对两种实现进行详细的介绍。
在阅读文章之前,读者最好先了解一下Struts2自定义标签的开发方法、Java的Concurrent多线程框架以及FreeMarker模板引擎,这将帮助你更好的理解文章的BigPipe实现方式。
技术简介
现在的浏览器,显示网页时需要经历连续的几个步骤,分别是请求网页->服务器端的页面生成->返回全部内容->浏览器渲染,在这一过程中,“服务器的页面生成”到“返回全部内容”阶段,浏览器什么也不做,大部分浏览器就直接显示空白。
可想而知,如果页面庞大,那么等待的时间就很长,这很可能导致大量的用户丢失。
Facebook提出的BigPipe技术就是为了解决这个问题,它是基于多线程实现,原理大致可以分为以下两点。
将一个页面分为多个的PageLet,每个的PageLet实际上就是一个HTML片段,每个PageLet的页面内容由单独的线程生成与处理。
由于使用了多线程,PageLet内容的返回顺序无法确定,因此如果将内容直接写回HTML文档内,它的位置是无法确定的,因此需要借助JavaScript将内容插入到正确的位置,因为脚本代码的位置无关紧要。
实现了以上两点,最终的效果将是网页中首先出现网页结构和基本的、简单的信息,然后才会在网页的各个PageLet位置出现具体内容,这些PageLet没有按流模型从上到下从左到右出现,而是“并行出现”,加载页面速度加快。
从以上的分析,这种技术至少有两种好处。
首先出现的结构和基本信息,告诉用户页面正在加载,是有希望的。
并行加载的机制使得某个PageLet的缓慢不会影响到别的PageLet的加载。
所有的PageLet在同一个HTTP请求内处理。
接下来,文章先进行示例程序的展示与分析,给出各种实现方式的对比,然后讲解了基于Struts2的BigPipe标签开发,最后总结了单线程与多线程实现方式的优缺点。
示例展示
为了让读者对本文所讲内容有一个实际的印象,提升您对该技术的兴趣,本文以一个例子,采用三种实现方式来实现。
该例子实现了一个2*3的表格,按从左到右、从上到下的顺序(也就是文档流模型的加载顺序),标明了序号。
每个单元格的内容,都使用Thread.sleep方法模拟了加载时间较长的HTML内容。
按照文档流顺序,每个单元格的线程等待时间分别是1、2、3、4、5、6秒。
我们观察三种实现方式:
普通实现、单线程BigPipe、多线程BigPipe,看它们对结果的影响。
示例程序在附件部分,它是一个JEEEclipse工程,读者可以到Eclipse官方网站下载JEEEclipse,下载后导入工程。
另外运行示例程序需要Tomcat6+的支持。
普通方式
打开附件,查看WebContent下的normal.jsp源码,如清单1所示。
清单1.normal.jsp源码
<%@pagelanguage="java"contentType="text/html。
charset=utf-8"
pageEncoding="utf-8"%>
<%longpstart=System.currentTimeMillis()。
%>
<%
longstart=System.currentTimeMillis()。
Thread.sleep(1000)。
longseconds=System.currentTimeMillis()-start。
%>
1秒的内容
加载耗时:
<%=seconds%>毫秒。
//中间的省略
//...
<%
start=System.currentTimeMillis()。
Thread.sleep(6000)。
seconds=System.currentTimeMillis()-start。
%>
6秒的内容
加载耗时:
<%=seconds%>毫秒。
<%seconds=System.currentTimeMillis()-pstart。
%>
整个页面加载耗费了:
<%=seconds%>毫秒
这是一个再普通不过的JSP文件,用Thread.sleep模拟长时间的HTML加载。
运行附件工程,打开http:
//localhost:
{yourport}/BigPipeImpl/normal.jsp。
接下来等待我们的就是一个很长时间的等待,浏览器一直处于白屏的状态,最终会出现如图1的结果。
图1.普通实现方式的结果(查看大图)
普通方式的实现缺点明显,从这个例子我们就可以知道,如果你的网页很大,这将直接导致用户无法等待。
为了给出更加准确的用户等待时间,使用Firebug的网络监测功能,查看网页的加载时间,结果如图2所示。
图2.普通实现的加载时间(查看大图)
可以看到,该页面的加载时间是21.02秒,试问有哪个用户会忍受这么长时间的页面空白呢?
该实现方式的效果也在预料之内,表格按照文档流的顺序进行加载,也就是按照单元格的编号顺序逐个加载,直到页面全部加载完才一口气写回到浏览器,这样用户必须等待较长的时间。
单线程方式
普通方式的用户体验很差,要想增强用户体验就可以用到单线程BigPipe技术。
单线程的实现方式,本质上与普通方式一样,但是不一样的是它可以将优先级高的区域提前加载,并且可以先将网页结构写回客户端,然后再显示内容,增强用户体验。
本文的单线程示例程序,单元格内容的加载顺序是可以编程设置的,不一定需要按照文档流的顺序。
由于增加了客户端的JavaScript处理,在总时间上会略微多于普通方式,但是在用户体验效果却远远优于普通方式。
当我们编程设置单元格显示顺序按照1-6显示时(后半部分为展示如何设置顺序),打开http:
//localhost:
{yourport}/BigPipeImpl/single.action,效果如图3所示。
图3.单元格1-6顺序的单线程加载结果(查看大图)
可以看到,打开不久,表格的框架就显示了出来,接下来,就会逐个的显示单元格的内容,其他的单元格则显示加载状态,等到他加载完毕,我们再通过Firebug查看它的加载时间,如图4所示。
图4.单元格1-6顺序的单线程加载时间(查看大图)
可以看到,网页的加载时间与普通实现方式一样,但是却带来了普通实现方式不可比拟的用户体验,有时候用户只希望网页及时的给他回馈,让用户充满希望。
有人说,这用Ajax一样可以实现,但是请再看图4,我们看到,浏览器发出的请求只有一个single.action,再没有别的请求,这大大减轻了服务器端的压力。
又有人说,可以在每加载一个内容完毕的时候,执行flush操作。
的确,这样可以实现图3的效果,但是,如果我想实现6-1的显示顺序呢,flush就无能为力了,而用单线程BigPipe,却可以通过简单的调整代码顺序,来改变加载顺序,6-1顺序的显示结果如图5所示。
图5.单元格6-1顺序的单线程加载结果(查看大图)
从上图我们看到,这次的加载顺序,是按照6-1的显示顺序,总时间不变。
这个功能很重要,有时候,重要的内容在文档流的后方,而我们想让它显示的优先级变高,那么单线程的实现方式将非常实用。
多线程方式
不管是单线程还是普通实现方式,它们加载页面所需的总时间没有减少,对于非常大的页面,缩短加载时间才是最重要的,那么就可以使用本文介绍的多线程BigPipe技术了。
多线程实现方式与Facebook的实现方式基本一致,在本文的例子中,将每个单元格视为一个PageLet,每个PageLet的内容交给单独的线程进行生成和处理,也就是说,六个PageLet的内容并行处理,无需按照文档流顺序进行处理。
我们打开http:
//localhost:
{yourport}/BigPipeImpl/multi.action,我们再次查看页面的内容加载时间,结果如图6所示。
图6.多线程实现方式的加载时间(查看大图)
看到了吗?
总共的加载时间变为了6秒,是不是很神奇,针对本文的例子,提高了3倍多,同时也只在一个请求内完成(另外两个请求是请求JavaScript文件和图片文件的)。
而实际上,这个6秒,是加载时间最长的PageLet所需要的时间,因为各个PageLet的加载是并行的,页面加载时间以最晚的那个PageLet为准。
本文例子的加载原理如图7所示。
图7.多线程BigPipe原理
可以看到,六个单元格并行加载,整个页面的加载时间由最长的单元格6决定。
按照图7的分析,单元格是按照1-6的顺序显示,同时每个单元格之间相差接近1秒。
经验证,单元格显示的顺序的确是1-6,结果如图8所示。
图8.多线程显示结果(查看大图)
在每个单元格(也就是PageLet)显示出内容的瞬间,Firebug的网络监控部分,就会显示出当时网页所消耗的时间,结果如图9所示。
图9.每个PageLet显示的时间
可以看到,每个PageLet的显示间隔正好一秒,与图7的分析完全一致。
这也证实了多线程加载PageLet的实现是正确的。
多种实现方式的对比
从以上的示例展示和结果分析,不难看出普通实现方式、单线程BigPipe、多线程BigPipe以及Ajax之间的差异,我们不防用一个表格来展示,对比结果如表1所示,注意:
我们使用本文的示例程序作为评价背景,因为对于不同网页有可能出现不同的结果。
表1.四种实现方式对比
类型
请求数
服务器端压力
用户体验
网页加载速度
模块加载顺序
实现难度
普通
1
小
差
慢
文档流顺序
简单
Ajax
多
大
好
快
不确定
困难
单线程BigPipe
1
小
好
慢
自定义
一般
多线程BigPipe
1
一般(线程池引起)
好
最快
不确定
最困难
针对本文的例子,给出了上表的评价结果,这些结果并不是绝对的,它是针对网页较大、内容模块较多情况下给出的结果,从中可以很容易看出各个实现方式的差异所在。
读者可以从中找到符合自己需求的实现方式。
基于Struts2的标签开发
根据前面的分析,读者应该可以领略到BigPipe技术的优点了,为了让单线程和多线程版本更加实用,文章结合Struts2的标签技术,开发实现BigPipe技术,这样就可以让Java开发人员可以真正的将该技术用于实际。
因此,这部分需要大致讲解一下Struts2自定义标签的开发方法。
实现基于Struts2的标签,需要重载两个类,org.apache.struts2.views.jsp.ComponentTagSupport和ponents.Component,实现ComponentTagSupport类的getBean方法和populateParams方法,getBean方法返回自定义Component的实例,populateParams则是负责将页面传递的参数装配到Component里。
在Component类里,需要重写start和end方法(也可以不重写),这两个方法分别代表标签的起始和标签的结束。
最后再新建一个tld文件,来配置这个标签,由于篇幅限制,本文对Struts2的标签开发不多加解释,读者可以自行上网搜索相关资料。
单线程实现
还记得单线程BigPipe的实现效果吗?
它可以自定义页面模块的显示顺序。
普通的JSP文档,它显示页面的顺序,是按照文档流的顺序,也就是从上到下,从左到右的生成。
但是页面中的一些元素,我们希望它早点显示出来,但是往往它又在文档流的后半部分,前半部分耽误了很多时间,这可能直接导致用户因为看不到重要信息而不再等候,离开页面。
有一些应用,用户只希望能看到希望(页面出现内容),而我们正文的内容需要访问数据库,可能稍微慢点,因此我们可以将文档的结构先显示给用户,文档内容再慢慢填充,这听起来像Ajax,然而这不是,在BigPipe技术里,文档内容的填充只在一个请求内完成,而Ajax则可能发出多个请求,对服务器的压力较大,这在前面也已经多次提到,读者要谨记这个不同点。
单线程实现的原理是:
网页的布局仍然是不重要的在上方,重要的在下方,但是对要显示的内容进行重新排序,重要的放在文档流上方,不重要的放在后方。
当重要内容加载完之后,再使用JavaScript将内容移到原有的位置。
因此,单线程需要两个标签,一个名为bigPipeTo,是一个占位标签,在原有的位置。
一个是bigPipeFrom,它包含了需要显示的内容,它的原理图如图10所示。
图10.单线程原理图
可以看到,如果按照普通的实现方式,网页是按照PageLet1To->PageLet2To->PageLet3To->PageLet4To的顺序加载内容。
但是由于我们将内容放在了网页结构的下方,初始化为不可见,经过重新排序,顺序则变为了2->3->1->4。
bigPipeFrom标签的内容,会经过moveContent的JavaScript方法移动到对应的bigPipeTo标签的位置。
在本文的例子中,单线程的JSP使用代码如清单2所示。
清单2.单线程的JSP
<%@pagelanguage="java"contentType="text/html。
charset=utf-8"
pageEncoding="utf-8"%>
<%@taglibprefix="s"uri="/struts-tags"%>
<%@taglibprefix="b"uri="/WEB-INF/bigpipe.tld"%>
<%longpstart=System.currentTimeMillis()。
%>
bigPipeToname="index1">编号:
1
bigPipeTo>
bigPipeToname="index2">编号:
2
bigPipeTo>
bigPipeToname="index3">编号:
3
bigPipeTo>
bigPipeToname="index4">编号:
4
bigPipeTo>
bigPipeToname="index5">编号:
5
bigPipeTo>
bigPipeToname="index6">编号:
6
bigPipeTo>
bigPipeFromname="index6"bigPipeJSPath="js/bigpipe.js"> <% longstart=System.currentTimeMillis()。 Thread.sleep(6000)。 longseconds=System.currentTimeMillis()-start。 %> 6秒的内容 加载耗时: <%=seconds%>毫秒。
bigPipeFrom>
//中间的4个由于篇幅限制,省略…
bigPipeFromname="index1"bigPipeJSPath="js/bigpipe.js"> <% longstart=System.currentTimeMillis()。 Thread.sleep(1000)。 longseconds=System.currentTimeMillis()-start。 %> 1秒的内容 加载耗时: <%=seconds%>毫秒。
bigPipeFrom>…
从清单2可以看出,bigPipeFrom标签的name属性和bigPipeTo标签的name是一一对应的,这样在bigPipeFrom标签里的内容加载完以后,会准确的将内容移到对应bigPipeTo标签的位置。
bigPipeFrom标签对应类BigPipeFrom的关键代码如清单3所示。
清单3.BigPipeFrom关键代码
@Override
publicbooleanstart(finalWriterwriter){
booleanresult=super.start(writer)。
try{
writer.flush()。
//刷新显示网页结构
//用DIV包围内容
if(visiable)
{
writer.write(" none'id='"+name+"_from'>")。 }else{ writer.write(" } }catch(IOExceptione){ e.printStackTrace()。 } returnresult。 } @Override publicbooleanend(Writerwriter,Stringbody){ booleanend=super.end(writer,body)。 try{ //DIV的结束,也就是该标签里的内容加载完毕 writer.write("
//引入移动内容的JavaScript文件,就是
BigPipeWriter.instance().writeJavaScript(bigPipeJSPath,writer)。
//调用moveContent方法的脚本代码
BigPipeWriter.instance().writeFromToTo(name,writer,copy)。
}catch(Exceptione){
e.printStackTrace()。
}
returnend。
}
在清单3的start方法里,执行flush,将已经加载的内容先写回浏览器,使用一个div包含主体内容。
在end方法里,不仅要写回div的后半部分,还要将移动内容的JavaScript代码写回去。
实际上就是
其中moveContent方法就是在bigpipe.js里定义的。
为了让bigPipeFrom的内容知道要移动到什么位置,所以在bigPipeTo标签对应的BigPipeTo类里,需要用一个div包围,BigPipeTo的代码如清单4所示。
清单4.BigPipeTo关键代码
publicbooleanstart(finalWriterwriter){
booleanresult=super.start(writer)。
try{
writer.write("
}catch(IOExceptione){
e.printStackTrace()。
}
returnresult。
}
@Override
publicbooleanend(Writerwriter,Stringbody){
booleanend=super.end(writer,body)。
try{
writer.write("
}catch(IOExceptione){
e.printStackTrace()。
}
returnend。
}
最后,只要将bigPipeFrom标签里的div内容,移动到bigPipeTo的div里,就完成了。
移动div内容的JavaScript代码非常简单,代码如清单5所示。
清单5.JavaScript移动内容的代码
functionmoveContent(fromId,toId)
{
document.getElementById(toId).innerHTML=document.getElementById(fromId).innerHTML。
document.getElementById(fromId).innerHTML=""。
}
基于以上的代码实现,bigPipeFrom使用的顺序,就是网页模块PageLet加载并显示的顺序,调整代码就等同于调整了加载顺序。
达到了重要在前,次要在后的效果。
多线程实现
从单线程BigPipe的实现方式可以看出,单线程并不能解决总时间加载慢的问题,它更适合对文档内容显示按照优先级排序的需求。
而要是总体时间过慢,就要考虑多线程的实现方式。
Java中提供了Concurrent框架可以很轻松的实现多线程技术。
先将页面内容分为多个的Page
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Struts 标签 BigPipe 专业技术实现 专业技术 实现