JSP 頁面透過 Expression Language 從 Bean、request、session 與參數解析資料並安全輸出

JSP EL 教學:Bean、Scope、param、運算子與 JSTL 函式

JSP EL(Expression Language)讓頁面用簡潔的表達式讀取 JavaBean、集合、request parameter、cookie、header 與不同 scope 的資料。它適合畫面層的資料呈現與簡單判斷,但不應取代 Servlet、Controller 或 Service 中的驗證與商業邏輯。

先看結論:JSP 範本中最常用的是 ${expression};資料先由後端放進 request 或其他 scope,再由 EL 讀取。未指定 scope 的名稱會依 page、request、session、application 順序尋找。要處理字串長度、替換或分割,可搭配 Jakarta Tags/JSTL 的 fn:* 函式;輸出 request parameter 等不可信內容時,應使用 <c:out> 或符合輸出情境的轉義方式。

JSP EL 是什麼?

Expression Language 最初是為 Web 應用程式的呈現層設計。它能解析變數、巢狀 property、Map、List、陣列與 JavaBean,也支援算術、比較、邏輯、條件與 empty 等運算子。

JSP 頁面會由容器轉換成實作類別並處理請求。EL 的角色是讓頁面作者不用在 HTML 中塞入大量 Java scriptlet,就能把後端準備好的資料顯示出來。例如:

<p>姓名:${user.name}</p>
<p>訂單編號:${requestScope.order.id}</p>

EL 語法精簡,不代表所有運算都適合放在頁面。權限檢查、輸入驗證、資料庫查詢、交易與複雜轉換仍應由後端處理。

先分清楚 Jakarta 與舊 Java EE 專案

目前 Jakarta Server Pages 4.0 搭配 Jakarta Expression Language 6.0;EL 4.0 起,API 命名空間由 javax 移到 jakarta。EL 6.0 對應 Jakarta EE 11,最低 Java 版本為 17。

舊的 Java EE/JSP 專案可能仍使用 javax.* 套件與傳統 JSTL URI。不要只改一行 taglib URI 就假設遷移完成:Servlet、JSP、EL、JSTL/Jakarta Tags、應用伺服器與依賴版本必須彼此相容。

實務判斷:新 Jakarta EE 專案依容器支援版本使用 jakarta.*;維護舊 Java EE 專案時,先確認 Tomcat/應用伺服器與現有依賴,再決定是否完整遷移,不要混用兩套命名空間。

${}#{} 有什麼差別?

JSP 規格定義兩種 EL 分隔符:

  • ${expr}:立即求值,JSP 範本文字中最常用;頁面產生回應時就會計算並輸出結果。
  • #{expr}:延遲求值,只有支援 deferred value 或 method expression 的標籤屬性才能使用。

一般 JSP 範本文字只允許 ${}。若把 #{} 直接放在範本文字,通常會造成轉譯錯誤。初學 EL 時,先把重點放在 ${} 即可。

${'Hello, EL!'}
${1 + 2}
${loggedIn ? user.name : '訪客'}

先由 Servlet/Controller 準備資料

EL 只負責讀取頁面可見的資料。典型流程是後端完成查詢與驗證,再把 View Model 或 JavaBean 放進 request:

// Servlet 或 Controller
request.setAttribute("user", userView);
request.setAttribute("orders", orderViews);
request.getRequestDispatcher("/WEB-INF/views/account.jsp")
       .forward(request, response);

JSP 再透過 EL 顯示:

<p>${requestScope.user.name}</p>
<p>第一筆訂單:${requestScope.orders[0].number}</p>

把資料準備與畫面輸出分開,能讓 JSP 更容易閱讀,也比較方便測試、重用與安全審查。

讀取 Bean、Map、List 與陣列

EL 的點號與中括號存取在語意上相通。${user.name} 等同 ${user["name"]};遇到包含連字號、空白或動態 key 時,中括號寫法更實用。

