從零手寫實現 nginx-29-try_files 指令

老马啸西风發表於2024-07-16

前言

大家好,我是老馬。很高興遇到你。

我們為 java 開發者實現了 java 版本的 nginx

https://github.com/houbb/nginx4j

如果你想知道 servlet 如何處理的,可以參考我的另一個專案:

手寫從零實現簡易版 tomcat minicat

手寫 nginx 系列

如果你對 nginx 原理感興趣,可以閱讀:

從零手寫實現 nginx-01-為什麼不能有 java 版本的 nginx?

從零手寫實現 nginx-02-nginx 的核心能力

從零手寫實現 nginx-03-nginx 基於 Netty 實現

從零手寫實現 nginx-04-基於 netty http 出入參最佳化處理

從零手寫實現 nginx-05-MIME型別(Multipurpose Internet Mail Extensions,多用途網際網路郵件擴充套件型別)

從零手寫實現 nginx-06-資料夾自動索引

從零手寫實現 nginx-07-大檔案下載

從零手寫實現 nginx-08-範圍查詢

從零手寫實現 nginx-09-檔案壓縮

從零手寫實現 nginx-10-sendfile 零複製

從零手寫實現 nginx-11-file+range 合併

從零手寫實現 nginx-12-keep-alive 連線複用

從零手寫實現 nginx-13-nginx.conf 配置檔案介紹

從零手寫實現 nginx-14-nginx.conf 和 hocon 格式有關係嗎?

從零手寫實現 nginx-15-nginx.conf 如何透過 java 解析處理?

從零手寫實現 nginx-16-nginx 支援配置多個 server

從零手寫實現 nginx-17-nginx 預設配置最佳化

從零手寫實現 nginx-18-nginx 請求頭+響應頭操作

從零手寫實現 nginx-19-nginx cors

從零手寫實現 nginx-20-nginx 佔位符 placeholder

從零手寫實現 nginx-21-nginx modules 模組資訊概覽

從零手寫實現 nginx-22-nginx modules 分模組載入最佳化

從零手寫實現 nginx-23-nginx cookie 的操作處理

從零手寫實現 nginx-24-nginx IF 指令

從零手寫實現 nginx-25-nginx map 指令

從零手寫實現 nginx-26-nginx rewrite 指令

從零手寫實現 nginx-27-nginx return 指令

從零手寫實現 nginx-28-nginx error_pages 指令

從零手寫實現 nginx-29-nginx try_files 指令

nginx try_files 指令是什麼?

Nginx 的 try_files 指令用於嘗試一個或多個檔案路徑或 URI,以處理客戶端請求。如果找到一個檔案或 URI 存在,則返回該檔案或執行該 URI。如果沒有找到,則返回一個指定的錯誤碼或重定向到一個預設的處理程式。try_files 指令通常用於靜態檔案服務、動態內容處理和錯誤處理。

語法

try_files file1 [file2 ... filen] uri|=code;
  • file1, file2, ... filen: 依次檢查這些檔案或目錄的存在性。
  • uri: 如果前面的檔案或目錄都不存在,重定向到指定的 URI。
  • =code: 如果前面的檔案或目錄都不存在,返回指定的 HTTP 狀態碼。

示例

靜態檔案服務

優先返回靜態檔案,如果檔案不存在,則返回 404 錯誤。

server {
    listen 80;
    server_name example.com;

    location / {
        root /var/www/html;
        try_files $uri $uri/ =404;
    }
}

動態內容處理

優先返回靜態檔案,如果檔案不存在,則將請求重定向到一個 PHP 處理程式。

server {
    listen 80;
    server_name example.com;

    location / {
        root /var/www/html;
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        root /var/www/html;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

單頁應用(SPA)

所有路徑都指向入口的 index.html 檔案,以支援前端路由。

server {
    listen 80;
    server_name example.com;

    location / {
        root /var/www/html;
        try_files $uri $uri/ /index.html;
    }
}

工作原理

  1. 嘗試多個檔案路徑:按順序檢查指定的檔案或目錄是否存在。如果找到一個存在的檔案或目錄,則立即返回該檔案或目錄。
  2. 重定向到 URI:如果所有指定的檔案或目錄都不存在,則重定向到一個指定的 URI。這通常用於將請求傳遞給動態處理程式。
  3. 返回指定的 HTTP 狀態碼:如果所有指定的檔案或目錄都不存在,並且未指定 URI,則返回指定的 HTTP 狀態碼(如 404)。

優點

  • 靈活性高:可以輕鬆處理靜態檔案、動態內容和錯誤處理。
  • 配置簡潔:透過一條指令即可實現多種路徑檢查和處理邏輯。
  • 效能最佳化:優先處理靜態檔案,避免不必要的動態處理,提高響應速度。

缺點

  • 除錯困難:由於會依次檢查多個路徑,除錯和排查問題時可能比較複雜。
  • 靈活性有限:無法進行條件判斷或複雜的重寫規則處理,需要結合其他指令使用。
  • 錯誤處理簡單:只能指定簡單的錯誤處理方式,無法進行復雜的錯誤處理邏輯。

總結

Nginx 的 try_files 指令是一種非常強大和靈活的工具,適用於處理多種請求路徑和檔案檢查需求。

透過適當的配置,可以顯著提高伺服器的效能和響應速度,同時簡化配置檔案的編寫和維護。

java 實現

整體思路

1)遍歷非最後一個的 uri,替換佔位符,判斷檔案是否存在,存在則返回。

2)判斷最後一個 uri 或者 code,直接處理對應的邏輯。

核心邏輯

/**
 * try_files path1 path2 ... final;
 *
 * - `path1`, `path2`, ...:要檢查的檔案或 URI 列表。可以是相對路徑或絕對路徑。
 *
 * - `final`:如果前面的所有路徑都不存在,最後一個引數可以是一個 URI,Nginx 將內部重定向到該 URI,或者是一個 HTTP 狀態碼(如 404),用於返回相應的錯誤。
 *
 * @see INginxPlaceholder 佔位符
 * @see com.github.houbb.nginx4j.support.request.dispatch.http.NginxRequestDispatchHttpReturn 設定對應的返回碼 =xxx
 */
public class NginxTryFilesDefault implements INginxTryFiles{

    private static final Log log = LogFactory.getLog(NginxTryFilesDefault.class);

    /**
     * 處理 try_files 指令
     *
     * @param request     請求
     * @param nginxConfig 配置
     * @param context     上下文
     */
    public void tryFiles(FullHttpRequest request,
                         final NginxConfig nginxConfig,
                         NginxRequestDispatchContext context) {
        // 獲取當前的 location
        List<NginxCommonConfigEntry> directiveList = InnerNginxContextUtil.getLocationDirectives(context);
        Map<String, List<NginxCommonConfigEntry>> directiveMap = InnerNginxContextUtil.getLocationDirectiveMap(directiveList);

        List<NginxCommonConfigEntry> tryFiles = directiveMap.get(NginxDirectiveEnum.TRY_FILES.getCode());
        if(CollectionUtil.isEmpty(tryFiles)) {
            return;
        }

        NginxCommonConfigEntry firstEntry = tryFiles.get(0);

        // 遍歷非 final 的檔案資訊
        String notFinalUri = getNotFinalMatchedFileUri(firstEntry, context);
        if(StringUtil.isNotEmpty(notFinalUri)) {
            request.setUri(notFinalUri);
            return;
        }

        // 判斷 final 變數
        final String lastUri = firstEntry.getValues().get(firstEntry.getValues().size()-1);
        if(lastUri.startsWith("=")) {
            // 拆分為 return
            NginxReturnResult result = new NginxReturnResult();
            result.setCode(Integer.parseInt(lastUri.substring(1)));
            result.setValue("try_files final");
            context.setNginxReturnResult(result);
            return;
        }

        String lastReplaceUri = replacePlaceholders(lastUri, context.getPlaceholderMap());
        request.setUri(lastReplaceUri);
    }

    /**
     * 獲取匹配的檔案 url
     * @param entry 實體
     * @param context 上下文
     * @return 結果
     */
    private String getNotFinalMatchedFileUri(final NginxCommonConfigEntry entry,
                                            NginxRequestDispatchContext context) {
        List<String> values = entry.getValues();

        for(int i = 0; i < values.size()-1; i++) {
            String replacedUri = getMatchedFileUri(values.get(i), context);

            if(StringUtil.isNotEmpty(replacedUri)) {
                return replacedUri;
            }
        }

        return null;
    }

    private String getMatchedFileUri(final String requestUri,
                                     NginxRequestDispatchContext context) {
        final Map<String, Object> replaceMap = context.getPlaceholderMap();

        String replacedUri = replacePlaceholders(requestUri, replaceMap);

        // 判斷檔案是否存
        File file = InnerFileUtil.getTargetFile(replacedUri, context);
        if(file.exists()) {
            log.info("Nginx getMatchedFileUri file={}", file.getAbsolutePath());
            return replacedUri;
        }

    return null;
    }

    /**
     * 替換字串中的佔位符
     *
     * @param input    使用者輸入的字串,包含佔位符
     * @param variables 儲存佔位符及其替換值的 Map
     * @return 替換後的字串
     */
    public static String replacePlaceholders(String input, Map<String, Object> variables) {
        // 使用 StringBuilder 來構建替換後的字串
        StringBuilder result = new StringBuilder(input);

        // 遍歷 Map 進行替換
        for (Map.Entry<String, Object> entry : variables.entrySet()) {
            String placeholder = entry.getKey();
            String replacement = String.valueOf(entry.getValue());

            // 使用 String 的 replace 方法替換所有佔位符
            int start = result.indexOf(placeholder);
            while (start != -1) {
                result.replace(start, start + placeholder.length(), replacement);
                start = result.indexOf(placeholder, start + replacement.length());
            }
        }

        return result.toString();
    }

}

小結

Nginx 的 try_files 指令是一個強大且靈活的工具,用於處理靜態檔案、友好 URL 重寫和自定義錯誤處理。

它透過按順序檢查多個路徑並執行相應操作,使配置檔案更加簡潔和高效。

主要用途

  • 靜態檔案服務:首先嚐試提供靜態檔案,如果找不到則執行其他操作。
  • 友好 URL 重寫:用於支援偽靜態 URL,將請求重寫到實際的檔案路徑或動態指令碼。
  • 錯誤處理:如果所有嘗試的檔案都不存在,則返回一個特定的錯誤頁面。

工作機制

  • try_files 按順序檢查列出的每個路徑。
  • 如果找到了一個存在的檔案或目錄,則立即停止進一步檢查並使用該檔案或目錄響應請求。
  • 如果所有列出的路徑都不存在,則執行最後指定的 URI,這通常是一個錯誤頁面或其他處理邏輯。

優點

  • 高效靜態檔案處理:減少了不必要的動態請求處理。
  • 靈活的錯誤處理:可以輕鬆配置自定義錯誤頁面或重寫規則。
  • 簡潔配置:使複雜的檔案查詢和重寫邏輯更加直觀和易於管理。

相關文章