背景
最近剛入職新公司,瀏覽一下新公司項目,發現項目中大多數JSP頁面都是獨立的、完整的頁面,因此許多頁面都會有如下重復的代碼:
<%@ page language="java" contentType="text/html; charset=UTF-8" import="java.util.Calendar" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <%@ taglib uri="/common-tags" prefix="m"%> <c:set var="ctx" value="${pageContext.request.contextPath}"></c:set> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> <title>${webModule.module.name} ---xxxx</title> <meta name="keywords" content="xxxx"/> <meta name="description" content="xxxx"/> <link rel="stylesheet" href="${ctx}/css/web-bbs.css"/> <link rel="stylesheet" href="${ctx}/css/page.css"/> <script type="text/javascript" src="${ctx}/js/jquery-1.7.2.min.js"></script> <script type="text/javascript" src="${ctx}/js/bbs.js"></script> <script type="text/javascript" src="${ctx}/js/webUtil.js"></script> <script type="text/javascript" src="${ctx}/js/index.js"></script> <script type="text/javascript" src="${ctx}/js/faces.js"></script>
小伙伴們每新添加一個頁面,就需要copy一份上面這坨代碼,還需要在各自頁面重復引入公共的頭尾文件(如header.jsp,footer.jsp等)。。。
對於這種開發方式,重復的工作量就不多描述了,更重要的問題是這種架構方式未來會導致更多的維護工作量、甚至是bug隱患。
舉兩個“栗子”:
- 如果今后開發過程中我們需要全局引入、刪除一些公共的腳本(例如在線客服圖標、GA分析腳本等),變更一下jQuery的版本,更改DocType類型為Html5類型等等。要完成類似的需求我們必須逐個修改JSP文件,工作量就會與項目中JSP文件數量成正比。
- 更麻煩的問題是,對於上述這些全局操作我們無法保證代碼是否是在所有頁面上都生效了,手工檢查?呵呵...
解決方案
上面扯了那么多,其實核心問題就是所有的jsp頁面都是各自為戰,沒有一個統一的公共的模板來維護一些全局的信息,所以這里就介紹一下我們以前的實現方案:
- 實現JSP文件的模板功能、讓所有的頁面都引入一個公共的模板。
- 公共部分信息直接在模板中維護,可變部分在模板中定義占位符,然后由頁面進行重寫來維護不同頁面的多樣性。
有了模板以后就可以這樣寫頁面了:
這樣的寫法好處顯而易見:
- 首先,頁面結構一目了然,寫頁面時無須再關注內容以外的公共部分,減少了許多copy代碼的工作量,同時也降低出錯率
- 其次,公共樣式、腳本等都在模板中引入,便於統一調整
模板內容大概是這個樣子的:
實現原理
實現原理其實很簡單,模板功能的實現主要是兩個自定義標簽(自定義標簽的開發步驟這里就不講了)
BlockTag
該標簽主要用於在模板文件中定義相應的模塊(可以看做一個占位符),在渲染JSP頁面時會將標簽定義的位置替換為頁面重寫的內容,替換時根據標簽的name屬性加上特定的前綴作為key值從request的attribute中讀取內容。

/** * 自定義標簽,用於在Jsp模板中占位 * * @author 逆風之羽 * */ public class BlockTag extends BodyTagSupport { /** * 占位模塊名稱 */ private String name; private static final long serialVersionUID = 1425068108614007667L; @Override public int doStartTag() throws JspException{ return super.doStartTag(); } @Override public int doEndTag() throws JspException { ServletRequest request = pageContext.getRequest(); //block標簽中的默認值 String defaultContent = (getBodyContent() == null)?"":getBodyContent().getString(); String bodyContent = (String) request.getAttribute(OverwriteTag.PREFIX+ name); //如果頁面沒有重寫該模塊則顯示默認內容 bodyContent = StringUtils.isEmpty(bodyContent)?defaultContent:bodyContent; try { pageContext.getOut().write(bodyContent); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } // TODO Auto-generated method stub return super.doEndTag(); } public String getName() { return name; } public void setName(String name) { this.name = name; } }
OverwriteTag
該標簽主要用於在最終的頁面上重寫模板中的相應模塊,在頁面渲染時將標簽內部的內容寫入到當前request的attribute中,該標簽有一個必填參數name屬性作為該內容的key值,這個name屬性必須要和模板中對應要重寫的block的name值相同。

/** * 自定義標簽,用於在jsp模板中重寫指定的占位內容 * * 基本原理: * 將overwrite標簽內容部分添加到ServletRequest的attribute屬性中 * 在后續block標簽中再通過屬性名讀取出來,將其渲染到最終的頁面上即可 * * @author 逆風之羽 * */ public class OverwriteTag extends BodyTagSupport { private static final long serialVersionUID = 5901780136314677968L; //模塊名的前綴 public static final String PREFIX = "JspTemplateBlockName_"; //模塊名 private String name; @Override public int doStartTag() throws JspException { // TODO Auto-generated method stub return super.doStartTag(); } @Override public int doEndTag() throws JspException { ServletRequest request = pageContext.getRequest(); //標簽內容 BodyContent bodyContent = getBodyContent(); request.setAttribute(PREFIX+name, StringUtils.trim(bodyContent.getString())); // TODO Auto-generated method stub return super.doEndTag(); } public String getName() { return name; } public void setName(String name) { this.name = name; } }
總結與拓展
- 所有頁面都使用了模板以后,就可以很方便的控制項目全局的樣式、腳本,由於屏蔽了許多頁面公共信息,也使得日常頁面開發更加高效並減少錯誤率。
- JSP原生是不支持模板機制的,但是僅僅稍加一些手段使用兩個自定義標簽就可以實現模板功能,減少了許多重復的工作量。因此,工作過程中的痛點往往也是個人獲得成長的機會。
- 我在上面Demo中只簡單定義了一個base_template.jsp這一個模板,但是實際場景中一個網站可能有許多布局風格不同類型的頁面,那么一個模板顯然不能滿足多樣性的布局要求,這時我們就可以給模板進行分級將模板定義為base,common,channel三個級別,抽象程度從高到低,實現channel->common->base的繼承關系,不同風格的頁面只需要引入對應的channel模板即可,具體如何抽象還需根據實際的場景區別對待。