資料 EL 寫法 用途
JavaBean property ${user.name} 通常對應 getName() 或可解析的 property
動態 property/Map key ${settings[key]} key 由另一個變數決定
特殊字元的 key ${headers["User-Agent"]} 不能直接使用點號的名稱
List/陣列 ${orders[0]} 以索引取得元素
巢狀資料 ${order.customer.name} 連續解析多層 property

若名稱拼錯或資料不存在,EL 在部分情況下會回傳 null 或經過寬鬆型別轉換,而不一定立刻報錯。因此不要把「頁面沒例外」當成資料一定正確。

page、request、session、application scope

JSP 提供四個常用 scope。若直接寫 ${profile},容器會依 page、request、session、application 的順序尋找同名 attribute。為了避免同名資料被錯誤遮蔽,重要資料可明確指定 scope。

Scope EL implicit object 生命週期與適合用途 範例
page pageScope 目前 JSP 頁面處理期間;頁面內暫存值 ${pageScope.notice}
request requestScope 目前請求;Controller 傳給 View 的資料 ${requestScope.order}
session sessionScope 使用者 session 有效期間;少量登入狀態或偏好 ${sessionScope.user}
application applicationScope 整個 Web 應用程式;全域共享設定或物件 ${applicationScope.siteName}

不要因為 session 方便就把大量或敏感資料長期塞進去。session 會占用伺服器資源,也會影響叢集同步與登出/失效行為。

JSP EL 的 implicit objects

除了四個 scope,JSP 還提供多個固定名稱的 implicit objects:

名稱 內容 常用範例
param 每個 request parameter 的單一字串值 ${param.id}
paramValues 每個 parameter 的全部值,型別為字串陣列 ${paramValues.tag[0]}
header 每個 request header 的單一字串值 ${header["User-Agent"]}
headerValues 每個 header 的全部字串值 ${headerValues.accept[0]}
cookie cookie 名稱對應的 Cookie 物件 ${cookie.theme.value}
initParam ServletContext 初始化參數 ${initParam.supportEmail}
pageContext 目前 JSP 的 PageContext 物件 ${pageContext.request.requestURI}

原本常見的 ${pageContext.attribute} 寫法容易造成誤解:一般頁面 attribute 應使用 pageScoperequestScope 等 Map;pageContext 是 PageContext 物件本身,適合存取它公開的 request、session 或其他 property。

算術、比較、邏輯與條件運算子

類型 運算子 範例
算術 +-*/div%mod ${subtotal + shipping}
比較 ==eq!=ne<lt>gt<=le>=ge ${order.total ge 1000}
邏輯 &&and||or!not ${loggedIn and not empty user}
空值判斷 empty ${empty orders}
條件 A ? B : C ${empty user ? '訪客' : user.name}
字串串接 += ${firstName += ' ' += lastName}

在 XML 語法或 HTML attribute 中,ltle 等文字形式通常比直接寫 < 更容易避免轉義問題。複雜條件應移回後端,或至少用括號清楚表達優先順序。

empty、null 與型別轉換要注意什麼?

empty 可判斷值是否為 null 或空集合、空字串等,適合在顯示清單或提示前先檢查:

<c:choose>
  <c:when test="${empty orders}">目前沒有訂單</c:when>
  <c:otherwise>共有 ${fn:length(orders)} 筆訂單</c:otherwise>
</c:choose>

EL 為呈現層提供相對寬鬆的預設值與型別轉換,能減少頁面因小問題中斷,但也可能掩蓋拼字錯誤或資料型別不一致。金額、日期、權限或必要欄位應在後端先驗證並轉成適合 View 使用的型別,不要依賴 EL 自動猜測。

EL 函式與 Jakarta Tags/JSTL 函式的差別

EL 本身定義「具 namespace 的函式呼叫」機制,函式會映射到 Java 類別的 public static method;但常見的 fn:lengthfn:replacefn:split 等,是 Jakarta Standard Tag Library 提供的標準函式,不是 EL 核心內建字串函式。

Jakarta Tags 3.0 的 taglib URI 如下:

<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>

