全部產品
Search
文件中心

Realtime Compute for Apache Flink:自訂表格值函數(UDTF)

更新時間:Sep 22, 2026

本文為您介紹如何為Flink自訂表格值函數(UDTF)開發、註冊和使用流程。

定義

自訂表格值函數(UDTF),自訂表格值函數,將0個、1個或多個標量值作為輸入參數(可以是變長參數)。與自訂的純量涵式類似,但與純量涵式不同。資料表值函式可以返回任意數量的行作為輸出,而不僅是1個值。返回的行可以由1個或多個列組成。調用一次函數輸出多行或多列資料。詳情參見User-defined Functions。

UDTF開發

說明

Flink為您提供了UDF樣本,便於您快速開發業務。Flink UDF樣本中包含UDSF、UDAF和UDTF的實現,樣本中已為您配置對應版本的開發環境,您無需進行環境搭建。

  1. 下載並解壓ASI_UDX_Demo樣本到本地。

    說明

    ASI_UDX_Demo屬於第三方搭建的網站,訪問時可能會存在無法開啟或訪問延遲的問題。

    解壓完成後,會產生ASI_UDX-main檔案夾。其中:

    • pom.xml:專案層級的設定檔,主要描述了專案的Maven座標、依賴關係,開發人員需要遵循的規則、缺陷管理系統,組織和Licenses,以及其他所有的專案相關因素。

    • \ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.java:自訂表格值函數(UDTF)樣本的Java代碼。

  2. 在IntelliJ IDEA中,單擊file > open,開啟剛才解壓縮完成的ASI_UDX-main。

  3. 雙擊開啟\ASI_UDX-main\src\main\java\ASI_UDTF後,根據您的業務,修改ASI_UDTF.java檔案內容。

    該樣本中,ASI_UDTF.java已配置了將一行字串按照豎線(|)分割成多列字串的代碼。

    package ASI_UDTF;
    
    import org.apache.flink.api.java.tuple.Tuple2;
    import org.apache.flink.table.functions.TableFunction;
    
    public class ASI_UDTF extends TableFunction<Tuple2<String,String>> {
        public void eval(String str){
            String[] split = str.split("\\|");
            String name = split[0];
            String place = split[1];
            Tuple2<String,String> tuple2 = Tuple2.of(name,place);
            collect(tuple2);
        }
    }

    TableFunction支援的資料類型及類型推導機制請參見Flink支援的資料類型和類型推導。

    說明

    以上兩個文檔連結為Flink 1.15版本對應的文檔,不同Flink大版本中TableFunction支援的資料類型及推導機制可能會存在差異,請您通過VVR和Flink版本的映射關係去參考對應版本的Flink文檔。Flink版本查看方法請參見空間管理與操作。

    常用的複合類型Tuple和Row樣本如下:

    • Tuple類型

      TableFunction<Tuple2<String,Integer>
    • Row類型

      @FunctionHint(output = @DataTypeHint("ROW<word STRING, length INT>"))
      public static class SplitFunction extends TableFunction<Row> {
      
        public void eval(String str) {
          for (String s : str.split(" ")) {
            // use collect(...) to emit a row
            collect(Row.of(s, s.length()));
          }
        }
      }
  4. 雙擊開啟\ASI_UDX-main\後,配置pom.xml。

    該樣本中,pom.xml檔案已配置了Flink 1.11版依賴的主要JAR包資訊。如果您的業務:

    • 不依賴其他JAR包:不用配置pom.xml檔案,繼續下一步。

    • 依賴其他JAR包:在pom.xml檔案中添加您所需依賴的JAR包資訊。

    Flink 1.11版依賴的主要JAR包如下。

    <dependencies>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-streaming-java_2.12</artifactId>
                <version>1.11.0</version>
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-table</artifactId>
                <version>1.11.0</version>
                <type>pom</type>
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-core</artifactId>
                <version>1.11.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-table-common</artifactId>
                <version>1.11.0</version>
            </dependency>
        </dependencies>
  5. 在下載檔案中pom.xml所在的目錄執行如下命令打包檔案。

    mvn package -Dcheckstyle.skip

    \ASI_UDX-main\target\目錄下會出現ASI_UDX-1.0-SNAPSHOT.jar的JAR包,即代表完成了UDTF開發工作。

自訂非同步資料表值函式(AsyncTableFunction)

非同步資料表值函式(AsyncTableFunction)用於在 Flink SQL 或 Table API 中非同步訪問外部服務,每次調用返回零行、一行或多行結果,屬於自訂表格值函數(UDTF)。典型情境如根據商品 ID 查詢多個標籤,或根據檢索條件返回多條匹配記錄。非同步資料表值函式既可以作為使用者自訂函數,通過 SQL 的 LATERAL TABLE 或 Table API 的 joinLateral 調用,形成非同步 Correlate 運算;也可以由維表連接器提供,作為非同步 Lookup Join 的查詢實現。

