### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《Java擴展Nginx》系列的第 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本文是《Java擴展Nginx》系列的第六篇,前文的五大handler形成了nginx-clojure開發的基本框架,初步評估已經可以支撐簡單的需求開發了,但nginx-clojure並未止步於handler,還提供了豐富的擴展能力,本篇的兩大filter就是比較常用的能力
- filter一共有兩種:header filter和body filter,nginx-clojure對他們的定位分別是對header的處理和對body的處理,接下來分別細說
Nginx Header Filter
- header filter顧名思義,是用於header處理的過濾器,它具有如下特點:
- header filter是location級別的配置,可以開發一個header filter,然後配置在不同的location中使用
- header filter必須實現NginxJavaHeaderFilter介面,功能代碼寫在doFilter方法中
- doFilter方法如果返回PHASE_DONE,nginx-clojure框架會繼續執行其他的filter和handler,如果返回的不是PHASE_DONE,nginx-clojure框架就會把當前filter當做普通的content handler來對待,將doFilter的返回值立即返回給客戶端
- 官方建議用header filter來動態處理response的header(增加、刪除、修改header項)
-
接下來開發一個header filter試試,還記得《Java擴展Nginx之一:你好,nginx-clojure》一文中的/java介面嗎,那是個最簡單的helloworld級別的location,content handler是HelloHandler.java,稍後驗證header filter功能的時候會用到它
-
先用postman請求/java介面,看看沒有使用header filter之前的response header,如下圖:
-
接下來新增一個location,配置如下,content handler還是HelloHandler.java,增加了header_filter_type和header_filter_name:
location /headerfilterdemo {
content_handler_type 'java';
content_handler_name 'com.bolingcavalry.simplehello.HelloHandler';
# header filter的類型是java
header_filter_type 'java';
# header
header_filter_name 'com.bolingcavalry.filterdemo.RemoveAndAddMoreHeaders';
}
- 執行header filter功能的類是RemoveAndAddMoreHeaders.java,如下所示,修改了Content-Type,還增加了兩個header項Xfeep-Header和Server:
package com.bolingcavalry.filterdemo;
import nginx.clojure.java.Constants;
import nginx.clojure.java.NginxJavaHeaderFilter;
import java.util.Map;
public class RemoveAndAddMoreHeaders implements NginxJavaHeaderFilter {
@Override
public Object[] doFilter(int status, Map<String, Object> request, Map<String, Object> responseHeaders) {
// 先刪再加,相當於修改了Content-Type的值
responseHeaders.remove("Content-Type");
responseHeaders.put("Content-Type", "text/html");
// 增加兩個header
responseHeaders.put("Xfeep-Header", "Hello2!");
responseHeaders.put("Server", "My-Test-Server");
// 返回PHASE_DONE表示告知nginx-clojure框架,當前filter正常,可以繼續執行其他的filter和handler
return Constants.PHASE_DONE;
}
}
- 將simple-hello和filter-demo兩個maven工程都編譯構建,會得到simple-hello-1.0-SNAPSHOT.jar和filter-demo-1.0-SNAPSHOT.jar這兩個jar,將其都放入nginx/jars目錄下,然後重啟nginx
- 用postman請求/headerfilterdemo,並將響應的header與/java做對比,如下圖,可見先刪再加、添加都正常,另外,由於Server配置項本來就存在,所以filter中的put操作的結果就是修改了配置項的值:
- 到這裡header filter就介紹完了,接下來要看的是body filter,顧名思義,這是用於處理響應body的過濾器,與header filter不同的是,由於響應body有不同的類型,因此body filter也不能一概而論,需要分場景開發和使用
Nginx Body Filter的第一個場景:字元串body(string faced Java body filter)
- Body Filter的作用很明確:修改原響應body的值,然後返回給客戶端
- 如果響應的body是字元串,那麼body filter相對簡單一些,以下幾個規則要註意:
- 繼承抽象類StringFacedJavaBodyFilter,
- 處理一次web請求的時候,doFilter方法可能被調用多次,有個名為isLast的入參,作用是標記當前調用是不是最後一次(true表示最後一次)
- doFilter方法的返回值與之前的NginxJavaRingHandler.invoke方法類似,是個一維數組,只有三個元素:status, headers, filtered_chunk,一旦status值不為空,nginx-clojure框架會用這次doFilter的返回值作為最後一次調用,返回給客戶端
- 結合2和3的特性,我們在編碼時要註意了:假設一次web請求,doFilter會被調用10次(每次body入參的值都是整個response body的一部分),那麼前9次的isLast都等於false,第10次的isLast等於true,假設第1次調用doFilter方法的時候返回的status不為空,就會導致後面9次的doFilter都不再被調用了!
- 接下來的實戰再次用到之前的HelloHandler.java作為content handler,因為它返回的body是字元串
- 先增加一個location配置,body_filter_type和body_filter_name是body filter的配置項:
# body filter的demo,response body是字元串類型
location /stringbodyfilterdemo {
content_handler_type 'java';
content_handler_name 'com.bolingcavalry.simplehello.HelloHandler';
# body filter的類型是java
body_filter_type 'java';
# body filter的類
body_filter_name 'com.bolingcavalry.filterdemo.StringFacedUppercaseBodyFilter';
}
- StringFacedUppercaseBodyFilter.java源碼如下(請重點閱讀註釋),可見該filter的功能是將原始body改為大寫,並且,代碼中檢查了isLast的值,isLast等於false的時候,status的值保持為null,這樣才能確保doFilter的調用不會提前結束,如此才能返回完整的body:
package com.bolingcavalry.filterdemo;
import nginx.clojure.java.StringFacedJavaBodyFilter;
import java.io.IOException;
import java.util.Map;
public class StringFacedUppercaseBodyFilter extends StringFacedJavaBodyFilter {
@Override
protected Object[] doFilter(Map<String, Object> request, String body, boolean isLast) throws IOException {
if (isLast) {
// isLast等於true,表示當前web請求過程中最後一次調用doFilter方法,
// body是完整response body的最後一部分,
// 此時返回的status應該不為空,這樣nginx-clojure框架就會完成body filter的執行流程,將status和聚合後的body返回給客戶端
return new Object[] {200, null, body.toUpperCase()};
}else {
// isLast等於false,表示當前web請求過程中,doFilter方法還會被繼續調用,當前調用只是多次中的一次而已,
// body是完整response body的其中一部分,
// 此時返回的status應該為空,這樣nginx-clojure框架就繼續body filter的執行流程,繼續調用doFilter
return new Object[] {null, null, body.toUpperCase()};
}
}
}
- 編譯,構建,部署之後,用postman訪問/stringbodyfilterdemo,得到的響應如下,可見body的內容已經全部大寫了,符合預期:
- 接下來要學習的還是body filter,只不過這次的body類型是二進位流(stream faced Java body filter)
Nginx Body Filter的第二個場景:二進位流body(stream faced Java body filter)
- 當響應body是二進位流的時候,如果想對響應body做讀寫操作,nginx-clojure的建議是在body filter中執行,這種body filter是專門用在二進位流body的場景下,有以下特點:
- 實現介面NginxJavaBodyFilter(註意區別:字元串body的filter是繼承抽象類StringFacedJavaBodyFilter),
- 處理一次web請求的時候,doFilter方法可能被調用多次,有個名為isLast的入參,作用是標記當前調用是不是最後一次(true表示最後一次)
- doFilter方法的返回值與之前的NginxJavaRingHandler.invoke方法類似,是個一維數組,只有三個元素:status, headers, filtered_chunk,一旦status值不為空,nginx-clojure框架會用這次doFilter的返回值作為最後一次調用,返回給客戶端
- 結合2和3的特性,我們在編碼時要註意了:假設一次web請求,doFilter會被調用10次(每次body入參的值都是整個response body的一部分),那麼前9次的isLast都等於false,第10次的isLast等於true,假設第1次調用doFilter方法的時候返回的status不為空,就會導致後面9次的doFilter都不再被調用了!
- doFilter方法有個入參名為bodyChunk,這表示真實響應body的一部分(假設一次web請求有十次doFilter調用,可以將每次doFilter的bodyChunk認為是完整響應body的十分之一),這裡有個重點註意的地方:bodyChunk只在當前doFilter執行過程中有效,不要將bodyChunk保存下來用於其他地方(例如放入body filter的成員變數中)
- 繼續看doFilter方法的返回值,剛剛提到返回值是一維數組,只有三個元素:status, headers, filtered_chunk,對於status和headers,如果之前已經設置好了(例如content handler或者header filter中),那麼此時返回的status和headers值就會被忽略掉(也就是說,其實nginx-clojure框架只判斷status是否為空,用於結束body filter的處理流程,至於status的具體值是多少並不關心)
- 再看doFilter方法的返回值的第三個元素filtered_chunk,它可以是以下四種類型之一:
-
File, viz. java.io.File
-
String
-
InputStream
-
Array/Iterable, e.g. Array/List/Set of above types
-
接下來進入實戰了,詳細步驟如下圖:
-
首先是開發一個返回二進位流的web介面,為了簡單省事兒,直接用nginx-clojure的另一個能力來實現:clojure類型的服務,在nginx.conf中添加以下內容即可,代碼雖然不是java但也能勉強看懂(能看懂就行,畢竟不是重點),就是持續寫入1024行字元串,每行的內容都是'123456789':
location /largebody {
content_handler_type 'clojure';
content_handler_code '
(do
(use \'[nginx.clojure.core])
(fn[req]
{:status 200
:headers {}
:body (for [i (range 1024)] "123456789\n")})
)';
}
- 接下來是重點面向二進位流的body filter,StreamFacedBodyFilter.java,用來處理二進位流的body filter,可見這是非常簡單的邏輯,您可以按照實際需要去使用這個InputStream:
package com.bolingcavalry.filterdemo;
import nginx.clojure.NginxChainWrappedInputStream;
import nginx.clojure.NginxClojureRT;
import nginx.clojure.java.NginxJavaBodyFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
public class StreamFacedBodyFilter implements NginxJavaBodyFilter {
@Override
public Object[] doFilter(Map<String, Object> request, InputStream bodyChunk, boolean isLast) throws IOException {
// 這裡僅將二進位文件長度列印到日誌,您可以按照業務實際情況自行修改
NginxClojureRT.log.info("isLast [%s], total [%s]", String.valueOf(isLast), String.valueOf(bodyChunk.available()));
// NginxChainWrappedInputStream的成員變數index記錄的讀取的位置,本次用完後要重置位置,因為doFilter之外的代碼中可能也會讀取bodyChunk
((NginxChainWrappedInputStream)bodyChunk).rewind();
if (isLast) {
// isLast等於true,表示當前web請求過程中最後一次調用doFilter方法,
// body是完整response body的最後一部分,
// 此時返回的status應該不為空,這樣nginx-clojure框架就會完成body filter的執行流程,將status和聚合後的body返回給客戶端
return new Object[] {200, null, bodyChunk};
}else {
// isLast等於false,表示當前web請求過程中,doFilter方法還會被繼續調用,當前調用只是多次中的一次而已,
// body是完整response body的其中一部分,
// 此時返回的status應該為空,這樣nginx-clojure框架就繼續body filter的執行流程,繼續調用doFilter
return new Object[] {null, null, bodyChunk};
}
}
}
- 還要在nginx.conf上做好配置,讓StreamFacedBodyFilter處理/largebody返回的body,如下所示,新增一個介面/streambodyfilterdemo,該介面會直接透傳到/largebody,而且會用StreamFacedBodyFilter處理響應body:
location /streambodyfilterdemo {
# body filter的類型是java
body_filter_type java;
body_filter_name 'com.bolingcavalry.filterdemo.StreamFacedBodyFilter';
proxy_http_version 1.1;
proxy_buffering off;
proxy_pass http://localhost:8080/largebody;
}
- 寫完後,編譯出jar文件,複製到jars目錄下,重啟nginx
- 在postman上訪問/streambodyfilterdemo,響應如下,符合預期:
- 再檢查文件nginx-clojure-0.5.2/logs/error.log,見到了StreamFacedBodyFilter的日誌,證明body filter確實已經生效,另外還可以看出一次請求中,StreamFacedBodyFilter對象的doFilter方法會被neginx-clojure多次調用:
2022-02-15 21:34:38[info][23765][main]isLast [false], total [3929]
2022-02-15 21:34:38[info][23765][main]isLast [false], total [4096]
2022-02-15 21:34:38[info][23765][main]isLast [false], total [2215]
2022-02-15 21:34:38[info][23765][main]isLast [true], total [0]
- 至此,咱們一同完成了header和body的filter和學習實踐,nginx-clojure的大體功能咱們已經瞭解得差不多了,但是《Java擴展Nginx》系列還沒結束呢,還有精彩的內容會陸續登場,敬請關註,欣宸原創必不辜負您的期待~
源碼下載
- 《Java擴展Nginx》的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos):
名稱 | 鏈接 | 備註 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 |
git倉庫地址(ssh) | [email protected]:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 |
-
這個git項目中有多個文件夾,本篇的源碼在nginx-clojure-tutorials文件夾下的filter-demo子工程中,如下圖紅框所示:
-
本篇涉及到nginx.conf的修改,完整的參考在此:https://raw.githubusercontent.com/zq2599/blog_demos/master/nginx-clojure-tutorials/files/nginx.conf