舊專案可能使用 http://java.sun.com/jsp/jstl/corehttp://java.sun.com/jsp/jstl/functions。Jakarta Tags 3.0 才將 URI 改為 jakarta.tags.*,因此要依容器與套件版本選擇,不能混搭。

函式 用途 範例
fn:length 集合項目數或字串長度 ${fn:length(items)}
fn:contains 字串是否包含子字串 ${fn:contains(name, keyword)}
fn:replace 替換字串內容 ${fn:replace(code, '-', '')}
fn:split 依分隔字串切成陣列 ${fn:split(tags, ',')}
fn:trim 移除前後空白 ${fn:trim(query)}
fn:escapeXml 轉義可被解讀為 XML markup 的字元 ${fn:escapeXml(param.q)}

直接輸出 param 可能造成 XSS

param、header 與 cookie 都可能被使用者或外部請求控制。直接把 ${param.q} 放進 HTML,瀏覽器可能把惡意內容當成標記或程式碼解讀。輸出一般 HTML 文字時,優先使用預設會轉義 XML 字元的 <c:out>

<p>搜尋:<c:out value="${param.q}" default="未輸入" /></p>

需要放進 attribute 時,可依情況使用 fn:escapeXml,但轉義必須符合輸出情境。HTML 文字、HTML attribute、URL、CSS 與 JavaScript 字串需要的處理方式不同;不要把未信任資料直接拼進 <script>、事件屬性或 URL。安全也不只靠輸出轉義,後端仍要驗證輸入、限制允許值並使用正確的 Content Security Policy。

常見錯誤與改善方式

  • 在 JSP 寫商業邏輯:把查詢、權限與複雜運算移到 Controller/Service,只把 View 需要的結果交給 JSP。
  • pageContext 當成 pageScope:頁面 attribute 使用 pageScope.name;PageContext 自身的 property 才從 pageContext 存取。
  • 未指定 scope:同名 attribute 可能被較前面的 scope 遮蔽;重要資料寫清楚 requestScopesessionScope
  • fn:* 說成 EL 內建:先載入相容版本的 Jakarta Tags/JSTL function tag library。
  • 直接輸出 request parameter:依輸出情境進行轉義,不要只因頁面看起來正常就忽略 XSS。
  • 混用 javaxjakarta依應用伺服器支援矩陣完整對齊 Servlet、JSP、EL 與 Tags 版本。

常見問題

JSP EL 可以呼叫 Java method 嗎?

EL 規格支援 method invocation 與映射到 public static method 的函式,但 JSP 頁面是否適合直接呼叫,還要看容器、EL 版本與設計。呈現層應保持簡單;複雜操作仍由後端完成。

${bean.property} 一定會呼叫 getter 嗎?

對 JavaBean 通常由 BeanELResolver 解析 property,但 EL 也能處理 Map、List、陣列、Record 等不同物件,實際 resolver 取決於 base 物件型別與容器。不要把所有點號存取都理解成單純字串替換。

param.idrequestScope.id 一樣嗎?

不一樣。param.id 來自 HTTP request parameter;requestScope.id 是後端以 attribute 方式放入 request scope 的物件。兩者來源、型別與信任程度都不同。

新專案要使用哪個 JSTL URI?

如果容器與依賴使用 Jakarta Tags 3.0,core 與 functions URI 分別是 jakarta.tags.corejakarta.tags.functions。舊 Java EE/JSTL 專案應保留與現有容器相容的 URI,直到完成整體 Jakarta 遷移。

重點整理

  • ${} 是 JSP 範本文字最常用的立即求值 EL 語法;#{} 用於特定延遲求值屬性。
  • 資料先由後端準備,再用 EL 讀取 Bean、Map、List、陣列與四種 scope。
  • paramcookieheaderpageContext 的資料來源不同,不應混為一談。
  • fn:* 是 Jakarta Tags/JSTL 提供的標準函式;版本與 taglib URI 必須和容器相容。
  • 輸出不可信資料時要依情境轉義,並把驗證、權限與商業邏輯留在後端。

官方參考資料

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *