1 示例
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-language" content="de" />
<script type="text/javascript">function arrived(id,text) { var b=document.getElementById(id); b.innerHTML = text; }</script></HEAD><BODY><div>Progressive Loading<div id="content1">-</div>
<div id="content2">-</div>
<div id="content3">-</div>
<div id="content4">-</div>
<div id="content5">-</div>
<div id="content6">-</div>
</div>
<script>arrived("content1", "Wohooo1");</script>
<script>arrived("content2", "Wohooo2");</script>
<script>arrived("content5", "Wohooo5");</script>
</BODY></HTML>
先輸出架構和占位符,然後服務端分步輸出腳本,用腳本渲染html
2 flush最終渲染結果是由tomcat容器做的,在 org.apache.catalina.connector.CoyoteAdapter.service(Request, Response)
/**
* End request.
*
* @throws IOException an underlying I/O error occurred
*/
@Override
public void endRequest()
throws IOException {
super.endRequest();
if (useSocketBuffer) {
socketBuffer.flushBuffer();
}
}
@Override
public void service(org.apache.coyote.Request req,
org.apache.coyote.Response res)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// Create objects
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);
// Link objects
request.setResponse(response);
response.setRequest(request);
// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);
// Set query string encoding
req.getParameters().setQueryStringEncoding
(connector.getURIEncoding());
}
if (connector.getXpoweredBy()) {
response.addHeader("X-Powered-By", POWERED_BY);
}
boolean comet = false;
boolean async = false;
try {
// Parse and set Catalina and configuration specific
// request parameters
req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
boolean postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
if (request.isComet()) {
if (!response.isClosed() && !response.isError()) {
if (request.getAvailable() || (request.getContentLength() > 0 && (!request.isParametersParsed()))) {
// Invoke a read event right away if there are available bytes
if (event(req, res, SocketStatus.OPEN)) {
comet = true;
res.action(ActionCode.COMET_BEGIN, null);
}
} else {
comet = true;
res.action(ActionCode.COMET_BEGIN, null);
}
} else {
// Clear the filter chain, as otherwise it will not be reset elsewhere
// since this is a Comet request
request.setFilterChain(null);
}
}
}
AsyncContextImpl asyncConImpl = (AsyncContextImpl)request.getAsyncContext();
if (asyncConImpl != null) {
async = true;
} else if (!comet) {
request.finishRequest();
response.finishResponse();
if (postParseSuccess &&
request.getMappingData().context != null) {
// Log only if processing was invoked.
// If postParseRequest() failed, it has already logged it.
// If context is null this was the start of a comet request
// that failed and has already been logged.
((Context) request.getMappingData().context).logAccess(
request, response,
System.currentTimeMillis() - req.getStartTime(),
false);
}
req.action(ActionCode.POST_REQUEST , null);
}
} catch (IOException e) {
// Ignore
} finally {
req.getRequestProcessor().setWorkerThreadName(null);
// Recycle the wrapper request and response
if (!comet && !async) {
request.recycle();
response.recycle();
} else {
// Clear converters so that the minimum amount of memory
// is used by this processor
request.clearEncoders();
response.clearEncoders();
}
}
}
而socket的關閉也是在tomcat裡的連接配接器裡
org.apache.tomcat.util.net.JIoEndpoint.SocketProcessor.run()
boolean launch = false;
synchronized (socket) {
try {
SocketState state = SocketState.OPEN;
try {
// SSL handshake
serverSocketFactory.handshake(socket.getSocket());
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
if (log.isDebugEnabled()) {
log.debug(sm.getString("endpoint.err.handshake"), t);
}
// Tell to close the socket
state = SocketState.CLOSED;
}
if ((state != SocketState.CLOSED)) {
if (status == null) {
state = handler.process(socket, SocketStatus.OPEN);
} else {
state = handler.process(socket,status);
}
}
if (state == SocketState.CLOSED) {
// Close socket
if (log.isTraceEnabled()) {
log.trace("Closing socket:"+socket);
}
countDownConnection();
try {
socket.getSocket().close();
} catch (IOException e) {
// Ignore
}
} else if (state == SocketState.OPEN ||
state == SocketState.UPGRADING ||
state == SocketState.UPGRADED){
socket.setKeptAlive(true);
socket.access();
launch = true;
} else if (state == SocketState.LONG) {
socket.access();
waitingRequests.add(socket);
}
} finally {
if (launch) {
try {
getExecutor().execute(new SocketProcessor(socket, SocketStatus.OPEN));
} catch (NullPointerException npe) {
if (running) {
log.error(sm.getString("endpoint.launch.fail"),
npe);
}
}
}
}
}
socket = null;
// Finish up this request
3 bigpipe關鍵步驟是分步輸出字元流,在一個連接配接中,一次http請求-響應中分步輸出,要保證所有内容都輸出後,才能關閉輸出流和socket。
如果是單線程的話,不會有任何問題;如果是多線程處理同一個請求的話,要用一些同步工具,比如latch或者barrier。
參考 http://www.searchtb.com/2011/04/an-introduction-to-bigpipe.html
http://codemonkeyism.com/facebook-bigpipe-java/
4 将bigpipe內建到webx裡面,首先是頁面架構
<script type="text/javascript">function arrivedHtml(id,text) { var b=document.getElementById(id); b.innerHTML = text; }</script></HEAD><BODY>
<div>Progressive Loading
<div class="pagelet" id="content1">-</div>
<div class="pagelet" id="content2">-</div>
<div class="pagelet" id="content3">-</div>
<div class="pagelet" id="content4">-</div>
</div>
然後分别定義每個content頁面,如content1
hello world $name
編寫bigpipevalve
package com.alibaba.webx.tutorial1.common;
import static com.alibaba.citrus.turbine.TurbineConstant.LAYOUT_TEMPLATE;
import static com.alibaba.citrus.turbine.TurbineConstant.SCREEN_MODULE_NO_TEMPLATE;
import static com.alibaba.citrus.turbine.TurbineConstant.SCREEN_PLACEHOLDER_KEY;
import static com.alibaba.citrus.turbine.TurbineConstant.SCREEN_TEMPLATE;
import static com.alibaba.citrus.turbine.util.TurbineUtil.getTurbineRunData;
import static com.alibaba.citrus.util.Assert.assertNotNull;
import static com.alibaba.citrus.util.BasicConstant.EMPTY_STRING;
import static com.alibaba.citrus.util.ObjectUtil.defaultIfNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.htmlparser.Node;
import org.htmlparser.Parser;
import org.htmlparser.tags.Div;
import org.htmlparser.util.NodeIterator;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
import org.springframework.beans.factory.annotation.Autowired;
import com.alibaba.citrus.service.mappingrule.MappingRuleService;
import com.alibaba.citrus.service.moduleloader.Module;
import com.alibaba.citrus.service.moduleloader.ModuleLoaderException;
import com.alibaba.citrus.service.moduleloader.ModuleLoaderService;
import com.alibaba.citrus.service.moduleloader.ModuleNotFoundException;
import com.alibaba.citrus.service.pipeline.PipelineContext;
import com.alibaba.citrus.service.pipeline.support.AbstractValve;
import com.alibaba.citrus.service.pipeline.support.AbstractValveDefinitionParser;
import com.alibaba.citrus.service.requestcontext.RequestContext;
import com.alibaba.citrus.service.requestcontext.basic.BasicRequestContext;
import com.alibaba.citrus.service.requestcontext.buffered.BufferedRequestContext;
import com.alibaba.citrus.service.template.TemplateException;
import com.alibaba.citrus.service.template.TemplateService;
import com.alibaba.citrus.turbine.Context;
import com.alibaba.citrus.turbine.TurbineConstant;
import com.alibaba.citrus.turbine.TurbineRunData;
import com.alibaba.citrus.turbine.TurbineRunDataInternal;
import com.alibaba.citrus.turbine.support.ContextAdapter;
import com.alibaba.citrus.util.StringUtil;
import com.alibaba.citrus.webx.WebxException;
public class BigPipeValve extends AbstractValve {
@Autowired
private BufferedRequestContext bufferedRequestContext;
@Autowired
private BasicRequestContext basicRequestContext;
@Autowired
private HttpServletRequest request;
@Autowired
private TemplateService templateService;
@Autowired
private MappingRuleService mappingRuleService;
@Autowired
private ModuleLoaderService moduleLoaderService;
public void invoke(PipelineContext pipelineContext) throws Exception {
// 拿出request,解析出下一批要執行的screen,flush目前渲染的。
// 周遊執行PerformScreenValve和RenderTemplateValve
TurbineRunDataInternal rundata = (TurbineRunDataInternal) getTurbineRunData(request);
System.out.println("valve started.");
RequestContext tempRC = basicRequestContext.getWrappedRequestContext();
String body = bufferedRequestContext.popCharBuffer();
System.out.println(body);
List<String> pageletList = parsePagelet(body);
tempRC.getResponse().getWriter().write(body);
tempRC.getResponse().getWriter().flush();
for (String id : pageletList) {
rundata.setTarget("//" + id);
rundata.setLayoutEnabled(false);
performScreenModule(rundata);
render(rundata);
body = bufferedRequestContext.popCharBuffer();
body = renderPagelet(id, body);
System.out.println(body);
tempRC.getResponse().getWriter().write(body);
tempRC.getResponse().getWriter().flush();
}
pipelineContext.invokeNext(); // 調用後序valves
System.out.println("valve ended.");
}
private String renderPagelet(String id, String body) {
body = body.replace("\r\n", "").replace("\n", "").replace("\"", "\'");
// body = StringEscapeUtils.escapeJavaScript(body);
String pagelet = "<script>arrivedHtml(\"contentId\", \"body\");</script>";
return pagelet.replace("contentId", id).replace("body", body);
}
private List<String> parsePagelet(String body) throws ParserException {
Parser parser = new Parser();
parser.setInputHTML(body);
NodeIterator iterator = parser.elements();
Node node = iterator.nextNode();
node = iterator.nextNode();
node = iterator.nextNode();
List<String> pageletList = new ArrayList<String>();
travel(node, pageletList);
return pageletList;
}
private void travel(Node node, List<String> pageletList) {
if (node == null) {
return;
} else {
if (node instanceof Div) {
String cls = ((Div) node).getAttribute("class");
if (cls != null && cls.contains("pagelet")) {
System.out.println(((Div) node).getAttribute("id"));
pageletList.add(((Div) node).getAttribute("id"));
}
}
}
NodeList childrens = node.getChildren();
if (childrens == null || childrens.size() == 0) {
return;
}
for (int i = 0; i < childrens.size(); i++) {
Node temp = childrens.elementAt(i);
travel(temp, pageletList);
}
}
private void render(TurbineRunDataInternal rundata) throws TemplateException, IOException {
String target = assertNotNull(rundata.getTarget(), "Target was not specified");
// 妫�煡閲嶅畾鍚戞爣蹇楋紝濡傛灉鏄噸瀹氬悜锛屽垯涓嶉渶瑕佸皢椤甸潰杈撳嚭銆�
if (!rundata.isRedirected()) {
Context context = rundata.getContext();
renderTemplate(getScreenTemplate(target), context, rundata);
// layout鍙绂佺敤銆�
if (rundata.isLayoutEnabled()) {
String layoutTemplateOverride = rundata.getLayoutTemplateOverride();
if (layoutTemplateOverride != null) {
target = layoutTemplateOverride;
}
String layoutTemplate = getLayoutTemplate(target);
if (templateService.exists(layoutTemplate)) {
String screenContent = defaultIfNull(bufferedRequestContext.popCharBuffer(), EMPTY_STRING);
context.put(SCREEN_PLACEHOLDER_KEY, screenContent);
renderTemplate(layoutTemplate, context, rundata);
}
}
}
}
/** 璁劇疆content type銆� */
protected void setContentType(TurbineRunData rundata) {
// 璁劇疆content
// type锛屼笉闇�璁劇疆charset锛屽洜涓篠etLocaleRequestContext宸茬粡璁劇疆浜哻harset銆�
// 閬垮厤瑕嗙洊鍒漢璁劇疆鐨刢ontentType銆�
if (StringUtil.isEmpty(rundata.getResponse().getContentType())) {
rundata.getResponse().setContentType("text/html");
}
}
/** 鎵цscreen妯″潡銆� */
protected void performScreenModule(TurbineRunData rundata) {
String target = assertNotNull(rundata.getTarget(), "Target was not specified");
// 浠巘arget涓彇寰梥creen module鍚嶇О
String moduleName = getModuleName(target);
try {
Module module = moduleLoaderService.getModuleQuiet(TurbineConstant.SCREEN_MODULE, moduleName);
// 褰撴寚瀹氫簡templateName鏃訛紝鍙互娌℃湁鐨剆creen module锛岃�鍗曞崟娓叉煋妯℃澘銆�
// 杩欐牱灏卞疄鐜頒簡page-driven锛屽嵆鍏堝啓妯℃澘锛屽繀瑕佹椂鍐嶅啓涓�釜module class涓庝箣瀵瑰簲銆�
if (module != null) {
module.execute();
} else {
if (isScreenModuleRequired()) {
throw new ModuleNotFoundException("Could not find screen module: " + moduleName);
}
}
} catch (ModuleLoaderException e) {
throw new WebxException("Failed to load screen module: " + moduleName, e);
} catch (Exception e) {
throw new WebxException("Failed to execute screen: " + moduleName, e);
}
}
/** 濡傛灉杩斿洖<code>true</code>锛岄偅涔堝綋妯″潡鎵句笉鍒版椂锛屼細鎶涘紓甯搞�瀛愮被鍙互瑕嗙洊姝ゆ柟娉曪紝浠ユ敼鍙樿涓恒� */
protected boolean isScreenModuleRequired() {
return false;
}
/** 鏍規嵁target鍙栧緱screen妯″潡鍚嶃�瀛愮被鍙互淇敼鏄犲皠瑙勫垯銆� */
protected String getModuleName(String target) {
return mappingRuleService.getMappedName(SCREEN_MODULE_NO_TEMPLATE, target);
}
protected String getScreenTemplate(String target) {
return mappingRuleService.getMappedName(SCREEN_TEMPLATE, target);
}
protected String getLayoutTemplate(String target) {
return mappingRuleService.getMappedName(LAYOUT_TEMPLATE, target);
}
protected void renderTemplate(String templateName, Context context, TurbineRunDataInternal rundata)
throws TemplateException, IOException {
rundata.pushContext(context);
try {
templateService.writeTo(templateName, new ContextAdapter(context), rundata.getResponse().getWriter());
} finally {
rundata.popContext();
}
}
public static class DefinitionParser extends AbstractValveDefinitionParser<BigPipeValve> {
}
}
pipeline配置
<?xml version="1.0" encoding="UTF-8" ?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:services="http://www.alibaba.com/schema/services"
xmlns:pl-conditions="http://www.alibaba.com/schema/services/pipeline/conditions"
xmlns:pl-valves="http://www.alibaba.com/schema/services/pipeline/valves"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.alibaba.com/schema/services http://localhost:8080/schema/services.xsd
http://www.alibaba.com/schema/services/pipeline/conditions http://localhost:8080/schema/services-pipeline-conditions.xsd
http://www.alibaba.com/schema/services/pipeline/valves http://localhost:8080/schema/services-pipeline-valves.xsd
http://www.springframework.org/schema/beans http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd
">
<services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves">
<!-- 初始化turbine rundata,并在pipelineContext中設定可能會用到的對象(如rundata、utils),以便valve取得。 -->
<prepareForTurbine />
<!-- 設定日志系統的上下文,支援把目前請求的詳情列印在日志中。 -->
<setLoggingContext />
<!-- 分析URL,取得target。 -->
<analyzeURL />
<!-- 檢查csrf token,防止csrf攻擊和重複送出。假如request和session中的token不比對,則出錯,或顯示expired頁面。 -->
<checkCsrfToken />
<loop>
<choose>
<when>
<!-- 執行帶模闆的screen,預設有layout。 -->
<pl-conditions:target-extension-condition extension="null, vm, jsp, jspx" />
<performAction />
<performTemplateScreen />
<renderTemplate />
</when>
<when>
<!-- 執行不帶模闆的screen,預設無layout。 -->
<pl-conditions:target-extension-condition extension="do" />
<performAction />
<performScreen />
</when>
<otherwise>
<!-- 将控制交還給servlet engine。 -->
<exit />
</otherwise>
</choose>
<!-- 假如rundata.setRedirectTarget()被設定,則循環,否則退出循環。 -->
<breakUnlessTargetRedirected />
</loop>
<valve class="com.alibaba.webx.tutorial1.common.BigPipeValve" />
</services:pipeline>
</beans:beans>