始,背景:
編碼問題一直困擾著程序開發人員,尤其是在 Java 中更加明顯,因為 Java 是跨平臺的語言,在不同平臺的編碼之間的切換較多。接下來將介紹 Java 編碼問題出現的根本原因;在 Java 中經常遇到的幾種編碼格式的區別;在 Java 中經常需要編碼的場景;出現中文問題的原因分析;在開發 Java Web 中可能存在編碼的幾個地方;一個 HTTP 請求怎么控制編碼格式;如何避免出現中文編碼問題等。
深入分析 Java Web 中的中文編碼問題
1幾種常見的編碼格式
1 為什么要編碼
2 如何翻譯
2在 Java 中需要編碼的場景
1 在 IO 操作中存在的編碼
2 在內存操作中的編碼
3在 Java 中如何編解碼
幾種編碼格式的比較
4在 Java Web 中涉及的編解碼
1 URL 的編解碼
2 HTTP Header 的編解碼
3 POST 表單的編解碼
4 HTTP BODY 的編解碼
5 其它需要編碼的地方
5常見問題分析
1 中文變成了看不懂的字符
2 一個漢字變成一個問號
3 一個漢字變成兩個問號
4 一種不正常的正確編碼
6總結
1、幾種常見的編碼格式
1.1 為什么要編碼
在計算機中存儲信息的最小單元是 1 個字節,即 8 個 bit, 所以能表示的字符范圍是 0 ~ 255 個。
要表示的符號太多,無法用 1 個字節來完全表示。
1.2 如何翻譯
計算機中提供多種翻譯方式,常見的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16等。這些都規定了轉化的規則,按照這個規則就可以讓計算機正確的表示我們的字符。下面介紹這幾種編碼格式:
ASCII 碼
總共有 128 個,用 1 個字節的低 7 位表示, 0 ~ 31 是控制字符如換行、回車、刪除等,32 ~ 126 是打印字符,可以通過鍵盤輸入并且能夠顯示出來。
ISO-8859-1
128 個字符顯然是不夠用的,所以 ISO 組織在 ASCII 的基礎上擴展,他們是 ISO-8859-1 至 ISO-8859-15,前者涵蓋大多數字符,應用最廣。ISO-8859-1 仍是單字節編碼,它總歸能表示 256 個字符。
GB2312
它是雙字節編碼,總的編碼范圍是 A1 ~ F7,其中 A1 ~ A9 是符號區,總共包含 682 個符號;B0 ~ F7 是漢字區,包含 6763 個漢字。
GBk
GBK 為《漢字內碼擴展規范》,為 GB2312 的擴展,它的編碼范圍是 8140 ~ FEFE(去掉XX7F),總共有 23940 個碼位,能表示 21003 個漢字,和 GB2312的編碼兼容,不會有亂碼。
UTF-16
它具體定義了 Unicode 字符在計算機中的存取方法。UTF-16 用兩個字節來表示 Unicode 的轉化格式,它采用定長的表示方法,即不論什么字符用兩個字節表示。兩個字節是 16 個 bit,所以叫 UTF-16。它表示字符非常方便,沒兩個字節表示一個字符,這就大大簡化了字符串操作。
UTF-8
雖說 UTF-16 統一采用兩個字節表示一個字符很簡單方便,但是很大一部分字符用一個字節就可以表示,如果用兩個字節表示,存儲空間放大了一倍,在網絡帶寬有限的情況下會增加網絡傳輸的流量。UTF-8 采用了一種變長技術,每個編碼區域有不同的字碼長度不同類型的字符可以由 1 ~ 6 個字節組成。
UTF-8 有以下編碼規則:
如果是 1 個字節,最高位(第 8 位)為 0,則表示這是一個 ASCII 字符(00 ~ 7F)
如果是 1 個字節,以 11 開頭,則連續的 1 的個數暗示這個字符的字節數
如果是 1 個字節,以 10 開頭,表示它不是首字節,則需要向前查找才能得到當前字符的首字節
2、在 Java 中需要編碼的場景
2.1 在 I/O 操作中存在的編碼
如上圖:Reader 類是在 Java 的 I/O 中讀取符的父類,而 InputStream 類是讀字節的父類, InputStreamReader 類就是關聯字節到字符的橋梁,它負責在 I/O 過程中處理讀取字節到字符的轉換,而對具體字節到字符的解碼實現,它又委托 StreamDecoder 去做,在 StreamDecoder 解碼過程中必須由用戶指定 Charset 編碼格式。值得注意的是,如果你沒有指定 Charset,則將使用本地環境中默認的字符集,如在中文環境中將使用 GBK 編碼。
如下面一段代碼,實現了文件的讀寫功能:
String file="c:/stream.txt";
String charset="UTF-8";
// 寫字符換轉成字節流
FileOutputStream outputStream=new FileOutputStream(file);
OutputStreamWriter writer=new OutputStreamWriter(
outputStream, charset);
try {
writer.write("這是要保存的中文字符");
} finally {
writer.close();
}
// 讀取字節轉換成字符
FileInputStream inputStream=new FileInputStream(file);
InputStreamReader reader=new InputStreamReader(
inputStream, charset);
StringBuffer buffer=new StringBuffer();
char[] buf=new char[64];
int count=0;
try {
while ((count=reader.read(buf)) !=-1) {
buffer.append(buffer, 0, count);
}
} finally {
reader.close();
}
在我們的應用程序中涉及 I/O 操作時,只要注意指定統一的編解碼 Charset 字符集,一般不會出現亂碼問題。
2.2 在內存操作中的編碼
在內存中進行從字符到字節的數據類型轉換。
1、String 類提供字符串轉換到字節的方法,也支持將字節轉換成字符串的構造函數。
String s="字符串";
byte[] b=s.getBytes("UTF-8");
String n=new String(b, "UTF-8");
2、Charset 提供 encode 與 decode,分別對應 char[] 到 byte[] 的編碼 和 byte[] 到 char[] 的解碼。
Charset charset=Charset.forName("UTF-8");
ByteBuffer byteBuffer=charset.encode(string);
CharBuffer charBuffer=charset.decode(byteBuffer);
3、在 Java 中如何編解碼
Java 編碼類圖
首先根據指定的 charsetName 通過 Charset.forName(charsetName) 設置 Charset 類,然后根據 Charset 創建 CharsetEncoder 對象,再調用 CharsetEncoder.encode 對字符串進行編碼,不同的編碼類型都會對應到一個類中,實際的編碼過程是在這些類中完成的。下面是 String. getBytes(charsetName) 編碼過程的時序圖
Java 編碼時序圖
從上圖可以看出根據 charsetName 找到 Charset 類,然后根據這個字符集編碼生成 CharsetEncoder,這個類是所有字符編碼的父類,針對不同的字符編碼集在其子類中定義了如何實現編碼,有了 CharsetEncoder 對象后就可以調用 encode 方法去實現編碼了。這個是 String.getBytes 編碼方法,其它的如 StreamEncoder 中也是類似的方式。
經常會出現中文變成“?”很可能就是錯誤的使用了 ISO-8859-1 這個編碼導致的。中文字符經過 ISO-8859-1 編碼會丟失信息,通常我們稱之為“黑洞”,它會把不認識的字符吸收掉。由于現在大部分基礎的 Java 框架或系統默認的字符集編碼都是 ISO-8859-1,所以很容易出現亂碼問題,后面將會分析不同的亂碼形式是怎么出現的。
幾種編碼格式的比較
對中文字符后面四種編碼格式都能處理,GB2312 與 GBK 編碼規則類似,但是 GBK 范圍更大,它能處理所有漢字字符,所以 GB2312 與 GBK 比較應該選擇 GBK。UTF-16 與 UTF-8 都是處理 Unicode 編碼,它們的編碼規則不太相同,相對來說 UTF-16 編碼效率最高,字符到字節相互轉換更簡單,進行字符串操作也更好。它適合在本地磁盤和內存之間使用,可以進行字符和字節之間快速切換,如 Java 的內存編碼就是采用 UTF-16 編碼。但是它不適合在網絡之間傳輸,因為網絡傳輸容易損壞字節流,一旦字節流損壞將很難恢復,想比較而言 UTF-8 更適合網絡傳輸,對 ASCII 字符采用單字節存儲,另外單個字符損壞也不會影響后面其它字符,在編碼效率上介于 GBK 和 UTF-16 之間,所以 UTF-8 在編碼效率上和編碼安全性上做了平衡,是理想的中文編碼方式。
4、在 Java Web 中涉及的編解碼
對于使用中文來說,有 I/O 的地方就會涉及到編碼,前面已經提到了 I/O 操作會引起編碼,而大部分 I/O 引起的亂碼都是網絡 I/O,因為現在幾乎所有的應用程序都涉及到網絡操作,而數據經過網絡傳輸都是以字節為單位的,所以所有的數據都必須能夠被序列化為字節。在 Java 中數據被序列化必須繼承 Serializable 接口。
一段文本它的實際大小應該怎么計算,我曾經碰到過一個問題:就是要想辦法壓縮 Cookie 大小,減少網絡傳輸量,當時有選擇不同的壓縮算法,發現壓縮后字符數是減少了,但是并沒有減少字節數。所謂的壓縮只是將多個單字節字符通過編碼轉變成一個多字節字符。減少的是 String.length(),而并沒有減少最終的字節數。例如將“ab”兩個字符通過某種編碼轉變成一個奇怪的字符,雖然字符數從兩個變成一個,但是如果采用 UTF-8 編碼這個奇怪的字符最后經過編碼可能又會變成三個或更多的字節。同樣的道理比如整型數字 1234567 如果當成字符來存儲,采用 UTF-8 來編碼占用 7 個 byte,采用 UTF-16 編碼將會占用 14 個 byte,但是把它當成 int 型數字來存儲只需要 4 個 byte 來存儲。所以看一段文本的大小,看字符本身的長度是沒有意義的,即使是一樣的字符采用不同的編碼最終存儲的大小也會不同,所以從字符到字節一定要看編碼類型。
我們能夠看到的漢字都是以字符形式出現的,例如在 Java 中“淘寶”兩個字符,它在計算機中的數值 10 進制是 28120 和 23453,16 進制是 6bd8 和 5d9d,也就是這兩個字符是由這兩個數字唯一表示的。Java 中一個 char 是 16 個 bit 相當于兩個字節,所以兩個漢字用 char 表示在內存中占用相當于四個字節的空間。
這兩個問題搞清楚后,我們看一下 Java Web 中那些地方可能會存在編碼轉換?
用戶從瀏覽器端發起一個 HTTP 請求,需要存在編碼的地方是 URL、Cookie、Parameter。服務器端接受到 HTTP 請求后要解析 HTTP 協議,其中 URI、Cookie 和 POST 表單參數需要解碼,服務器端可能還需要讀取數據庫中的數據,本地或網絡中其它地方的文本文件,這些數據都可能存在編碼問題,當 Servlet 處理完所有請求的數據后,需要將這些數據再編碼通過 Socket 發送到用戶請求的瀏覽器里,再經過瀏覽器解碼成為文本。這些過程如下圖所示:
一次 HTTP 請求的編碼示例
4.1 URL 的編解碼
用戶提交一個 URL,這個 URL 中可能存在中文,因此需要編碼,如何對這個 URL 進行編碼?根據什么規則來編碼?有如何來解碼?如下圖一個 URL:
-上圖中以 Tomcat 作為 Servlet Engine 為例,它們分別對應到下面這些配置文件中:
Port 對應在 Tomcat 的 中配置,而 Context Path 在 中配置,Servlet Path 在 Web 應用的 web.xml 中的
<servlet-mapping>
<servlet-name>junshanExample</servlet-name>
<url-pattern>/servlets/servlet/*</url-pattern>
</servlet-mapping>
中配置,PathInfo 是我們請求的具體的 Servlet,QueryString 是要傳遞的參數,注意這里是在瀏覽器里直接輸入 URL 所以是通過 Get 方法請求的,如果是 POST 方法請求的話,QueryString 將通過表單方式提交到服務器端。
上圖中 PathInfo 和 QueryString 出現了中文,當我們在瀏覽器中直接輸入這個 URL 時,在瀏覽器端和服務端會如何編碼和解析這個 URL 呢?為了驗證瀏覽器是怎么編碼 URL 的我選擇的是360極速瀏覽器并通過 Postman 插件觀察我們請求的 URL 的實際的內容,以下是 URL:
君山的編碼結果是:e5 90 9b e5 b1 b1,和《深入分析 Java Web 技術內幕》中的結果不一樣,這是因為我使用的瀏覽器和插件和原作者是有區別的,那么這些瀏覽器之間的默認編碼是不一樣的,原文中的結果是:
君山的編碼結果分別是:e5 90 9b e5 b1 b1,be fd c9 bd,查閱上一屆的編碼可知,PathInfo 是 UTF-8 編碼而 QueryString 是經過 GBK 編碼,至于為什么會有“%”?查閱 URL 的編碼規范 RFC3986 可知瀏覽器編碼 URL 是將非 ASCII 字符按照某種編碼格式編碼成 16 進制數字然后將每個 16 進制表示的字節前加上“%”,所以最終的 URL 就成了上圖的格式了。
從上面測試結果可知瀏覽器對 PathInfo 和 QueryString 的編碼是不一樣的,不同瀏覽器對 PathInfo 也可能不一樣,這就對服務器的解碼造成很大的困難,下面我們以 Tomcat 為例看一下,Tomcat 接受到這個 URL 是如何解碼的。
解析請求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,這個方法把傳過來的 URL 的 byte[] 設置到 org.apache.coyote.Request 的相應的屬性中。這里的 URL 仍然是 byte 格式,轉成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:
protected void convertURI(MessageBytes uri, Request request)
throws Exception {
ByteChunk bc=uri.getByteChunk();
int length=bc.getLength();
CharChunk cc=uri.getCharChunk();
cc.allocate(length, -1);
String enc=connector.getURIEncoding();
if (enc !=null) {
B2CConverter conv=request.getURIConverter();
try {
if (conv==null) {
conv=new B2CConverter(enc);
request.setURIConverter(conv);
}
} catch (IOException e) {...}
if (conv !=null) {
try {
conv.convert(bc, cc, cc.getBuffer().length -
cc.getEnd());
uri.setChars(cc.getBuffer(), cc.getStart(),
cc.getLength());
return;
} catch (IOException e) {...}
}
}
// Default encoding: fast conversion
byte[] bbuf=bc.getBuffer();
char[] cbuf=cc.getBuffer();
int start=bc.getStart();
for (int i=0; i < length; i++) {
cbuf[i]=(char) (bbuf[i + start] & 0xff);
}
uri.setChars(cbuf, 0, length);
}
從上面的代碼中可以知道對 URL 的 URI 部分進行解碼的字符集是在 connector 的 中定義的,如果沒有定義,那么將以默認編碼 ISO-8859-1 解析。所以如果有中文 URL 時最好把 URIEncoding 設置成 UTF-8 編碼。
QueryString 又如何解析? GET 方式 HTTP 請求的 QueryString 與 POST 方式 HTTP 請求的表單參數都是作為 Parameters 保存,都是通過 request.getParameter 獲取參數值。對它們的解碼是在 request.getParameter 方法第一次被調用時進行的。request.getParameter 方法被調用時將會調用 org.apache.catalina.connector.Request 的 parseParameters 方法。這個方法將會對 GET 和 POST 方式傳遞的參數進行解碼,但是它們的解碼字符集有可能不一樣。POST 表單的解碼將在后面介紹,QueryString 的解碼字符集是在哪定義的呢?它本身是通過 HTTP 的 Header 傳到服務端的,并且也在 URL 中,是否和 URI 的解碼字符集一樣呢?從前面瀏覽器對 PathInfo 和 QueryString 的編碼采取不同的編碼格式不同可以猜測到解碼字符集肯定也不會是一致的。的確是這樣 QueryString 的解碼字符集要么是 Header 中 ContentType 中定義的 Charset 要么就是默認的 ISO-8859-1,要使用 ContentType 中定義的編碼就要設置 connector 的 中的 useBodyEncodingForURI 設置為 true。這個配置項的名字有點讓人產生混淆,它并不是對整個 URI 都采用 BodyEncoding 進行解碼而僅僅是對 QueryString 使用 BodyEncoding 解碼,這一點還要特別注意。
從上面的 URL 編碼和解碼過程來看,比較復雜,而且編碼和解碼并不是我們在應用程序中能完全控制的,所以在我們的應用程序中應該盡量避免在 URL 中使用非 ASCII 字符,不然很可能會碰到亂碼問題,當然在我們的服務器端最好設置 中的 URIEncoding 和 useBodyEncodingForURI 兩個參數。
4.2 HTTP Header 的編解碼
當客戶端發起一個 HTTP 請求除了上面的 URL 外還可能會在 Header 中傳遞其它參數如 Cookie、redirectPath 等,這些用戶設置的值很可能也會存在編碼問題,Tomcat 對它們又是怎么解碼的呢?
對 Header 中的項進行解碼也是在調用 request.getHeader 是進行的,如果請求的 Header 項沒有解碼則調用 MessageBytes 的 toString 方法,這個方法將從 byte 到 char 的轉化使用的默認編碼也是 ISO-8859-1,而我們也不能設置 Header 的其它解碼格式,所以如果你設置 Header 中有非 ASCII 字符解碼肯定會有亂碼。
我們在添加 Header 時也是同樣的道理,不要在 Header 中傳遞非 ASCII 字符,如果一定要傳遞的話,我們可以先將這些字符用 org.apache.catalina.util.URLEncoder 編碼然后再添加到 Header 中,這樣在瀏覽器到服務器的傳遞過程中就不會丟失信息了,如果我們要訪問這些項時再按照相應的字符集解碼就好了。
4.3 POST 表單的編解碼
在前面提到了 POST 表單提交的參數的解碼是在第一次調用 request.getParameter 發生的,POST 表單參數傳遞方式與 QueryString 不同,它是通過 HTTP 的 BODY 傳遞到服務端的。當我們在頁面上點擊 submit 按鈕時瀏覽器首先將根據 ContentType 的 Charset 編碼格式對表單填的參數進行編碼然后提交到服務器端,在服務器端同樣也是用 ContentType 中字符集進行解碼。所以通過 POST 表單提交的參數一般不會出現問題,而且這個字符集編碼是我們自己設置的,可以通過 request.setCharacterEncoding(charset) 來設置。
另外針對 multipart/form-data 類型的參數,也就是上傳的文件編碼同樣也是使用 ContentType 定義的字符集編碼,值得注意的地方是上傳文件是用字節流的方式傳輸到服務器的本地臨時目錄,這個過程并沒有涉及到字符編碼,而真正編碼是在將文件內容添加到 parameters 中,如果用這個編碼不能編碼時將會用默認編碼 ISO-8859-1 來編碼。
4.4 HTTP BODY 的編解碼
當用戶請求的資源已經成功獲取后,這些內容將通過 Response 返回給客戶端瀏覽器,這個過程先要經過編碼再到瀏覽器進行解碼。這個過程的編解碼字符集可以通過 response.setCharacterEncoding 來設置,它將會覆蓋 request.getCharacterEncoding 的值,并且通過 Header 的 Content-Type 返回客戶端,瀏覽器接受到返回的 socket 流時將通過 Content-Type 的 charset 來解碼,如果返回的 HTTP Header 中 Content-Type 沒有設置 charset,那么瀏覽器將根據 Html 的 中的 charset 來解碼。如果也沒有定義的話,那么瀏覽器將使用默認的編碼來解碼。
4.5 其它需要編碼的地方
除了 URL 和參數編碼問題外,在服務端還有很多地方可能存在編碼,如可能需要讀取 xml、velocity 模版引擎、JSP 或者從數據庫讀取數據等。
xml 文件可以通過設置頭來制定編碼格式
<?xml version="1.0" encoding="UTF-8"?>
Velocity 模版設置編碼格式:
services.VelocityService.input.encoding=UTF-8
JSP 設置編碼格式:
<%@page contentType="text/html; charset=UTF-8"%>
訪問數據庫都是通過客戶端 JDBC 驅動來完成,用 JDBC 來存取數據要和數據的內置編碼保持一致,可以通過設置 JDBC URL 來制定如 MySQL:url=”jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK”。
5、常見問題分析
下面看一下,當我們碰到一些亂碼時,應該怎么處理這些問題?出現亂碼問題唯一的原因都是在 char 到 byte 或 byte 到 char 轉換中編碼和解碼的字符集不一致導致的,由于往往一次操作涉及到多次編解碼,所以出現亂碼時很難查找到底是哪個環節出現了問題,下面就幾種常見的現象進行分析。
5.1 中文變成了看不懂的字符
例如,字符串“淘!我喜歡!”變成了“ì ? £ ?? ò ?2?? £ ?”編碼過程如下圖所示:
字符串在解碼時所用的字符集與編碼字符集不一致導致漢字變成了看不懂的亂碼,而且是一個漢字字符變成兩個亂碼字符。
5.2 一個漢字變成一個問號
例如,字符串“淘!我喜歡!”變成了“??????”編碼過程如下圖所示:
將中文和中文符號經過不支持中文的 ISO-8859-1 編碼后,所有字符變成了“?”,這是因為用 ISO-8859-1 進行編解碼時遇到不在碼值范圍內的字符時統一用 3f 表示,這也就是通常所說的“黑洞”,所有 ISO-8859-1 不認識的字符都變成了“?”。
5.3 一個漢字變成兩個問號
例如,字符串“淘!我喜歡!”變成了“????????????”編碼過程如下圖所示:
這種情況比較復雜,中文經過多次編碼,但是其中有一次編碼或者解碼不對仍然會出現中文字符變成“?”現象,出現這種情況要仔細查看中間的編碼環節,找出出現編碼錯誤的地方。
5.4 一種不正常的正確編碼
還有一種情況是在我們通過 request.getParameter 獲取參數值時,當我們直接調用
String value=request.getParameter(name); 會出現亂碼,但是如果用下面的方式
String value=String(request.getParameter(name).getBytes(" ISO-8859-1"), "GBK");
解析時取得的 value 會是正確的漢字字符,這種情況是怎么造成的呢?
看下如所示:
這種情況是這樣的,ISO-8859-1 字符集的編碼范圍是 0000-00FF,正好和一個字節的編碼范圍相對應。這種特性保證了使用 ISO-8859-1 進行編碼和解碼可以保持編碼數值“不變”。雖然中文字符在經過網絡傳輸時,被錯誤地“拆”成了兩個歐洲字符,但由于輸出時也是用 ISO-8859-1,結果被“拆”開的中文字的兩半又被合并在一起,從而又剛好組成了一個正確的漢字。雖然最終能取得正確的漢字,但是還是不建議用這種不正常的方式取得參數值,因為這中間增加了一次額外的編碼與解碼,這種情況出現亂碼時因為 Tomcat 的配置文件中 useBodyEncodingForURI 配置項沒有設置為”true”,從而造成第一次解析式用 ISO-8859-1 來解析才造成亂碼的。
6、總結
本文首先總結了幾種常見編碼格式的區別,然后介紹了支持中文的幾種編碼格式,并比較了它們的使用場景。接著介紹了 Java 那些地方會涉及到編碼問題,已經 Java 中如何對編碼的支持。并以網絡 I/O 為例重點介紹了 HTTP 請求中的存在編碼的地方,以及 Tomcat 對 HTTP 協議的解析,最后分析了我們平常遇到的亂碼問題出現的原因。
綜上所述,要解決中文問題,首先要搞清楚哪些地方會引起字符到字節的編碼以及字節到字符的解碼,最常見的地方就是讀取會存儲數據到磁盤,或者數據要經過網絡傳輸。然后針對這些地方搞清楚操作這些數據的框架的或系統是如何控制編碼的,正確設置編碼格式,避免使用軟件默認的或者是操作系統平臺默認的編碼格式。
、FreeMaker介紹
FreeMarker是一款免費的Java模板引擎,是一種基于模板和數據生成文本(HMLT、電子郵件、配置文件、源代碼等)的工具,它不是面向最終用戶的,而是一款程序員使用的組件。
FreeMarker最初設計是用來在MVC模式的Web開發中生成HTML頁面的,所以沒有綁定Servlet或任意Web相關的東西上,所以它可以運行在非Web應用環境中。
發展史
FreeMarker第一版在1999年未就發布了,2002年初使用JavaCC(Java Compiler Compiler是一個用Java開發的語法分析生成器)重寫了FreeMarker的核心代碼,2015年FreeMarker代碼遷移到了Apache下。
GitHub地址:https://github.com/apache/freemarker
工作原理
FreeMarker模板存儲在服務器上,當有用戶訪問的時候,FreeMarker會查詢出相應的數據,替換模板中的標簽,生成最終的HTML返回給用戶,如下圖:
二、FreeMarker基礎使用
基礎使用分為3部分,這3部分組成了FreeMarker:
指令是FreeMarker用來識別轉換的特殊標簽,表達式是標簽里具體的語法實現,其他部分是一些不好分類的模板。
2.1 指令
使用FTL(freemarker template language)標簽來調用指令。
指令速覽:
下來我們分別來看每個指令對應具體使用。
2.1.1 assign 代碼聲明
assign 分為變量和代碼片段聲明兩種。
2.1.1.1 變量聲明
可以是單變量聲明,或多變量聲明,下面是多變量聲明的示例:
<#assign name="adam" age=18 "sex"="man">
${name} - ${age} - ${"sex"}
單個變量的話,只寫一個就可以了。
2.1.1.2 代碼片段聲明
<#assign code>
<#list ["java","golang"] as c>
${c}
</#list>
</#assign>
${code}
其中 ${code} 是用來執行方法的,如果不調用話,代碼片段不會執行。
2.1.2 attempt, recover 異常指令
attempt(嘗試), recover(恢復)指令類似于程序的try catch,示例如下:
<#attempt>
i am ${name}
<#recover>
error name
</#attempt>
如果有變量“name”就會正常顯示,顯示“i am xxx”,如果沒有變量就會顯示“error name”。
2.1.3 compress 壓縮代碼移除空白行
<#compress>
1 2 3 4 5
?
test only
?
I said, test only
</#compress>
?
1 2 3 4 5
?
?
test only
?
I said, test only
效果如下:
對空白不敏感的格式,移除空白行還是挺有用的功能。
2.1.4 escape, noescape 轉義,不轉義
2.1.4.1 escape使用
<#escape x as x?html>
${firstName}
${lastName}
</#escape>
上面的代碼,類似于:
${firstName?html}
${lastName?html}
Java代碼:
@RequestMapping("/")
public ModelAndView index() {
ModelAndView modelAndView=new ModelAndView("/index");
modelAndView.addObject("firstName", "<span style='color:red'>firstName</span>");
modelAndView.addObject("lastName", "lastName");
return modelAndView;
}
最終的效果是:
2.1.4.2 “?html”語法解析
單問號后面跟的是操作函數,類似于Java中的方法名,html屬于內建函數的一個,表示字符串會按照HTML標記輸出,字符替換規則如下:
2.1.4.3 noescape使用
HTML代碼:
<#escape x as x?html>
<#noescape>
${firstName}
</#noescape>
${lastName}
</#escape>
Java代碼:
@RequestMapping("/")
public ModelAndView index() {
ModelAndView modelAndView=new ModelAndView("/index");
modelAndView.addObject("firstName", "<span style='color:red'>firstName</span>");
modelAndView.addObject("lastName", "lastName");
return modelAndView;
}
最終效果:
2.1.5 function, return 方法聲明
代碼格式:
<#function name param1 param2 ... paramN>
...
<#return returnValue>
...
</#function>
示例代碼如下:
<#function sum x y z>
<#return x+y+z>
</#function>
?
${sum(5,5,5)}
注意:function如果沒有return是沒有意義的,相當于返回null,而function之中信息是不會打印到頁面的,示例如下:
<#function wantToPrint>
這里的信息是顯示不了的
</#function>
?
<#if wantToPrint()??>
Message:${wantToPrint()}
</#if>
“??”用于判斷值是否是null,如果為null是不執行的。如果不判null直接使用${}打印,會報模板錯誤,效果如下:
2.1.6 global 全局代碼聲明
語法如下:
<#global name=value>
或
<#global name1=value1 name2=value2 ... nameN=valueN>
或
<#global name>
capture this
</#global>
global使用和assign用法類似,只不過global聲明是全局的,所有的命名空間都是可見的。
2.1.7 if elseif else 條件判斷
語法如下:
<#if condition>
...
<#elseif condition2>
...
<#elseif condition3>
...
...
<#else>
...
</#if>
示例如下:
<#assign x=1 >
<#if x==1>
x is 1
<#elseif x==2>
x is 2
<#else>
x is not 1
</#if>
2.1.8 import 引入模板
語法: <#import path as hash>
示例如下
footer.ftl 代碼如下:
<html>
<head>
<title>王磊的博客</title>
</head>
<body>
this is footer.ftl
<#assign copy="來自 王磊的博客">
</body>
</html>
index.ftl 代碼如下:
<html>
<head>
<title>王磊的博客</title>
</head>
<body>
<#import "footer.ftl" as footer>
${footer.copy}
</body>
</html>
最終輸出內容:
來自 王磊的博客
2.1.9 include 嵌入模板
語法: <#include path>
示例如下
footer.ftl 代碼如下:
<html>
<head>
<title>王磊的博客</title>
</head>
<body>
this is footer.ftl
<#assign copy="來自 王磊的博客">
</body>
</html>
index.ftl 代碼如下:
<html>
<head>
<title>王磊的博客</title>
</head>
<body>
<#include "footer.ftl">
</body>
</html>
最終內容如下:
this is footer.ftl
2.1.10 list, else, items, sep, break 循環
2.1.10.1 正常循環
輸出1-3的數字,如果等于2跳出循環,代碼如下:
<#list 1..3 as n>
${n}
<#if n==2>
<#break>
</#if>
</#list>
注意:“1..3”等于[1,2,3]。
結果: 1 2
2.1.10.2 使用items輸出
示例如下:
<#list 1..3>
<ul>
<#items as n>
<li>${n}</li>
</#items>
</ul>
</#list>
2.1.10.3 sep 使用
跳過最后一項
<#list 1..3 as n>
${n}
<#sep>,</#sep>
</#list>
最終結果:1 , 2 , 3
2.1.10.4 數組最后一項
代碼如下:
<#list 1..3 as n>
${n}
<#if !n_has_next>
最后一項
</#if>
</#list>
使用“變量_has_next”判斷是否還有下一個選項,來找到最后一項,最終的結果:1 2 3 最后一項
2.1.11 macro 宏
宏:是一個變量名的代碼片段,例如:
<#macro sayhi name>
Hello, ${name}
</#macro>
?
<@sayhi "Adam" />
相當于聲明了一個名稱為“sayhi”有一個參數“name”的宏,使用自定義標簽“@”調用宏。
輸出的結果: Hello, Adam
2.1.12 switch, case, defalut, break 多條件判斷
示例代碼如下:
<#assign animal="dog" >
<#switch animal>
<#case "pig">
This is pig
<#break>
<#case "dog">
This is dog
<#break>
<#default>
This is Aaimal
</#switch>
2.1.13 擴展知識
指令自動忽略空格特性
FreeMarker會忽略FTL標簽中的空白標記,所以可以直接寫:
<#list ["老王","老李","老張"]
as
p>
${p}
</#list>
即使是這個格式也是沒有任何問題的,FreeMarker會正常解析。
2.2 表達式
2.2.1 字符串拼接
字符拼接代碼:
<#assign name="ABCDEFG">
${"Hello, ${name}"}
結果:Hello, ABCDEFG
2.2.2 算術運算
2.2.2.1 算術符
算術符有五種:
示例代碼:
${100 - 10 * 20}
輸出:
-100
2.2.2.2 數值轉換
${1.999?int}
輸出:
1
注意:數值轉換不會進行四舍五入,會舍棄小數點之后的。
2.2.3 內建函數(重點)
內建函數:相當于我們Java類里面的內置方法,非常常用,常用的內建函數有:時間內建函數、字符內建函數、數字內建函數等。
2.2.3.1 單個問號和兩個問號的使用和區別
單問號:在FreeMarker中用單個問號,來調用內建函數,比如: ${"admin"?length} 查看字符串“admin”的字符長度,其中length就是字符串的內建函數。
雙引號:表示用于判斷值是否為null,比如:
<#if admin??>
Admin is not null
</#if>
2.2.3.2 字符串內建函數
2.2.3.2.1 是否包含判斷
使用contains判斷,代碼示例:
<#if "admin"?contains("min")>
min
<#else >
not min
</#if>
輸出:
min
2.2.3.2.2 大小寫轉換
示例代碼:
<#assign name="Adam">
${name?uncap_first}
${name?upper_case}
${name?cap_first}
${name?lower_case}
輸出:
adam ADAM Adam adam
更多的字符串內建函數:https://freemarker.apache.org/docs/ref_builtins_string.html
2.2.3.3 數字內建函數
示例代碼:
${1.23569?string.percent}
${1.23569?string["0.##"]}
${1.23569?string["0.###"]}
輸出:
124% 1.24 1.236
注意:
2.2.3.4 時間內建函數
2.2.3.4.1 時間戳轉換為任何時間格式
代碼:
<#assign timestamp=1534414202000>
${timestamp?number_to_datetime?string["yyyy/MM/dd HH:mm"]}
輸出:
2018/08/16 18:10
2.2.3.4.2 時間格式化
示例代碼:
<#assign nowTime=.now>
${nowTime} <br />
${nowTime?string["yyyy/MM/dd HH:mm"]} <br />
輸出:
2018-8-16 18:33:50
2018/08/16 18:33
更多內建方法:https://freemarker.apache.org/docs/ref_builtins.html
三、Spring Boot 集成
3.1 集成環境
3.2 集成步驟
3.2.1 pom.xml 添加FreeMaker依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
3.2.2 application.properties 配置模板
主要配置,如下:
## Freemarker 配置
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.request-context-attribute=request
spring.freemarker.suffix=.ftl
配置項類型默認值建議值說明spring.freemarker.template-loader-pathStringclasspath:/templates/默認模版存放路徑spring.freemarker.cachebooltrue默認是否開啟緩存,生成環境建議開啟spring.freemarker.charsetString-UTF-8編碼spring.freemarker.content-typeStringtext/htmltext/htmlcontent-type類型spring.freemarker.suffixString.ftl.ftl模板后綴spring.freemarker.expose-request-attributesboolfalsefalse設定所有request的屬性在merge到模板的時候,是否要都添加到model中spring.freemarker.expose-session-attributesboolfalsefalse設定所有HttpSession的屬性在merge到模板的時候,是否要都添加到model中.spring.freemarker.request-context-attributeString-requestRequestContext屬性的名稱
更多配置:
# FREEMARKER (FreeMarkerProperties)
spring.freemarker.allow-request-override=false # Whether HttpServletRequest attributes are allowed to override (hide) controller generated model attributes of the same name.
spring.freemarker.allow-session-override=false # Whether HttpSession attributes are allowed to override (hide) controller generated model attributes of the same name.
spring.freemarker.cache=false # Whether to enable template caching.
spring.freemarker.charset=UTF-8 # Template encoding.
spring.freemarker.check-template-location=true # Whether to check that the templates location exists.
spring.freemarker.content-type=text/html # Content-Type value.
spring.freemarker.enabled=true # Whether to enable MVC view resolution for this technology.
spring.freemarker.expose-request-attributes=false # Whether all request attributes should be added to the model prior to merging with the template.
spring.freemarker.expose-session-attributes=false # Whether all HttpSession attributes should be added to the model prior to merging with the template.
spring.freemarker.expose-spring-macro-helpers=true # Whether to expose a RequestContext for use by Spring's macro library, under the name "springMacroRequestContext".
spring.freemarker.prefer-file-system-access=true # Whether to prefer file system access for template loading. File system access enables hot detection of template changes.
spring.freemarker.prefix=# Prefix that gets prepended to view names when building a URL.
spring.freemarker.request-context-attribute=# Name of the RequestContext attribute for all views.
spring.freemarker.settings.*=# Well-known FreeMarker keys which are passed to FreeMarker's Configuration.
spring.freemarker.suffix=.ftl # Suffix that gets appended to view names when building a URL.
spring.freemarker.template-loader-path=classpath:/templates/ # Comma-separated list of template paths.
spring.freemarker.view-names=# White list of view names that can be resolved.
3.2.3 編寫HTML代碼
<html>
<head>
<title>王磊的博客</title>
</head>
<body>
<div>
Hello,${name}
</div>
</body>
</html>
3.2.4 編寫Java代碼
新建index.java文件,Application.java(入口文件)代碼不便,index.java代碼如下:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
?
@Controller
@RequestMapping("/")
public class Index {
@RequestMapping("/")
public ModelAndView index() {
ModelAndView modelAndView=new ModelAndView("/index");
modelAndView.addObject("name", "老王");
return modelAndView;
}
}
?
關鍵代碼解讀:
3.2.5 運行
執行上面4個步驟之后,就可以運行這個Java項目了,如果是IDEA使用默認快捷鍵“Shift + F10”啟動調試,在頁面訪問:http://localhost:8080/ 就可看到如下效果:
四、參考資料
FreeMarker官方文檔:https://freemarker.apache.org/
FreeMarker翻譯的中文網站:http://freemarker.foofun.cn/toc.html
近看了一些關于JavaScript的測試腳本,覺得JS 中問號的用法還是蠻有意思的,于是做了一下總結,在這里分享給大家!JS中的問號大概有三種用法,分別是:空值合并操作符、可選鏈操作符和三目運算。
空值合并操作符??是一個邏輯操作符,當左側的操作數為 null 或者 undefined 時,返回其右側操作數,否則返回左側操作數。
例如
console.log(null ?? "xx")
輸出 xx
console.log(1 ?? "xx")
輸出 1
可選鏈操作符(?.)可選鏈操作符允許讀取位于連接對象鏈深處的屬性的值,而不必明確驗證鏈中的每個引用是否有效。 使用它的好處是引用為null 或者 undefined的情況下不會引起錯誤。
語法:obj?.prop obj?.[expr] arr?.[index] func?.(args)
例如
var obj={a:{b:1}}
console.log(obj?.a?.b)
輸出1
console.log(obj?.a?.c)
輸出 undefined
這是三目運算,具體表達式是(condition ? exprIfTrue : exprIfFalse)
該表達式的含義是 條件condition是真,則執行exprIfTrue ,否則執行exprIfFalse
舉個例子大家就懂了
var n=10;
console.log((n >=11) ? "a" : "b");
輸出b
當 var n=12;
輸出a
如果您還知道哪些JS 中關于問號的特殊用法歡迎留言討論。如果文章幫到了您,勞煩點贊轉發!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。