JSP EL 教學:Bean、Scope、param、運算子與 JSTL 函式
JSP EL(Expression Language)讓頁面用簡潔的表達式讀取 JavaBean、集合、request parameter、cookie、header 與不同 scope 的資料。它適合畫面層的資料呈現與簡單判斷,但不應取代 Servlet、Controller 或 Service 中的驗證與商業邏輯。
${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.*;維護舊 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 應使用 pageScope、requestScope 等 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 中,lt、le 等文字形式通常比直接寫 < 更容易避免轉義問題。複雜條件應移回後端,或至少用括號清楚表達優先順序。
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:length、fn:replace、fn: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/core 與 http://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 遮蔽;重要資料寫清楚
requestScope或sessionScope。 - 把
fn:*說成 EL 內建:先載入相容版本的 Jakarta Tags/JSTL function tag library。 - 直接輸出 request parameter:依輸出情境進行轉義,不要只因頁面看起來正常就忽略 XSS。
- 混用
javax與jakarta:依應用伺服器支援矩陣完整對齊 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.id 和 requestScope.id 一樣嗎?
不一樣。param.id 來自 HTTP request parameter;requestScope.id 是後端以 attribute 方式放入 request scope 的物件。兩者來源、型別與信任程度都不同。
新專案要使用哪個 JSTL URI?
如果容器與依賴使用 Jakarta Tags 3.0,core 與 functions URI 分別是 jakarta.tags.core、jakarta.tags.functions。舊 Java EE/JSTL 專案應保留與現有容器相容的 URI,直到完成整體 Jakarta 遷移。
重點整理
${}是 JSP 範本文字最常用的立即求值 EL 語法;#{}用於特定延遲求值屬性。- 資料先由後端準備,再用 EL 讀取 Bean、Map、List、陣列與四種 scope。
param、cookie、header與pageContext的資料來源不同,不應混為一談。fn:*是 Jakarta Tags/JSTL 提供的標準函式;版本與 taglib URI 必須和容器相容。- 輸出不可信資料時要依情境轉義,並把驗證、權限與商業邏輯留在後端。















