All Products
Search
Document Center

Realtime Compute for Apache Flink:Java UDTFs

Last Updated:Sep 22, 2026

User-defined table-valued function (UDTF) menerima nol atau lebih nilai skalar sebagai input dan menghasilkan beberapa baris sebagai output. Setiap baris yang dihasilkan dapat memiliki satu atau lebih kolom. Hal ini berbeda dari fungsi skalar, yang memetakan input ke satu nilai output tunggal.

Flink membedakan beberapa jenis fungsi:

Jenis fungsi Perilaku
Fungsi skalar Memetakan nilai skalar ke nilai skalar baru
Table-valued function (UDTF) Memetakan nilai skalar ke baris-baris baru
Fungsi agregat Memetakan nilai skalar dari beberapa baris ke nilai skalar baru
Fungsi agregat tabel Memetakan nilai skalar dari beberapa baris ke baris-baris baru

Gunakan UDTF ketika logika Anda harus menghasilkan beberapa baris output dari satu baris input—misalnya, memisahkan string yang dibatasi menjadi baris-baris terpisah.

Untuk latar belakang tentang user-defined functions Flink, lihat User-defined functions.

Prasyarat

Sebelum memulai, pastikan Anda telah:

  • Apache Maven terinstal di mesin lokal Anda

  • IntelliJ IDEA terinstal di mesin lokal Anda

  • Lingkungan pekerjaan Flink SQL telah disiapkan di Realtime Compute for Apache Flink

Kembangkan UDTF

Catatan

Realtime Compute for Apache Flink menyediakan proyek contoh (ASI_UDX_Demo) dengan lingkungan pengembangan yang telah dikonfigurasi sebelumnya serta implementasi contoh untuk user-defined scalar functions, user-defined aggregate functions (UDAFs), dan UDTF. Akses ke repositori GitHub pihak ketiga ini kadang-kadang bisa lambat atau tidak tersedia.

  1. Unduh dan ekstrak proyek contoh ASI_UDX_Demo ke mesin lokal Anda. Setelah diekstraksi, folder ASI_UDX-main dibuat dengan file-file berikut:

    • pom.xml: File konfigurasi proyek Maven, yang menjelaskan koordinat proyek, dependensi, dan pengaturan build.

    • \ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.java: Kode Java contoh untuk UDTF.

  2. Di IntelliJ IDEA, klik File > Open dan buka folder ASI_UDX-main yang telah diekstraksi.

  3. Buka \ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.java dan implementasikan logika UDTF Anda. UDTF memperluas TableFunction<T>, di mana T adalah tipe output. Metode eval() harus mengembalikan void—alih-alih pernyataan return, panggil collect() untuk mengeluarkan baris output. Ini berbeda dari fungsi skalar, yang menggunakan return. Contoh berikut membagi string menjadi dua kolom menggunakan tanda batang vertikal (|) sebagai pembatas:

    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];
            // emit one row with two columns
            collect(Tuple2.of(name, place));
        }
    }

    Mendeklarasikan tipe output Flink mendukung dua pendekatan untuk mendeklarasikan skema output UDTF:

    • Tipe Row dengan `@FunctionHint` dan `@DataTypeHint` (direkomendasikan): Mendeklarasikan secara eksplisit nama dan tipe bidang, sehingga skema output menjadi jelas bagi Flink dan pembaca.

      @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()));
              }
          }
      }
    • Tipe Tuple (tipe diinferensi dari generics): Lebih sederhana untuk ditulis, tetapi nama kolom tidak eksplisit.

      TableFunction<Tuple2<String, Integer>>
    Catatan

    Tipe data dan mekanisme inferensi tipe yang didukung oleh TableFunction bervariasi antar versi utama Flink. Contoh di atas mengikuti dokumentasi Flink 1.15 (Data types dan Type inference). Periksa dokumentasi Flink yang sesuai dengan versi Ververica Runtime (VVR) Anda. Untuk mengetahui versi Flink Anda, lihat Storage management and operations.

  4. Buka pom.xml di folder ASI_UDX-main dan konfigurasikan dependensi Anda. Proyek contoh mencakup dependensi Maven berikut untuk Flink 1.11:

    <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>

    Jika UDTF Anda tidak memiliki dependensi tambahan selain yang tercantum, lanjutkan ke langkah berikutnya. Jika tidak, tambahkan entri dependensi yang diperlukan ke pom.xml.

  5. Di direktori yang berisi pom.xml, jalankan perintah berikut untuk mengemas proyek:

    mvn package -Dcheckstyle.skip

    Jika build berhasil, ASI_UDX-1.0-SNAPSHOT.jar dihasilkan di direktori \ASI_UDX-main\target\. Pengembangan UDTF selesai.

Asynchronous table functions (AsyncTableFunction)

Asynchronous table function (AsyncTableFunction) mengakses layanan eksternal secara asinkron dalam Flink SQL atau Table API dan mengembalikan nol, satu, atau beberapa baris untuk setiap pemanggilan. Ini merupakan jenis user-defined table-valued function (UDTF). Skenario khas meliputi mengkueri beberapa tag berdasarkan ID produk atau mengembalikan beberapa catatan yang sesuai untuk suatu kondisi pencarian. Asynchronous table function dapat berupa user-defined function yang dipanggil oleh LATERAL TABLE dalam SQL atau joinLateral dalam Table API untuk membentuk operasi correlate asinkron. Fungsi ini juga dapat disediakan oleh konektor tabel dimensi sebagai implementasi kueri untuk asynchronous lookup join.

Parameter konfigurasi

Parameter berikut mengontrol perilaku runtime asynchronous table functions.

Parameter Skenario Tujuan
table.exec.async-table.max-concurrent-operations Async Correlate Mengontrol jumlah permintaan konkuren dari fungsi tabel asinkron.
table.exec.async-table.timeout Async Correlate Menetapkan total waktu tunggu pemanggilan, termasuk retry yang diaktifkan.
table.exec.async-table.retry-strategy Async Correlate Menetapkan strategi retry untuk pemanggilan fungsi.
table.exec.async-lookup.buffer-capacity Async Lookup Join Mengontrol kapasitas buffer untuk lookup asinkron.
table.exec.async-lookup.timeout Async Lookup Join Menetapkan timeout untuk lookup tabel dimensi asinkron. Petunjuk LOOKUP yang berlaku juga dapat menentukan timeout untuk satu join.

Contoh pengembangan

Saat mengimplementasikan AsyncTableFunction, deklarasikan metode eval publik yang non-static. Parameter pertama adalah CompletableFuture<Collection<T>> yang digunakan untuk mengirimkan hasil, dan parameter sisanya adalah parameter bisnis yang diteruskan oleh pemanggilan SQL.

Fungsi segera mengembalikan kontrol setelah mengirim permintaan asinkron, dan mengirimkan hasil dalam callback penyelesaian dengan memanggil result.complete(rows), atau mengirimkan exception dengan memanggil result.completeExceptionally(error). Beberapa baris dari satu pemanggilan dikirimkan sebagai satu koleksi. Kirim koleksi kosong jika tidak ada hasil.

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;

/**
 * Queries product tags through an asynchronous HTTP interface.
 *
 * <p>Interface contract: 200 returns UTF-8 text with one tag per line; 204 and 404 indicate no tags.
 * This example suits read-only queries that return short text. Other status codes are treated as invocation failures and handed over to the engine.
 */
@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("Configure 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("The service URL must be an HTTP(S) base URL that ends with a forward slash (/).");
        }
        client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
    }

    @Override
    public boolean isDeterministic() {
        // Product information may change. The same ID may return different results at different times.
        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(
                                                "Failed to query the product. HTTP status code: " + response.statusCode()));
                            })
                    .whenComplete(
                            (rows, error) -> {
                                if (error != null) {
                                    result.completeExceptionally(error);
                                } else {
                                    result.complete(rows);
                                }
                            });
        } catch (RuntimeException e) {
            result.completeExceptionally(e);
        }
    }
}

HttpClient.sendAsync melakukan permintaan jaringan secara asinkron, sehingga fungsi tidak perlu membuat kolam thread sendiri. Callback hanya mengurai teks pendek dan mengirimkan hasil. Saat menggunakan klien lain, lebih baik gunakan juga antarmuka asinkron aslinya. Untuk perilaku klien JDK, lihat HttpClient API.

Tag produk dapat berubah seiring waktu, sehingga contoh ini menetapkan isDeterministic() ke false. Untuk fungsi yang selalu mengembalikan hasil yang sama untuk input yang sama, pertahankan perilaku default.

Penanganan timeout kustom

Secara default, jika pemanggilan eval tidak menyelesaikan CompletableFuture sebelum timeout berakhir dan retry yang dikonfigurasi telah habis, framework akan melemparkan java.util.concurrent.TimeoutException dan gagal menjalankan pekerjaan. Anda dapat mendeklarasikan metode timeout(...) yang sesuai dengan metode eval dalam subclass AsyncTableFunction untuk mengembalikan baris fallback atau mengimplementasikan penanganan timeout kustom.

Timeout cenderung lebih sering terjadi saat memanggil kluster LLM remote, dan timeout biasanya tidak boleh langsung menyebabkan pekerjaan gagal. Dengan metode timeout yang dideklarasikan, hasil fallback dikembalikan saat timeout dan pekerjaan dilanjutkan.

