Tüm Ürünler
Search
Belge Merkezi

Realtime Compute for Apache Flink:Java UDTF'leri

Son Güncelleme:Oct 04, 2026

Kullanıcı tanımlı tablo değerli bir işlev (UDTF), girdi olarak sıfır veya daha fazla skaler değer alır ve çıktı olarak birden çok satır döndürür. Döndürülen her satır bir veya daha fazla sütun içerebilir. Bu durum, girdiyi tek bir çıktı değerine eşleyen skaler işlevlerden farklıdır.

Flink, birkaç farklı işlev türü arasında ayrım yapar:

İşlev türü Davranış
Skaler işlev Skaler değerleri yeni bir skaler değere eşler
Tablo değerli işlev (UDTF) Skaler değerleri yeni satırlara eşler
Toplama işlevi Birden çok satırdan gelen skaler değerleri yeni bir skaler değere eşler
Tablo toplama işlevi Birden çok satırdan gelen skaler değerleri yeni satırlara eşler

Mantığınızın tek bir girdi satırından birden çok çıktı satırı üretmesi gerektiğinde bir UDTF kullanın; örneğin, sınırlayıcıyla ayrılmış bir dizeyi ayrı satırlara bölmek gibi.

Flink kullanıcı tanımlı işlevleri hakkında arka plan bilgisi için bkz. Kullanıcı tanımlı işlevler.

Ön koşullar

Başlamadan önce aşağıdakilere sahip olduğunuzdan emin olun:

  • Yerel makinenizde kurulu olan Apache Maven

  • Yerel makinenizde kurulu olan IntelliJ IDEA

  • Realtime Compute for Apache Flink'te yapılandırılmış bir Flink SQL iş ortamı

Bir UDTF Geliştirme

Not

Realtime Compute for Apache Flink, önceden yapılandırılmış geliştirme ortamları ve kullanıcı tanımlı skaler işlevler, kullanıcı tanımlı toplama işlevleri (UDAF'ler) ve UDTF'ler için örnek uygulamalar içeren bir örnek proje (ASI_UDX_Demo) sağlar. Bu üçüncü taraf GitHub deposuna erişim zaman zaman yavaş olabilir veya kullanılamayabilir.

  1. ASI_UDX_Demo örnek projesini yerel makinenize indirin ve sıkıştırılmış dosyayı açın. Sıkıştırma işleminden sonra aşağıdaki dosyaları içeren bir ASI_UDX-main klasörü oluşturulur:

    • pom.xml: Projenin koordinatlarını, bağımlılıklarını ve derleme ayarlarını açıklayan Maven proje yapılandırma dosyası.

    • \ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.java: UDTF için örnek Java kodu.

  2. IntelliJ IDEA'da File > Open seçeneklerine tıklayın ve sıkıştırması açılan ASI_UDX-main klasörünü açın.

  3. \ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.java dosyasını açın ve UDTF mantığınızı uygulayın. Bir UDTF, çıktı türünün T olduğu TableFunction<T> sınıfından türetilir. eval() yöntemi void döndürmelidir; bir return deyimi kullanmak yerine çıktı satırlarını yaymak için collect() yöntemini çağırın. Bu durum, return kullanan skaler işlevlerden farklıdır. Aşağıdaki örnek, bir dizeyi dikey çubuk (|) ayırıcısını kullanarak iki sütuna böler:

    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));
        }
    }

    Çıktı türlerini bildirme Flink, bir UDTF'nin çıktı şemasını bildirmek için iki yaklaşımı destekler:

    • @FunctionHint ve @DataTypeHint ile Satır türü (önerilen): Alan adlarını ve türlerini açıkça bildirerek çıktı şemasının hem Flink hem de okuyucular için net olmasını sağlar.

      @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()));
              }
          }
      }
    • Tuple türü (tür genel türlerden çıkarılır): Yazılması daha basittir ancak sütun adları açık değildir.

      TableFunction<Tuple2<String, Integer>>
    Not

    TableFunction tarafından desteklenen veri türleri ve tür çıkarsama mekanizması, ana Flink sürümleri arasında değişiklik gösterir. Yukarıdaki örnekler Flink 1.15 belgelerini takip eder (Veri türleri ve Tür çıkarsama). Ververica Runtime (VVR) sürümünüzle eşleşen Flink belgelerini kontrol edin. Flink sürümünüzü bulmak için bkz. Depolama yönetimi ve işlemleri.

  4. ASI_UDX-main klasöründeki pom.xml dosyasını açın ve bağımlılıklarınızı yapılandırın. Örnek proje, Flink 1.11 için aşağıdaki Maven bağımlılıklarını içerir:

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

    UDTF'nizin listelenenlerin dışında ek bağımlılığı yoksa bir sonraki adıma geçin. Aksi takdirde, gerekli bağımlılık girişlerini pom.xml dosyasına ekleyin.

  5. pom.xml dosyasını içeren dizinde projeyi paketlemek için aşağıdaki komutu çalıştırın:

    mvn package -Dcheckstyle.skip

    Derleme başarılı olursa \ASI_UDX-main\target\ dizininde ASI_UDX-1.0-SNAPSHOT.jar dosyası oluşturulur. UDTF geliştirme işlemi tamamlanmıştır.

Eşzamansız tablo işlevleri (AsyncTableFunction)

Eşzamansız bir tablo işlevi (AsyncTableFunction), Flink SQL veya Tablo API'sinde harici hizmetlere eşzamansız olarak erişir ve her çağrı için sıfır, bir veya birden çok satır döndürür. Bu, bir tür kullanıcı tanımlı tablo değerli işlevdir (UDTF). Tipik senaryolar arasında ürün kimliğine göre birden çok etiketi sorgulama veya bir arama koşulu için birden çok eşleşen kaydı döndürme yer alır. Eşzamansız bir tablo işlevi, SQL'de LATERAL TABLE veya Tablo API'sinde joinLateral tarafından çağrılan ve eşzamansız bir ilişkilendirme işlemi oluşturan bir kullanıcı tanımlı işlev olabilir. Ayrıca, eşzamansız arama birleştirmesi için sorgu uygulaması olarak bir boyut tablosu bağlayıcısı tarafından sağlanabilir.

Yapılandırma parametreleri

Aşağıdaki parametreler eşzamansız tablo işlevlerinin çalışma zamanı davranışını kontrol eder.

Parametre Senaryo Amaç
table.exec.async-table.max-concurrent-operations Eşzamansız İlişkilendirme Eşzamansız tablo işlevinin eşzamanlı istek sayısını kontrol eder.
table.exec.async-table.timeout Eşzamansız İlişkilendirme Etkin yeniden denemeler dahil olmak üzere bir çağrının toplam bekleme süresini ayarlar.
table.exec.async-table.retry-strategy Eşzamansız İlişkilendirme İşlev çağrılarının yeniden deneme stratejisini ayarlar.
table.exec.async-lookup.buffer-capacity Eşzamansız Arama Birleştirmesi Eşzamansız aramaların arabellek kapasitesini kontrol eder.
table.exec.async-lookup.timeout Eşzamansız Arama Birleştirmesi Eşzamansız boyut tablosu aramalarının zaman aşımını ayarlar. Uygulanabilir bir LOOKUP ipucu, tek bir birleştirme için zaman aşımını da belirtebilir.

Geliştirme örneği

Bir AsyncTableFunction uygularken, statik olmayan herkese açık bir eval yöntemi bildirin. İlk parametre, sonuçları göndermek için kullanılan bir CompletableFuture<Collection<T>> nesnesidir ve kalan parametreler SQL çağrısı tarafından geçirilen iş parametreleridir.

İşlev, eşzamansız isteği gönderdikten sonra hızlıca döner ve result.complete(rows) çağrısı yaparak tamamlama geri çağrısında sonuçları gönderir veya result.completeExceptionally(error) çağrısı yaparak bir özel durum gönderir. Tek bir çağrının birden çok satırı tek bir koleksiyon olarak gönderilir. Sonuç olmadığında boş bir koleksiyon gönderin.

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 ağ isteklerini eşzamansız olarak yürüttüğünden işlevin kendi iş parçacığı havuzunu oluşturmasına gerek yoktur. Geri çağrı yalnızca kısa metni ayrıştırır ve sonuçları gönderir. Başka bir istemci kullanırken, onun yerel eşzamansız arayüzünü tercih edin. JDK istemcisinin davranışı için bkz. HttpClient API.

Ürün etiketleri zaman içinde değişebileceğinden örnek isDeterministic() değerini false olarak ayarlar. Aynı girdi için her zaman aynı sonucu döndüren bir işlev için varsayılan davranışı koruyun.

Özel zaman aşımı işleme

Varsayılan olarak, bir eval çağrısı zaman aşımı süresi dolmadan ve yapılandırılan yeniden denemeler tükenmeden CompletableFuture nesnesini tamamlamazsa çerçeve bir java.util.concurrent.TimeoutException özel durumu oluşturur ve iş başarısız olur. Bir AsyncTableFunction alt sınıfında eval yöntemiyle eşleşen bir timeout(...) yöntemi bildirerek yedek bir satır döndürebilir veya özel zaman aşımı işleme uygulayabilirsiniz.

Uzak bir LLM kümesine çağrı yapılırken zaman aşımları daha olasıdır ve bir zaman aşımı genellikle işi doğrudan başarısız etmemelidir. Bildirilen bir timeout yöntemi ile zaman aşımı durumunda bir yedek sonuç döndürülür ve iş devam eder.

