Java擴展Nginx之六:兩大filter

程序員欣宸 發佈 2024-03-24T19:33:06.045572+00:00

本文是《Java擴展Nginx》系列的第六篇,前文的五大handler形成了nginx-clojure開發的基本框架,初步評估已經可以支撐簡單的需求開發了,但nginx-clojure並未止步於handler,還提供了豐富的擴展能力,本篇的兩大filter就是比較常用的能力。

歡迎訪問我的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》系列還沒結束呢,還有精彩的內容會陸續登場,敬請關注,欣宸原創必不辜負您的期待~

源碼下載

  • 《Java擴展Nginx》的完整源碼可在gitHub下載到,地址和連結信息如下表所示(https://github.com/zq2599/blog_demos):
  • 這個git項目中有多個文件夾,本篇的源碼在nginx-clojure-tutorials文件夾下的filter-demo子工程中,如下圖紅框所示:
  • 本篇涉及到nginx.conf的修改,完整的參考在此:https://raw.githubusercontent.com/zq2599/blog_demos/master/nginx-clojure-tutorials/files/nginx.conf

歡迎關注頭條號:程式設計師欣宸

  • 學習路上,你不孤單,欣宸原創一路相伴...
關鍵字: