--- title: 'Servlet & 基礎 JSP' disqus: kyleAlien --- Servlet & 基礎 JSP === ## Overview of Content 如有引用參考請詳註出處,感謝 :cat: [TOC] ## JSP ### JSP 生命週期 * JSP 與 Servlet 是一體兩面 **JSP 可以做到與 Servlet 的所有事情,因為 Servlet 會透過對 JSP(`MetaData`) 的解釋,最終將 JSP 轉譯為 Servlet 程式碼** 它在服務端的概念如下圖 ```mermaid graph TB; subgraph 客戶端 瀏覽器 end subgraph 伺服器端 JSP -.-> |轉譯| Java -.-> |編譯| Class Class --> |載入| Servlet end 瀏覽器 -.-> |呼叫 JSP| Servlet ``` ### JSP 元素概述 * 在 JSP 中,我們大多時都在寫 HTML,但其實我們仍可在 JSP 中透過一些「元素」的標示,來撰寫 Java 程式 > 直接在 JSP 中撰寫的 HMTL 都會直接變成輸出 JSP 的程式如下,它類似於 HTML 語言,不過它會透過 Web 容器轉譯,最終變為 Servlet ```xml= <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> </body> </html> ``` * JSP 指示(`Directive`)元素的主要目的,是在於告訴 Web 容器如何解釋這個 JSP,並且在使用這些元素時會有一些規則 :::info * 這些元素包括「**指示元素**」、「**隱含元素**」、「**Scriptlet 元素**」、「**運算元素**」… 等等 ::: ### JSP 中的註解 * JSP 中的註解格式有以下幾種 ```xml= <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <%-- 註解 1 (JSP 專屬註解) --%> <%-- // 單行 /** * 多行 */ --%> <!-- 註解 2 (網頁版本註解) --> </body> </html> ``` ### JSP 隱含物件 - PageContext * 在 Web 容器將 JSP 轉譯 Servlet 時會有一些隱含物件,這些隱含物件可以直接在 JSP 中使用,但 JSP 所見到的物件是經過包裝的物件,所以名稱不同… 如下表 | JSP 所見的隱含物件 | 轉譯 Servlet 後,其中的物件 | | - | - | | `out` | `JspWriter`(具有 BufferWriter、PrintWriter 功能) | | `request` | `HttpServletRequest` | | `response` | `HttpServletResponse` | | `config` | `ServletConfig` | | `application` | `ServletContext` | | `pageContext` | `PageContext`(它提供了對 JSP 頁面資源的封裝,可設定屬性範圍) | | `exception` | `Throwable`(尤其他 JSP 頁面拋出的異常物件) | | `page` | `this` | :::danger * 隱含物件的轉譯 隱含物件在轉譯為 Servlet 後,會存在 `_jspService` 中,所以 **無法在宣告元素中使用(也就是這些隱含物件不能用在 `<%!` 到 `%>` 之間)** ::: * 其中較為特別的是 `pageContext` 物件:它提供了對 JSP 頁面資源的封裝 對應的物件是 `PageContext`,它是 Servlet 的拓展 [**`javax.servlet.jsp`**](https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api/2.3.3),原生 Servlet 可能會沒有,所以需要專案中增加 Library 依賴(如下) ```xml= <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> ``` * `PageContext` 中同樣可設定「屬性」,並且該屬性的特別之處在於,它可以設定「範疇」;PageContext 的屬性範疇有以下四種 ```java= public abstract class PageContext extends JspContext { // 只能保留在頁面 public static final int PAGE_SCOPE = 1; // 只能在 Request 時取得 public static final int REQUEST_SCOPE = 2; // 可以在整個 Session 中取得 public static final int SESSION_SCOPE = 3; // 整個應用都可取得 public static final int APPLICATION_SCOPE = 4; ... } ``` ### JSP 轉譯 Servlet :::info 使用 Tomcat Web 容器 > 可以在 `<Web 容器>/work/Catalina/localhost/<專案名>/org/apache/` 資料夾下,看到 Web 容器將 JSP 檔案解譯為 Servlet 的結果 ::: * JSP 其實是 Web 容器在編譯期間對 MetaData 的解析 並且看到 Web 容器 JSP 檔案解譯 Servlet 的結果,可以更清楚的了解到 JSP 是的元素 * **`JSP` 檔案** ```xml= <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> </body> </html> ``` * **`JSP` 解譯為 `Servlet` 後的檔案** ```java= package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class sample_005fjsp_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory.getDefaultFactory(); private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants; private static final java.util.Set<java.lang.String> _jspx_imports_packages; private static final java.util.Set<java.lang.String> _jspx_imports_classes; static { _jspx_imports_packages = new java.util.HashSet<>(); _jspx_imports_packages.add("javax.servlet"); _jspx_imports_packages.add("javax.servlet.http"); _jspx_imports_packages.add("javax.servlet.jsp"); _jspx_imports_classes = null; } private volatile javax.el.ExpressionFactory _el_expressionfactory; private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map<java.lang.String,java.lang.Long> getDependants() { return _jspx_dependants; } public java.util.Set<java.lang.String> getPackageImports() { return _jspx_imports_packages; } public java.util.Set<java.lang.String> getClassImports() { return _jspx_imports_classes; } public javax.el.ExpressionFactory _jsp_getExpressionFactory() { if (_el_expressionfactory == null) { synchronized (this) { if (_el_expressionfactory == null) { _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory(); } } } return _el_expressionfactory; } public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() { if (_jsp_instancemanager == null) { synchronized (this) { if (_jsp_instancemanager == null) { _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig()); } } } return _jsp_instancemanager; } public void _jspInit() { } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) { final java.lang.String _jspx_method = request.getMethod(); if ("OPTIONS".equals(_jspx_method)) { response.setHeader("Allow","GET, HEAD, POST, OPTIONS"); return; } if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) { response.setHeader("Allow","GET, HEAD, POST, OPTIONS"); response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS"); return; } } final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; out.write("\n"); out.write("\n"); out.write("<html>\n"); out.write("<head>\n"); out.write(" <title>Title</title>\n"); out.write("</head>\n"); out.write("<body>\n"); out.write("\n"); out.write("</body>\n"); out.write("</html>\n"); } catch (java.lang.Throwable t) { if (!(t instanceof javax.servlet.jsp.SkipPageException)){ out = _jspx_out; if (out != null && out.getBufferSize() != 0) try { if (response.isCommitted()) { out.flush(); } else { out.clearBuffer(); } } catch (java.io.IOException e) {} if (_jspx_page_context != null) _jspx_page_context.handlePageException(t); else throw new ServletException(t); } } finally { _jspxFactory.releasePageContext(_jspx_page_context); } } } ``` * 在查看 `HttpJspBase` 後我們可以發現,其實它是一個「**模板設計模式**」,並且有以下特點 * JSP 轉譯為 Servlet 並載入容器後,容器產生的 Servlet 會呼叫 `_jspInit()` 方法 * 每當客戶端對 JSP 發送請求後,容器產生的 Servlet 會呼叫 `_jspService()` 方法 * 當銷毀 JSP 產生的 Servlet 物件之前,容器產生的 Servlet 會呼叫 `_jspDestory()` 方法 :::info * 如果沒有 `HttpJspBase` 這個類,那我們可以添加以下依賴 ```xml= <dependency> <groupId>org.eclipse.jetty.orbit</groupId> <artifactId>org.apache.jasper.glassfish</artifactId> <version>2.2.2.v201112011158</version> </dependency> ``` ::: ```java= public abstract class HttpJspBase extends HttpServlet implements HttpJspPage { protected HttpJspBase() { } public final void init(ServletConfig config) throws ServletException { super.init(config); this.jspInit(); this._jspInit(); } public String getServletInfo() { return Localizer.getMessage("jsp.engine.info"); } public final void destroy() { this.jspDestroy(); this._jspDestroy(); } public final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this._jspService(request, response); } public void jspInit() { } // 給 JSP 使用 public void _jspInit() { } public void jspDestroy() { } // 給 JSP 使用 protected void _jspDestroy() { } // 給 JSP 使用 public abstract void _jspService(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException; } ``` ## 指示元素 Directive JSP 指示元素的語法如下,其中 **一個指示類型可以對應到多個屬性** ```shell= <%@ 指示類型 [屬性="值"] %> ``` JSP 中三種常用的指示類型如下表 | 指示類型 | 功能概述 | 屬性 | | - | - | - | | `page` | 告訴 Web 容器如何轉譯目前的 JSP 網頁 | 常用屬性有 `import`、`contentType`、`pageEncoding` | | `include` | 告知 Web 容器將指定 JSP 頁面加入,連同當前 JSP 一起輸出為一個網頁 | 有分靜態、動態 | | `taglib` | 告知 Web 容器如何轉譯這個頁面的「標籤庫」(`Tag Library`) | - | ### 指示類型 - page * page 是用來告訴 Web 容器如何轉譯目前的 JSP 網頁 常用屬性有 `import`、`contentType`、`pageEncoding`, * **`import` 屬性**: 可以有多個 import,**用來引入該 JSP 解譯程 Servlet 後,所需的類型** * **`contentType` 屬性**: 設定該屬性,則同使用 HttpServletReponse 類的 `setContentType(...)` 函數,用來指定網頁的 **內容類型** * **`pageEncoding` 屬性**: 同上,不過這是用來指定該 **網頁的編碼類型** (`charset`),替換成 Servlet 後,程式碼等同如下 ```java= // Servlet 程式實作 pageEncoding="text/html;charset=UTF8" response.setContentType("text/html;charset=UTF8") ``` JSP 範例如下… ```xml= <%@ page import="java.util.List, java.util.HashMap, java.util.Date" contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %> <html> <head> <title>Title</title> </head> <body> </body> </html> ``` * 其中 page 類型還可以指定以下屬性 | page 屬性 | 功能概述 | | - | - | | `info` | 用於設定目前 JSP 頁面的基本資訊,這個訊息最後會轉換為 Servlet 可取得的資訊 | | `autoFlash` | 用於設定輸出串流(`OutputStream`)的 **自動出清** | | `buffer` | 設定輸出的緩出區大小,預設為 8kb | | `extends` | 用來告訴 Web 容器,該 **JSP 該繼承哪個類別** | | `errorPage` | 用於設定 JSP 執行時發生例外(各種 Exception)的處理頁面 | | `isErrorPage` | 標記該頁面是否是要處理錯誤的頁面 | | `session` | 設定是否在轉譯的 Servlet 中 **建立 HttpSession 物件** | | `language` | 指定 Web 容器要使用哪種程式語言來轉譯 JSP | | `isELIgnored` | 設定是否忽略概 JSP 網頁的運算式(`Expression Language`) | | `isThreadSafe` | 轉譯後的 Servlet 是否要使用執行序安全 Servlet(這樣效能會較低) | ### 指示類型 - 靜態 include * include 指示類型是用來告知 Web 容器該 JSP 要「包含」另一個 JSP; 這我們將可以下完整的 JSP 程式拆分,將其分成不同檔案 ```xml= <%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <title>Title</title> </head> <body> <a href="login.do">Log in</a> </body> </html> ``` 範例如下… 1. **要被包含的 JSP 檔案** ```xml= <%@ page contentType="text/html;charset=UTF-8" %> <a href="login.do">Log in</a> ``` 2. **主 JSP 檔案** 其中可以使用 `<%@include>` 標籤來將其他 JSP 加入近來 ```xml= <%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <title>Title</title> </head> <body> <!-- include JSP 檔案 --> <%@include file="include_usage_2.jsp"%> </body> </html> ``` :::info * 最終 Servlet 轉譯完後,會將將 JSP 轉為「一個檔案」 > 這是一種靜態包括的方式(在編譯期就會完成) 如果是要在 Runtime 做動態包括則需使用 `<jsp:include>` 標籤,該標籤會將每一個 JSP 檔案「分別轉譯」(有幾個 JSP 檔案就會轉成幾分) ::: * 也可以在 `web.xml` 檔中設定 JSP 開頭、結尾所要 include 的網頁 ```xml= <jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <include-prelude>jsp/pre.jsp</include-prelude> <include-prelude>jsp/coda.jsp</include-prelude> </jsp-property-group> </jsp-config> ``` ## 宣告元素 如果要在 Web 容器轉譯 JSP 後的 Servlet 中宣告類成員、類方法、陳述句,那就可以使用 **宣告、Scriptlet、運算元素** ### JSP 宣告成員、函數 `<%!` * JSP 宣告成員、函數的格式如下 ```xml= <%! 類成員... 類方法... %> ``` :::info 相較於 JSP 指示元素,宣告元素移除 `@` 改為 `!` 符號 ::: 範例如下;**我們在 `<%!`、`%>` 之間宣告成員、函數** ```xml= <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <%! String msg = "Hello"; String getMessage() { return msg + "~ JSP"; } %> </body> </html> ``` 開啟 JSP 網頁查看 ```shell= http://localhost:8080/JavaEEHelloWorld/jsp/declaration_jsp.jsp?name=Hello ``` > ![image](https://hackmd.io/_uploads/Bk0KdCwdp.png) * **查看 Web 容器生產的源碼**: 經過 Web 容器解譯過後的呈現,我們可以看到「**JSP 宣告成員、函數**」會被定義在 Servlet 物件的內部(作為成員、函數) ```java= public final class declaration_005fjsp_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { String msg = "Hello"; String getMessage() { return msg + "~ JSP"; } ... } ``` ### JSP Scriptlet 元素 - Java 宣告陳述句 `<%` * JSP 宣告陳數據(方法內的成員或是判斷)的格式如下 ```xml= <% 陳述句... %> ``` :::info 相較於 JSP 指示元素,宣告元素移除 `@` 符號 ::: 範例如下;我們 **在 `<%`、`%>` 符號間撰寫 Java 陳述句**,並且在其中呼叫宣告元素 ```xml= <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <%-- 宣告元素 --> <%! String msg = "Hello"; String getMessage() { return msg + "~ JSP"; } %> <% String userName = request.getParameter("name"); if (userName != null) { out.write(getMessage() + ", Hello " + userName); } %> </body> </html> ``` * **查看 Web 容器生產的源碼**: 可以看到 JSP Scriptlet 元素所生產出來的 **Java 宣告陳述句是放在 `_jspService` 函數裡**! ```java= public final class declaration_005fjsp_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { String msg = "Hello"; String getMessage() { return msg + "~ JSP"; } ... public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { ... try { ... // Scriptlet 元素 產出的程式 String userName = request.getParameter("name"); if (userName != null) { out.write(getMessage() + ", Hello " + userName); } } catch (java.lang.Throwable t) { ... } finally { ... } } } ``` ### JSP 運算元素 `<%=` * 運算元素的格式如下 ```xml= <%= 運算元素... %> ``` :::info 相較於 JSP 指示元素,宣告元素移除 `@` 改為 `=` 符號 ::: 我們可以運算是元素中撰寫 Java 運算式,並且運算式的結果會直接輸出為網頁的一部分(將結果直接傳給 `out`) ```xml= <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <%= new Date() %> </body> </html> ``` :::danger * 運算式內不需要 `;` 號 因為運算元素經過 Web 容器轉譯後,就會自動加入到 out 的輸出中 ```java= // 如果加上 ; 號,Java 語法就會錯 // out.print(new Date();); out.print(new Date()); ``` > ![image](https://hackmd.io/_uploads/H1gF9rMu6.png) ::: * **查看 Web 容器生產的源碼**: 我們可以看到,**JSP 宣告的運算元素同樣是產生放在 `_jspService` 函數裡**,並且直接透過 `out.write` 輸出到 HTML 中 ```java= public final class declaration_005fjsp_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { ... public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { ... try { ... out.write("<html>\n"); out.write("<head>\n"); out.write(" <title>Title</title>\n"); out.write("</head>\n"); out.write("<body>\n"); out.write("\n"); // JSP 產生的運算元素 out.print(new Date()); ... out.write("</body>\n"); out.write("</html>\n"); } catch (java.lang.Throwable t) { ... } finally { ... } } } ``` ## EL 運算式 對於 JSP 中一些基礎、簡單的屬性、請求參數、標頭、Cookie... 等等可以使用 EL (`Expression Language`) 運算式語言 這樣可以有效地減少 Scriptlet 的程式 ### EL 隱含物件 * 在 EL 運算式中,有提供幾個隱含物件,這些物件大部分都是 Map 類型 * **非 Map 類型** * `pageContext`:本身是一個 JavaBean 只要是 `getXxx()` 方法就可以透過 EL 運算式取得 * **Map 類型** * 屬性相關:`pageScope`、`requestScope`、`sessionScope`、`applicationScope` 物件 分別可以可以用來取得 `pageContext`、`request`、`session`、`application` 的 `setAttribute` 方法設定的物件 ```xml= <% String testMsg = "HelloWorld"; request.setAttribute("msg", testMsg); session.setAttribute("session", 123); %> ${requestScope["msg"]} <br> ${sessionScope["session"]} <br> ``` * 參數相關:`param`、`paramValues` | JSP 寫法 | EL 表達式寫法 | | - | - | | `<%= request.getParameter("person")>` | `${param.person}` | | `<%= request.getParameterValues("persons")[0]` | `${param.persons[0]}` | * 標頭相關:`header`、`headerValues` | JSP 寫法 | EL 表達式寫法 | | - | - | | `<%= reqest.getHeader("User-Agent") %>` | `${header["User-Agent"]}` | | `<%= reqest.getHeaders() %>` | `${headerValues}` | * Cookie 相關:`cookie` JSP 會將 cookie 陣列轉為 Map 類型,使其可以透過`[]` 符號取得;eg. `${cookie.username}` * 初始化參數:`initParam` JSP 會將 initParam 陣列轉為 Map 類型,使其可以透過`[]` 符號取得 ### 使用 EL 運算式、EL 特點 * EL 運算式使用 1. 使用 `${}` 符號來包裹運算式 ```xml= ${param.a} ``` 2. 取得陣列(`Array`)、列表(`List`)物件的元素方式可以簡化為使用 `[]` 符號 * Array Getter ```xml= <% String[] fruits = {"Apple", "Banana"}; request.setAttribute("fruits", fruits); %> ${fruits[0]} <br> ${fruits[1]} <br> ``` * List Getter(可使用 `[]` 或是 `.` 符號取得元素) ```xml= <% List<String> colorList = new ArrayList<String>() { { add("Red"); add("Green"); add("Blue"); } }; request.setAttribute("colorList", colorList); %> ${colorList[0]} <br> ${colorList.get(1)} <br> ${colorList[2]} <br> ``` * Map Getter(可使用 `[]` 或是 `.` 符號取得元素) ```xml= <% Map<String, String> messageMap = new HashMap<String, String>() { { put("Alien", "Hello world"); put("Kyle", "Yo yo! what up!"); } }; request.setAttribute("messageMap", messageMap); %> ${messageMap["Alien"]} <br> ${messageMap.get("Kyle")} <br> ``` * EL 運算式還有以下特點 * **安全的處理 null 值** ```xml= <%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <title>Title</title> </head> <body> <%--<% String a = request.getParameter("a"); String b = request.getParameter("b"); int calAdd = Integer.parseInt(a) + Integer.parseInt(b); out.println("a + b = " + calAdd); %>--%> <!-- 等同如上 --> ${param.a} + ${param.b} = ${param.a + param.b} </body> </html> ``` > 假設只帶入參數 a = 10, JSP 也可以安全的處理沒有帶入的參數, `http://localhost:8080/JavaEEHelloWorld-1.0-SNAPSHOT/el_usage/el_base_usage.jsp?a=10` > > ![image](https://hackmd.io/_uploads/HkbX-i2tT.png) * **對於 `Getter` 函數可以連續存取物件**(火車方式,或是說連接呼叫) ```xml= <%@ page import="jakarta.servlet.http.HttpServletRequest" %> <%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <title>Title</title> </head> <body> <!--<% HttpServletRequest curRequest = (HttpServletRequest) (pageContext.getRequest()); %>--> <!--<%= curRequest.getMethod() %>--> <!--<%= curRequest.getQueryString() %>--> <!--<%= curRequest.getRemoteAddr() %>--> <!-- 等同如上 --> <h1>${pageContext.request.method}</h1> <h1>${pageContext.request.queryString}</h1> <h1>${pageContext.request.remoteAddr}</h1> </body> </html> ``` > 帶入參數的測試 URL `http://localhost:8080/JavaEEHelloWorld-1.0-SNAPSHOT/el_usage/el_link_call.jsp?name=Kyle` > ![image](https://hackmd.io/_uploads/r149Mjhtp.png) ### 自訂 EL 函數 * 如果需要自訂 EL 函數,需要經過以下步驟 * 自訂函數檔(`.java`),它必須滿足以下條件 1. **公開**(`public`)類別 2. 需要呼叫的方法需要是 **公開靜態**(`public static`) 方法 ```java= public class StaticMessage { private StaticMessage() { } public static String getStaticMessage() { return "Define EL expression"; } } ``` * 自訂 **標籤程式庫檔**(`Tag Library Descriptor`, `TLD`),**檔案內容格式是 `XML`,附檔名為 `.tld`** 範例如下: ```xml= <?xml version="1.0" encoding="UTF-8" ?> <taglib version="2.1" xmlns="https://jakarta.ee/xml/ns/jakartaee"> <tlib-version>1.0</tlib-version> <short-name>static.msg</short-name> <uri>http://static.msg/util</uri> <function> <description>Static Message</description> <name>msg</name> <function-class>com.example.javaeehelloworld.el.StaticMessage</function-class> <function-signature>java.lang.String getStaticMessage()</function-signature> </function> </taglib> ``` * JSP 中使用自訂義的標籤 `uri` 屬性對應自定義標籤的 `uri`;`prefix` 是使用自定義標籤時參數名 ```xml= <%@ page contentType="text/html;charset=UTF-8" %> <%@taglib prefix="util" uri="http://static.msg/util" %> <html> <head> <title>Title</title> </head> <body> ${util:msg()} </body> </html> ``` > ![image](https://hackmd.io/_uploads/r1463n2Kp.png) ## JSP 標準標籤 在 JSP 的規範中,提供一些標準標籤(`Standard Tag`),這些標準標籤可以當作規範標籤,不管是哪個容器(`Glassfish`, `Tomcat`... 等等)都會實現 > JSP 早期規範中提出,在這之後才提出 JSTL(`JavaServer Page Standard Tag Library`)、運算式語言(`Expression Language`) ### include、forward 標籤 > 這裡的 include、forward 標籤與上述小節說的不同 * 標準標籤 `<jsp:include>`、`<jsp:forward>` 它的特性是「**動態**」,它是在運行時(`Run-time`)做編譯,可以用來依照條件動態調整 JSP 頁面 經過運行時編譯,會「自己 **單獨編譯成一個 Servlet 類**」 直接來看範例: 1. 提供一個 `sub_get_specific_params.jsp` 碎片,這個 JSP 的目的是為了解析特定 Parameter ```xml= <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <!-- 解析特定 --> <%= request.getParameter("apple") %> <%= request.getParameter("banana") %> </body> </html> ``` 2. 使用靜態標籤 `<jsp:include>` 來引入指定 JSP ```xml= <%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <title>Title</title> </head> <body> <jsp:include page="sub_get_specific_params.jsp"> <jsp:param name="apple" value="100"/> <jsp:param name="banana" value="99999"/> </jsp:include> </body> </html> ``` > ![image](https://hackmd.io/_uploads/HJWrkjDFa.png) ### Bean、Property 標籤 * 可以使用標準標籤 `<jsp:useBean>` 來建立 Servlet 中的 JavaBean 物件,並透過 `<jsp:setProperty>`、`<jsp:getProperty>` 來設定、取得 JavaBean 物件 > 而這個 JavaBean 物件在 JSP 框架下有些設置條件,它必須是純 JavaBean… 細節如下 1. 必須要「**可序列化**」,**也就是須實做 `java.io.Serializable` 界面** 2. 變數必須「**私有**, `private`」 3. 可以沒有建構函數,如果有指定的建構函數,那必須是「**無參建構函數**」 4. 具有「**公開的 `Setter`/`Getter`**」 ```java= public class Person implements Serializable { private String name; private long id; public Person() { } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setId(long id) { this.id = id; } public long getId() { return id; } } ``` * 我們來看看兩種 JavaBean 的用法 * 以往在 **沒有使用標準標籤** 的 JSP 中,如果要使用 Person JavaBean ```xml= <%@ page contentType="text/html;charset=UTF-8" import="com.example.javaeehelloworld.jsp.Person" %> <html> <head> <title>Title</title> </head> <body> <!-- 在 Scriptlet 中撰寫邏輯,取得 JavaBean --> <% Person person = new Person(); person.setName(request.getParameter("Name")); person.setId(Long.parseLong(request.getParameter("Id"))); %> <% String msg = "Hello, "; if (person.getName() != null) { msg = msg + person.getName(); %> <h1><%=msg%></h1> <% } else { %> <h1>No name be set.</h1> <% } %> </body> </html> ``` > 使用的參數 `Name=Alien&Id=9527` > > ![image](https://hackmd.io/_uploads/HkNFx8FF6.png) * **使用標準標籤 `<jsp:useBean>` 建立 JavaBean 物件,使用 `<jsp:setProperty>`、`<jsp:getProperty>` 來設定、取得 JavaBean 物件** | 標籤 | 標籤的屬性 | 功能 | | - | - | - | | `<jsp:useBean>` | `id` | Java 實例的名稱 | | | `class` | 實例化的類別全路徑 | | | `scope` | JavaBean 的存取範疇,除此之外還有 `request`、`session`、`application` | | `<jsp:setProperty>` | `name` | 指定你要獲取哪個名稱的 JavaBean | | | `property` | 指定要取得 JavaBean 的那一個數值;如果使用 `*` 代表自動尋找 | | | `value`(Option) | 設定 JavaBean 數值 | | | `type`(Option) | 指定 JavaBean 的宣告類型 | | `<jsp:getProperty>` | `name`、`property`、`type` | 同上 | ```xml= <%@ page contentType="text/html;charset=UTF-8"%> <jsp:useBean id="person" class="com.example.javaeehelloworld.jsp.Person" scope="page" /> <html> <head> <title>Title</title> </head> <body> <jsp:setProperty name="person" property="name" value="${param.Name}"/> <jsp:setProperty name="person" property="id" value="${param.Id}"/> <% if (person.getName() != null) { %> <h1>Hello, <jsp:getProperty name="person" property="name" /></h1> <% } else { %> <h1>No name be set.</h1> <% } %> </body> </html> ``` > 使用的參數 `Name=Kyle&Id=9527` > > ![image](https://hackmd.io/_uploads/r1YcPUKF6.png) 我們進入 `Tomcat` 的專案目錄下,直接看 JSP 為這些標籤產生了哪些關鍵程式 1. **建立 JavaBean 實例 `<jsp:useBean/>` 標籤** 在下面程式中,我們可以看到 userBean 有存儲的 Scope 控制,預設是 `PAGE_SCOPE`(除此之外還有 `REQUEST_SCOPE`、`SESSION_SCOPE`、`APPLICATION_SCOPE`),這會影響到這個 JavaBean 存取範圍 ```java= public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { ... try { response.setContentType("text/html;charset=UTF-8"); ... // id = "person"(`id` 的設定) // java bean 的類型(`type` 的設定) com.example.javaeehelloworld.jsp.Person person = null; // 嘗試獲取屬性 person = (com.example.javaeehelloworld.jsp.Person) _jspx_page_context.getAttribute("person", javax.servlet.jsp.PageContext.PAGE_SCOPE); if (person == null){ // 創建 JavaBean person = new com.example.javaeehelloworld.jsp.Person(); // 設定屬性(範疇為 PAGE_SCOPE) _jspx_page_context.setAttribute("person", person, javax.servlet.jsp.PageContext.PAGE_SCOPE); } } catch (java.lang.Throwable t) { ... } ``` 2. **使用 Perperty 調用 JavaBean 實例** * **設定 JavaBean `<jsp:setProperty>`** ```java= public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { ... try { response.setContentType("text/html;charset=UTF-8"); ... org.apache.jasper.runtime.JspRuntimeLibrary.handleSetPropertyExpression( _jspx_page_context.findAttribute("person"), "name", "${param.Name}", _jspx_page_context, null); out.write('\n'); org.apache.jasper.runtime.JspRuntimeLibrary.handleSetPropertyExpression( _jspx_page_context.findAttribute("person"), "id", "${param.Id}", _jspx_page_context, null); } catch (java.lang.Throwable t) { ... } ``` * **取得 JavaBean `<jsp:getProperty>`** ```java= out.write("<h1>Hello, "); out.write( org.apache.jasper.runtime.JspRuntimeLibrary.toString( ( ( (com.example.javaeehelloworld.jsp.Person)_jspx_page_context.findAttribute("person") ).getName() ) )); ``` :::info * 從這邊可以看到,不管是設定還是尋找,都是使用 `PageContext`#`findAttribute` 來尋找屬性 並且屬性的尋找順序是按照 `Page`, `Request`, `Session`, `Application` ::: ## 其他 ### JSP 預設 config 設定 * 我們可以透過對 `web.xml` 檔的 `jsp-config` 設定預設的 **網頁編碼**(`page-encoding`)、**內容類型**(`default-content-type`)、**緩衝區大小**(`buffer`)… 等等 範例如下 ```xml= <web-app> <jsp-config> <jsp-property-group> <!-- 使用 延伸對應(extension mapping)URL --> <url-pattern>*.jsp</url-pattern> <page-encoding>UTF-8</page-encoding> <default-content-type>text/html</default-content-type> <!-- 緩衝區大小設置,結尾記得加單位 kb --> <buffer>16kb</buffer> <!-- 縮排設置 --> <trim-directive-whitespaces>true</trim-directive-whitespaces> </jsp-property-group> </jsp-config> </web-app> ``` ### JSP 使用已用的關鍵字 * JSP 中已經使用的關鍵字主要就是「`<%`」、「`%>`」 如果要在 JSP 中顯示,就需要使用 ASCII 或是 HTML 的 [**轉譯格式**](https://tool.oschina.net/commons?type=2) * 「`<%`」HTML 轉譯為 `&lt;%`、ASCII 轉譯為 `&#60;` * 「`%>`」HTML需轉譯為 `%&gt;`、ASCII 轉譯為 `&#62;` ### JSP 錯誤時機 * 由於 JSP 是在 Web 容器的掌控之下進行轉譯、編譯的,雖然方便但出問題時較難去 Debug… 而 JSP 容易出錯會發生在三個時候 1. **轉譯 Servlet 時**: Web 容器轉譯 JSP,將其轉譯為 Servlet 以下我們將 buffer 的數值故意寫錯,這時開啟 JSP 後就會出現錯誤訊息 ```xml= <%@ page contentType="text/html;charset=UTF-8" buffer="10" %> <html> <head> <title>Title</title> </head> <body> </body> </html> ``` > ![image](https://hackmd.io/_uploads/Hy1FTozdp.png) 2. **編譯 Servlet 時**: 轉譯之後的下一個步驟就是編譯,Web 容器會將轉譯過後的 Servlet Java 檔進行編譯 以下我們故意 import 一個不存在的類,這時如果開啟 JSP 後就會出現 javac 相關的錯誤訊息 ```xml= <%@ page contentType="text/html;charset=UTF-8" import="apple.banana.List" %> <html> <head> <title>Title</title> </head> <body> </body> </html> ``` > ![image](https://hackmd.io/_uploads/rkNnRoGOT.png) 3. **Web 容器加載時**: Servlet 編譯成功後(產生 `.class` 檔成功),就會進行載入;這時發生的問題通常是業務邏輯錯誤、程式判斷錯誤,其中最常見的情況就是 NPE ```xml= <%@ page contentType="text/html;charset=UTF-8" import="java.lang.String" %> <html> <head> <title>Title</title> </head> <body> <%! String msg = null; %> <% msg.getBytes(); %> </body> </html> ``` > ![image](https://hackmd.io/_uploads/SJ0Zb2fdT.png) ### JSP 錯誤轉導 * 我們可以統一導向一個執行時發生例外的錯誤顯示頁面,範例如下 * **設定 JSP 錯誤處理頁面** 在 JSP 中設定 `isErrorPage` 為 true,就表示這個頁面是錯誤顯示頁面 > `handle_error.jsp` ```xml= <%@ page contentType="text/html;charset=UTF-8" isErrorPage="true" %> <html> <head> <title>Error</title> </head> <body> <!-- 使用隱含物件,打印錯誤堆疊 --> <% exception.printStackTrace(new PrintWriter(out)); %> </body> </html> ``` * **設定處理導向** 故意引入(`import`)一個不存在的包 ```xml= <%@ page contentType="text/html;charset=UTF-8" import="apple.banana.List" errorPage="handle_error.jsp" %> <html> <head> <title>Title</title> </head> <body> </body> </html> ``` 測試如下,可以看到它確實打印出了錯誤的堆疊 > ![image](https://hackmd.io/_uploads/SyHTC0vda.png) ## Appendix & FAQ :::info ::: ###### tags: `Web`