Metode timeout harus memenuhi semua kondisi berikut:

  • Deklarasi metode: Metode harus berupa metode instans public, tidak boleh static, dan harus bernama timeout.

  • Signature selaras dengan eval: Daftar parameter harus benar-benar identik dengan metode eval yang sesuai. Parameter pertama adalah CompletableFuture yang tipe generiknya harus persis sama dengan eval, dan tipe serta urutan parameter sisanya juga harus persis sama. timeout mendukung overloading: Anda dapat mendeklarasikan metode timeout terpisah untuk setiap overload eval yang memerlukan logika fallback.

  • Penyelesaian sinkron (wajib): Callback ini berjalan pada thread mailbox operator dan harus menyelesaikan future sebelum metode mengembalikan kontrol. Segera setelah metode mengembalikan kontrol, framework memeriksa future.isDone(). Jika future belum selesai, framework akan memaksanya diselesaikan dengan IllegalStateException. Oleh karena itu:

    • Jangan memulai pemanggilan asinkron lain dalam isi metode, seperti retry lain atau kueri kedua, dan mengharapkan callback-nya menyelesaikan future. Pada saat callback dipicu, framework sudah menyerah pada catatan tersebut.

    • Jangan memulai thread baru untuk menyelesaikan future secara asinkron, karena alasan yang sama.

    • Isi metode hanya melakukan satu hal: memberikan hasil fallback ringan, seperti baris konstan, baris NULL, atau koleksi kosong, atau memanggil completeExceptionally(...) dengan exception bisnis kustom.

  • Melemparkan exception aman: Melemparkan exception secara sinkron dalam isi metode diperbolehkan. Framework meneruskan exception tersebut ke ResultFuture downstream, yang setara dengan memanggil secara eksplisit future.completeExceptionally(thrown).

Di luar batasan ini, framework juga menjamin perilaku stabil dalam skenario berikut:

  • Tidak ada metode timeout yang dideklarasikan: TimeoutException default dari framework digunakan, dan tidak terjadi kegagalan selama pembuatan kode.

  • Signature tidak kompatibel: Jika metode timeout memiliki visibilitas yang valid tetapi daftar parameternya tidak dapat sesuai dengan tipe argumen aktual di lokasi pemanggilan, framework akan gagal selama perencanaan (pembuatan kode) dengan ValidationException. Pesan error berisi nama kelas lengkap fungsi dan menunjukkan signature yang diharapkan dan yang aktual.

  • Koleksi kosong sebagai fallback: Saat future.complete(Collections.emptyList()) dipanggil, baris tersebut dibuang untuk INNER lookup join, dan bidang tabel kanan diisi dengan NULL untuk LEFT OUTER lookup join.

Contoh berikut menambahkan callback timeout ke asynchronous table function dan menunjukkan cara menyelesaikan future secara sinkron serta mengembalikan baris fallback:

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) {
        // ...same logic as the preceding development example: dispatch the asynchronous task,
        // and then complete `future` in the callback thread.
    }

    // The parameter list must align with eval(): the same CompletableFuture generic type,
    // and the same types and order for the remaining parameters (Integer waitMax).
    // The method body must complete `future` synchronously. Do not start another asynchronous call
    // or a new thread to complete it asynchronously. When this callback is triggered,
    // the operator has already given up on this record.
    public void timeout(CompletableFuture<Collection<Long>> future, Integer waitMax) {
        future.complete(Collections.singletonList(-1L));
        // You can also use a custom business exception instead of the default TimeoutException:
        // future.completeExceptionally(new RuntimeException(waitMax));
    }
}

Daftarkan UDTF

Untuk petunjuk cara mendaftarkan UDTF, lihat Manage user-defined functions (UDFs).

Gunakan UDTF

Setelah mendaftarkan UDTF, gunakan dalam pekerjaan Flink SQL.

  1. Kembangkan pekerjaan Flink SQL. Untuk informasi lebih lanjut, lihat Job development map. SQL berikut membagi bidang message di ASI_UDTF_Source menjadi dua kolom menggunakan pembatas |, lalu memasukkan hasilnya ke ASI_UDTF_Sink. Sintaks LATERAL TABLE melakukan cross-join setiap baris input dengan baris-baris yang dihasilkan oleh UDTF.

    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. Di halaman Operation Center > Job O&M, temukan pekerjaan target dan klik Start di kolom Actions. Setelah pekerjaan dimulai, string di bidang message dari ASI_UDTF_Source dibagi oleh pembatas |, dan kolom hasilnya dimasukkan ke ASI_UDTF_Sink.

Langkah berikutnya