配置參數

通過以下參數控制非同步資料表值函式的運行行為。

配置項

適用情境

作用

table.exec.async-table.max-concurrent-operations

非同步 Correlate

控制非同步資料表值函式的並發請求數量。

table.exec.async-table.timeout

非同步 Correlate

設定調用的總等待時間,包含啟用的重試。

table.exec.async-table.retry-strategy

非同步 Correlate

設定函數調用的重試策略。

table.exec.async-lookup.buffer-capacity

非同步 Lookup Join

控制非同步尋找的緩衝容量。

table.exec.async-lookup.timeout

非同步 Lookup Join

設定非同步維表尋找的逾時時間,也可由適用的 LOOKUP Hint 為單個關聯指定。

開發樣本

實現 AsyncTableFunction 時,需要聲明公開、非靜態 eval 方法。第一個參數是用於提交結果的 CompletableFuture<Collection<T>>,其餘參數是 SQL 調用時傳入的業務參數。

函數發起非同步請求後及時返回,在請求完成的回調中調用 result.complete(rows) 提交結果,或調用 result.completeExceptionally(error) 提交異常。一次調用的多行結果通過一個集合提交,無結果時提交空集合。

package com.example.udf;

import org.apache.flink.table.functions.AsyncTableFunction;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.types.Row;
import org.apache.flink.table.functions.FunctionContext;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;

/**
 * 通過非同步 HTTP 介面查詢商品標籤。
 *
 * <p>介面約定:200 返回 UTF-8 文本,每行一個標籤;204、404 表示無標籤。
 * 本樣本適用於返回短文本的唯讀查詢;其他狀態代碼作為調用失敗交給引擎處理。
 */
@FunctionHint(output = @DataTypeHint("ROW<tag STRING>"))
public class AsyncProductTags extends AsyncTableFunction<Row> {

    private transient HttpClient client;
    private transient URI serviceBaseUri;

    @Override
    public void open(FunctionContext context) {
        String baseUrl = context.getJobParameter("product.service.base-url", null);
        if (baseUrl == null || baseUrl.isBlank()) {
            throw new IllegalArgumentException("請配置 product.service.base-url");
        }
        serviceBaseUri = URI.create(baseUrl);
        if (!("https".equalsIgnoreCase(serviceBaseUri.getScheme())
                        || "http".equalsIgnoreCase(serviceBaseUri.getScheme()))
                || serviceBaseUri.getHost() == null
                || serviceBaseUri.getRawQuery() != null
                || serviceBaseUri.getRawFragment() != null
                || !baseUrl.endsWith("/")) {
            throw new IllegalArgumentException("服務地址須為以 / 結尾的 HTTP(S) 基礎地址");
        }
        client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
    }

    @Override
    public boolean isDeterministic() {
        // 商品資訊可能變化,相同 ID 在不同時間查詢可能得到不同結果。
        return false;
    }

    public void eval(CompletableFuture<Collection<Row>> result, Long productId) {
        if (productId == null) {
            result.complete(Collections.emptyList());
            return;
        }

        try {
            HttpRequest request =
                    HttpRequest.newBuilder(
                                    serviceBaseUri.resolve("products/" + productId + "/tags"))
                            .timeout(Duration.ofSeconds(5))
                            .header("Accept", "text/plain")
                            .GET()
                            .build();

            client.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
                    .thenApply(
                            response -> {
                                if (response.statusCode() == 200) {
                                    return response.body().lines()
                                            .map(String::strip)
                                            .filter(tag -> !tag.isEmpty())
                                            .map(tag -> Row.of(tag))
                                            .collect(Collectors.toList());
                                }
                                if (response.statusCode() == 204 || response.statusCode() == 404) {
                                    return Collections.<Row>emptyList();
                                }
                                throw new CompletionException(
                                        new IOException(
                                                "商品查詢失敗,HTTP 狀態代碼:" + response.statusCode()));
                            })
                    .whenComplete(
                            (rows, error) -> {
                                if (error != null) {
                                    result.completeExceptionally(error);
                                } else {
                                    result.complete(rows);
                                }
                            });
        } catch (RuntimeException e) {
            result.completeExceptionally(e);
        }
    }
}

HttpClient.sendAsync 非同步執行網路請求,函數無需自行建立線程池。回調只完成短文本解析和結果提交。使用其他用戶端時,也應優先選擇其原生非同步介面。JDK 用戶端的行為請參見 HttpClient API。

商品標籤可能隨時間變化,因此樣本將 isDeterministic() 設定為 false。對於同樣的輸入始終返回相同結果的函數,可保留預設行為。

自訂逾時處理

