---
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
```
> 
* **查看 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());
```
> 
:::
* **查看 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`
>
> 
* **對於 `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`
> 
### 自訂 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>
```
> 
## 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>
```
> 
### 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`
>
> 
* **使用標準標籤 `<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`
>
> 
我們進入 `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 轉譯為 `<%`、ASCII 轉譯為 `<`
* 「`%>`」HTML需轉譯為 `%>`、ASCII 轉譯為 `>`
### 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>
```
> 
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>
```
> 
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>
```
> 
### 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>
```
測試如下,可以看到它確實打印出了錯誤的堆疊
> 
## Appendix & FAQ
:::info
:::
###### tags: `Web`