timeout yöntemi aşağıdaki tüm koşulları karşılamalıdır:

  • Yöntem bildirimi: Yöntem, public bir örnek yöntemi olmalı, static olmamalı ve timeout olarak adlandırılmalıdır.

  • **eval ile uyumlu imza**: Parametre listesi, ilgili eval yöntemininkiyle kesinlikle aynı olmalıdır. İlk parametre, genel türü eval ile tam olarak aynı olan bir CompletableFuture nesnesidir ve kalan parametrelerin türleri ve sırası da kesinlikle aynı olmalıdır. timeout aşırı yüklemeyi destekler: yedek mantığı gerektiren her eval aşırı yüklemesi için ayrı bir timeout yöntemi bildirebilirsiniz.

  • Eşzamanlı tamamlama (zorunlu): Bu geri çağrı, operatörün posta kutusu iş parçacığında çalışır ve yöntem dönüşünden önce geleceği tamamlamalıdır. Yöntem döndükten hemen sonra çerçeve future.isDone() değerini kontrol eder. Gelecek tamamlanmamışsa çerçeve onu bir IllegalStateException ile zorla tamamlar. Bu nedenle:

    • Yöntem gövdesinde başka bir eşzamansız çağrı (örneğin başka bir yeniden deneme veya ikinci bir sorgu) başlatmayın ve geri çağrısının geleceği tamamlamasını beklemeyin. Geri çağrı tetiklendiğinde çerçeve kayıt için zaten vazgeçmiş olur.

    • Geleceği eşzamansız olarak tamamlamak için yeni bir iş parçacığı başlatmayın, neden aynıdır.

    • Yöntem gövdesi yalnızca tek bir şey yapar: sabit bir satır, NULL satırı veya boş bir koleksiyon gibi hafif bir yedek sonuç sağlar veya özel bir iş özel durumuyla completeExceptionally(...) çağrısı yapar.

  • Özel durum oluşturma güvenlidir: Yöntem gövdesinde eşzamanlı olarak bir özel durum oluşturmak kabul edilebilir. Çerçeve özel durumu aşağı akış ResultFuture nesnesine iletir; bu, açıkça future.completeExceptionally(thrown) çağrısı yapmaya eşdeğerdir.

Bu kısıtlamaların ötesinde çerçeve aşağıdaki senaryolarda da kararlı davranış garantisi verir:

  • **timeout yöntemi bildirilmemiş**: Çerçevenin varsayılan TimeoutException özel durumu kullanılır ve kod oluşturma sırasında hata oluşmaz.

  • Uyumsuz imza: timeout yöntemi geçerli görünürlüğe sahip ancak parametre listesi çağrı sitesindeki gerçek bağımsız değişken türleriyle eşleşmiyorsa çerçeve, planlama (kod oluşturma) sırasında bir ValidationException ile başarısız olur. Hata iletisi işlevin tam nitelikli sınıf adını içerir ve hem beklenen hem de gerçek imzaları gösterir.

  • Yedek olarak boş koleksiyon: future.complete(Collections.emptyList()) çağrıldığında, satır INNER arama birleştirmesi için atılır ve LEFT OUTER arama birleştirmesi için sağ tablo alanları NULL ile doldurulur.

Aşağıdaki örnek, eşzamansız bir tablo işlevine bir timeout geri çağrısı ekler ve geleceğin nasıl eşzamanlı olarak tamamlanacağını ve bir yedek satırın nasıl döndürüleceğini gösterir:

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));
    }
}

Bir UDTF Kaydetme

Bir UDTF'nin nasıl kaydedileceğine ilişkin talimatlar için bkz. Kullanıcı tanımlı işlevleri (UDF'ler) yönetme.

Bir UDTF Kullanma

UDTF'yi kaydettikten sonra bunu bir Flink SQL işinde kullanın.

  1. Bir Flink SQL işi geliştirin. Daha fazla bilgi için bkz. İş geliştirme haritası. Aşağıdaki SQL, ASI_UDTF_Source içindeki message alanını | ayırıcısını kullanarak iki sütuna böler ve ardından sonuçları ASI_UDTF_Sink içine ekler. LATERAL TABLE sözdizimi, her girdi satırını UDTF tarafından yayılan satırlarla çapraz birleştirir.

    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. Operation Center > Job O&M sayfasında hedef işi bulun ve Actions sütunundaki Start seçeneğine tıklayın. İş başladıktan sonra ASI_UDTF_Source içindeki message alanındaki dizeler | ayırıcısına göre bölünür ve elde edilen sütunlar ASI_UDTF_Sink içine eklenir.

Sonraki adımlar