Java擴充套件Nginx之六:兩大filter

2023-07-16 12:00:47

歡迎存取我的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處理的過濾器,它具有如下特點:
  1. header filter是location級別的設定,可以開發一個header filter,然後設定在不同的location中使用
  2. header filter必須實現NginxJavaHeaderFilter介面,功能程式碼寫在doFilter方法中
  3. doFilter方法如果返回PHASE_DONE,nginx-clojure框架會繼續執行其他的filter和handler,如果返回的不是PHASE_DONE,nginx-clojure框架就會把當前filter當做普通的content handler來對待,將doFilter的返回值立即返回給使用者端
  4. 官方建議用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-HeaderServer
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-hellofilter-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相對簡單一些,以下幾個規則要注意:
  1. 繼承抽象類StringFacedJavaBodyFilter,
  2. 處理一次web請求的時候,doFilter方法可能被呼叫多次,有個名為isLast的入參,作用是標記當前呼叫是不是最後一次(true表示最後一次)
  3. doFilter方法的返回值與之前的NginxJavaRingHandler.invoke方法類似,是個一維陣列,只有三個元素:status, headers, filtered_chunk,一旦status值不為空,nginx-clojure框架會用這次doFilter的返回值作為最後一次呼叫,返回給使用者端
  4. 結合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的場景下,有以下特點:
  1. 實現介面NginxJavaBodyFilter(注意區別:字串body的filter是繼承抽象類StringFacedJavaBodyFilter),
  2. 處理一次web請求的時候,doFilter方法可能被呼叫多次,有個名為isLast的入參,作用是標記當前呼叫是不是最後一次(true表示最後一次)
  3. doFilter方法的返回值與之前的NginxJavaRingHandler.invoke方法類似,是個一維陣列,只有三個元素:status, headers, filtered_chunk,一旦status值不為空,nginx-clojure框架會用這次doFilter的返回值作為最後一次呼叫,返回給使用者端
  4. 結合2和3的特性,我們在編碼時要注意了:假設一次web請求,doFilter會被呼叫10次(每次body入參的值都是整個response body的一部分),那麼前9次的isLast都等於false,第10次的isLast等於true,假設第1次呼叫doFilter方法的時候返回的status不為空,就會導致後面9次的doFilter都不再被呼叫了!
  5. doFilter方法有個入參名為bodyChunk,這表示真實響應body的一部分(假設一次web請求有十次doFilter呼叫,可以將每次doFilter的bodyChunk認為是完整響應body的十分之一),這裡有個重點注意的地方:bodyChunk只在當前doFilter執行過程中有效,不要將bodyChunk儲存下來用於其他地方(例如放入body filter的成員變數中)
  6. 繼續看doFilter方法的返回值,剛剛提到返回值是一維陣列,只有三個元素:status, headers, filtered_chunk,對於status和headers,如果之前已經設定好了(例如content handler或者header filter中),那麼此時返回的status和headers值就會被忽略掉(也就是說,其實nginx-clojure框架只判斷status是否為空,用於結束body filter的處理流程,至於status的具體值是多少並不關心)
  7. 再看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》系列還沒結束呢,還有精彩的內容會陸續登場,敬請關注,欣宸原創必不辜負您的期待~

原始碼下載

名稱 連結 備註
專案主頁 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協定

歡迎關注部落格園:程式設計師欣宸

學習路上,你不孤單,欣宸原創一路相伴...