預設情況下,如果某次 eval 調用在逾時時間之後仍未完成 CompletableFuture,且配置的重試次數也已用盡,架構會拋出 java.util.concurrent.TimeoutException 並讓作業失敗。支援在 AsyncTableFunction 子類中聲明一個與 eval 方法相匹配的 timeout(...) 方法,由該方法返回一行兜底資料,或實現自訂的逾時處理邏輯。

調用遠程 LLM 叢集等情境的逾時機率相對較高,通常不希望逾時直接導致作業失敗。聲明 timeout 方法後,可在逾時時返回兜底結果,避免作業中斷。

timeout 方法需要同時滿足以下條件:

  • 方法聲明:必須是 public 執行個體方法,不能是 static,方法名固定為 timeout。

  • 簽名與 eval 對齊:參數列表需要與對應的 eval 嚴格一致,第一個參數是 CompletableFuture,其泛型型別必須與 eval 完全相同,其餘參數的類型和順序也必須與 eval 完全相同。timeout 支援重載,每一個需要兜底邏輯的 eval 重載都可以單獨聲明一個對應的 timeout。

  • 同步完成(強制要求):該回調在運算元的 mailbox 線程上運行,必須在方法返回前完成 future。方法返回後,架構會立即檢查 future.isDone();如果尚未完成,架構會強制以 IllegalStateException 完成 future。因此:

    • 不要在方法體內再發起一次非同步呼叫(例如又一次重試或二次查詢),並指望其回調最終完成 future。回調觸發時,架構已放棄該條記錄。

    • 不要啟動新線程非同步完成 future,原因同上。

    • 方法體只做一件事:給出輕量的兜底結果,例如返回一個常量行、一個 NULL 行、空集合,或使用業務自訂異常調用 completeExceptionally(...)。

  • 拋異常是安全的:直接在方法體內同步拋出異常沒有問題,架構會把異常轉交給下遊的 ResultFuture,效果等價於顯式調用 future.completeExceptionally(thrown)。

在以上約束之外,架構還會在以下情境保證穩定的行為:

  • 未聲明 timeout 方法:沿用架構預設的 TimeoutException,不會引發代碼產生階段的失敗。

  • 簽名不相容:如果 timeout 方法可見度合法,但參數列表無法和當前調用點的實參類型對應,架構會在規劃期(代碼產生階段)以 ValidationException 直接失敗。錯誤資訊中包含函數的全限定類名,並同時給出期望簽名與實際簽名。

  • 空集合兜底:調用 future.complete(Collections.emptyList()) 時,對 INNER lookup join 而言該行會被丟棄;對 LEFT OUTER lookup join 而言,右表欄位會被填充為 NULL。

以下樣本在非同步資料表值函式的基礎上增加 timeout 回調,展示如何同步完成 future 並返回一行兜底資料:

import org.apache.flink.table.functions.AsyncTableFunction;

import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;

public static class BackgroundFunctionWithTimeout extends AsyncTableFunction<Long> {

    public void eval(CompletableFuture<Collection<Long>> future, Integer waitMax) {
        // ...邏輯與前述開發樣本一致:把非同步任務派發出去,
        // 然後在回調線程中完成 `future`。
    }

    // 參數列表必須與 eval() 對齊:CompletableFuture 的泛型相同,
    // 後續參數(Integer waitMax)的類型和順序也相同。
    // 方法體必須同步完成 `future`——不要再發起非同步呼叫,
    // 也不要啟動新線程非同步去完成它;這個回調被觸發時,
    // 運算元已經放棄了這條記錄。
    public void timeout(CompletableFuture<Collection<Long>> future, Integer waitMax) {
        future.complete(Collections.singletonList(-1L));
        // 也可以選擇用業務自訂異常代替預設的 TimeoutException:
        // future.completeExceptionally(new RuntimeException(waitMax));
    }
}

UDTF註冊

UDTF註冊過程,請參見管理自訂函數(UDF)。

UDTF使用

在註冊UDTF完成後,您就可以使用UDTF,詳細的操作步驟如下。

  1. Flink SQL作業開發。詳情請參見作業開發地圖。

    ASI_UDTF_Source表中message欄位每行字串按照豎線(|)分割成多列,程式碼範例如下。

    CREATE TEMPORARY TABLE ASI_UDTF_Source (
      `message`  VARCHAR
    ) WITH (
      'connector'='datagen'
    );
    
    CREATE TEMPORARY TABLE ASI_UDTF_Sink (
      name  VARCHAR,
      place  VARCHAR
    ) WITH (
      'connector' = 'blackhole'
    );
    
    INSERT INTO ASI_UDTF_Sink
    SELECT name,place
    FROM ASI_UDTF_Source,lateral table(ASI_UDTF(`message`)) as T(name,place);
  2. 在營運中心 > 作業營運頁面,單擊目標作業名稱操作列的啟動。

    啟動成功後,ASI_UDTF_Sink表會被插入ASI_UDTF_Source表中message欄位按照豎線(|)分割成多列的字元。