Apache Wayang 快速入门与集成示例

以可运行工程入门 Apache Wayang 1.1.1:pom 全文与 WordCount 起步,实测文件读写、过滤聚合、底层算子 API,并对比 Java 与 Spark local 双后端运行结果,附依赖冲突排除与堆验证。

Apache WayangApache Wayang2026/10/12

Apache Wayang 快速入门与集成示例

一、Wayang 与核心概念

Apache Wayang 是跨平台数据处理引擎:只写一遍处理逻辑,通过注册不同后端插件,把同一计划跑在 Java Stream、Spark、Flink 或数据库上,也可以交给代价优化器自动选择执行位置。本文在 OpenJDK 17.0.20.1 + Maven 3.6.3 环境对 1.1.1 版本完成全部示例实测。

四个核心概念:

概念类作用
上下文org.apache.wayang.core.api.WayangContext持有 Configuration 并注册后端插件
插件org.apache.wayang.java.Java / org.apache.wayang.spark.Spark声明可用的执行后端,如 Java.basicPlugin()
构建器org.apache.wayang.api.JavaPlanBuilderfluent 风格装配读、转换、写算子
元组org.apache.wayang.basic.data.Tuple2Key-Value 型中间数据

前置条件:按《Ubuntu 22.04 安装 Apache Wayang 指南》或《CentOS 8 安装 Apache Wayang 指南》完成 JDK 17、Maven 安装,并准备输入文件:

mkdir -p /root/wayang-demo/src/main/java/com/example /root/wayang-data
cat > /root/wayang-data/input.txt << 'EOF'
Apache Wayang is a cross platform data processor
wayang can run on java streams and spark
Apache Wayang mixes java and spark backends
big data with wayang big data
EOF

二、完整工程搭建

2.1 pom.xml 全文

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>wayang-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <wayang.version>1.1.1</wayang.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.wayang</groupId>
            <artifactId>wayang-core</artifactId>
            <version>${wayang.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.wayang</groupId>
            <artifactId>wayang-basic</artifactId>
            <version>${wayang.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.wayang</groupId>
            <artifactId>wayang-java</artifactId>
            <version>${wayang.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.wayang</groupId>
            <artifactId>wayang-api-scala-java</artifactId>
            <version>${wayang.version}</version>
        </dependency>
    </dependencies>
</project>

wayang-api-scala-java 是 JavaPlanBuilder 所在模块,1.x 必须显式引入,缺失会报 package org.apache.wayang.api does not exist(实测踩坑)。

2.2 工程目录结构

/root/wayang-demo
├── pom.xml
└── src/main/java/com/example
    ├── WordCount.java
    ├── WordCountFile.java
    ├── FilterAggregate.java
    └── LowLevelWordCount.java

2.3 WordCount 主示例

package com.example;

import org.apache.wayang.api.JavaPlanBuilder;
import org.apache.wayang.basic.data.Tuple2;
import org.apache.wayang.core.api.Configuration;
import org.apache.wayang.core.api.WayangContext;
import org.apache.wayang.java.Java;

import java.util.Arrays;
import java.util.Collection;

public class WordCount {
    public static void main(String[] args) {
        WayangContext wayang = new WayangContext(new Configuration())
                .withPlugin(Java.basicPlugin());

        Collection<Tuple2<String, Integer>> result = new JavaPlanBuilder(wayang)
                .withJobName("WordCount")
                .withUdfJarOf(WordCount.class)
                .readTextFile("file:///root/wayang-data/input.txt")
                .flatMap(line -> Arrays.asList(line.toLowerCase().split("\\W+")))
                .filter(word -> !word.isEmpty())
                .map(word -> new Tuple2<>(word, 1))
                .reduceByKey(Tuple2::getField0,
                        (t1, t2) -> new Tuple2<>(t1.getField0(), t1.getField1() + t2.getField1()))
                .collect();

        result.forEach(t -> System.out.println(t.getField0() + ": " + t.getField1()));
    }
}

编译并运行(堆限定 256-512 MB):

cd /root/wayang-demo
export MAVEN_OPTS="-Xms256m -Xmx512m"
mvn package dependency:copy-dependencies -DoutputDirectory=target/lib
java -Xms256m -Xmx512m -cp "target/wayang-demo-1.0.0.jar:target/lib/*" com.example.WordCount

实测回显:

big: 2
a: 1
data: 3
streams: 1
cross: 1
run: 1
is: 1
wayang: 4
backends: 1
processor: 1
platform: 1
with: 1
can: 1
java: 2
apache: 2
spark: 2
and: 2
mixes: 1
on: 1

三、文件读写示例

把 collect 换成 writeTextFile 写回文件(注意 1.1.1 为三参数,第三位是字符编码,官方旧示例的两参数写法编译不过):

package com.example;

import org.apache.wayang.api.JavaPlanBuilder;
import org.apache.wayang.basic.data.Tuple2;
import org.apache.wayang.core.api.Configuration;
import org.apache.wayang.core.api.WayangContext;
import org.apache.wayang.java.Java;

import java.util.Arrays;

public class WordCountFile {
    public static void main(String[] args) {
        WayangContext wayang = new WayangContext(new Configuration())
                .withPlugin(Java.basicPlugin());

        new JavaPlanBuilder(wayang)
                .withJobName("WordCountFile")
                .withUdfJarOf(WordCountFile.class)
                .readTextFile("file:///root/wayang-data/input.txt")
                .flatMap(line -> Arrays.asList(line.toLowerCase().split("\\W+")))
                .filter(word -> !word.isEmpty())
                .map(word -> new Tuple2<>(word, 1))
                .reduceByKey(Tuple2::getField0,
                        (t1, t2) -> new Tuple2<>(t1.getField0(), t1.getField1() + t2.getField1()))
                .writeTextFile("file:///root/wayang-data/output.txt",
                        t -> t.getField0() + ": " + t.getField1(), "UTF-8");
    }
}

运行并查看结果文件:

mvn -q package
java -Xms256m -Xmx512m -cp "target/wayang-demo-1.0.0.jar:target/lib/*" com.example.WordCountFile
sort /root/wayang-data/output.txt | head -5

实测回显:

a: 1
and: 2
apache: 2
backends: 1
big: 2

四、过滤聚合示例

集合数据源 + filter/map/reduce 的典型组合:

package com.example;

import org.apache.wayang.api.JavaPlanBuilder;
import org.apache.wayang.core.api.Configuration;
import org.apache.wayang.core.api.WayangContext;
import org.apache.wayang.java.Java;

import java.util.Arrays;
import java.util.Collection;

public class FilterAggregate {
    public static void main(String[] args) {
        WayangContext wayang = new WayangContext(new Configuration())
                .withPlugin(Java.basicPlugin());

        Collection<Integer> result = new JavaPlanBuilder(wayang)
                .withJobName("FilterAggregate")
                .loadCollection(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
                .filter(n -> n % 2 == 0)
                .map(n -> n * 10)
                .reduce((a, b) -> a + b)
                .collect();

        System.out.println("sum = " + result);
    }
}

实测运行:

java -Xms256m -Xmx512m -cp "target/wayang-demo-1.0.0.jar:target/lib/*" com.example.FilterAggregate

回显:

sum = [300]

五、底层算子 API 示例

不经过 JavaPlanBuilder,直接用 wayang-basic 算子拼装计划并执行(适合需要精细控制算子的场景):

package com.example;

import org.apache.wayang.basic.data.Tuple2;
import org.apache.wayang.basic.operators.FilterOperator;
import org.apache.wayang.basic.operators.FlatMapOperator;
import org.apache.wayang.basic.operators.LocalCallbackSink;
import org.apache.wayang.basic.operators.MapOperator;
import org.apache.wayang.basic.operators.ReduceByOperator;
import org.apache.wayang.basic.operators.TextFileSource;
import org.apache.wayang.core.api.WayangContext;
import org.apache.wayang.core.plan.wayangplan.WayangPlan;
import org.apache.wayang.java.Java;

import java.util.Arrays;

public class LowLevelWordCount {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Class<Tuple2<String, Integer>> tupleClass = (Class<Tuple2<String, Integer>>) (Class<?>) Tuple2.class;

        WayangContext wayang = new WayangContext().withPlugin(Java.basicPlugin());

        TextFileSource source = new TextFileSource("file:///root/wayang-data/input.txt");
        FlatMapOperator<String, String> tokens = new FlatMapOperator<>(
                line -> Arrays.asList(line.toLowerCase().split("\\W+")),
                String.class, String.class);
        FilterOperator<String> nonEmpty = new FilterOperator<>(
                w -> !w.isEmpty(), String.class);
        MapOperator<String, Tuple2<String, Integer>> pairs = new MapOperator<>(
                word -> new Tuple2<>(word, 1), String.class, tupleClass);
        ReduceByOperator<Tuple2<String, Integer>, String> counts = new ReduceByOperator<>(
                Tuple2::getField0,
                (t1, t2) -> new Tuple2<>(t1.getField0(), t1.getField1() + t2.getField1()),
                String.class, tupleClass);
        LocalCallbackSink<Tuple2<String, Integer>> sink = LocalCallbackSink.createStdoutSink(tupleClass);

        source.connectTo(0, tokens, 0);
        tokens.connectTo(0, nonEmpty, 0);
        nonEmpty.connectTo(0, pairs, 0);
        pairs.connectTo(0, counts, 0);
        counts.connectTo(0, sink, 0);

        wayang.execute(new WayangPlan(sink));
    }
}

底层 API 的两个签名要点(实测编译确认):connectTo(输出端口, 目标算子, 输入端口),端口在前;FlatMap/Filter 构造器 lambda 在前、Class 在后。运行:

java -Xms256m -Xmx512m -cp "target/wayang-demo-1.0.0.jar:target/lib/*" com.example.LowLevelWordCount

实测回显(节选,Tuple2 的 toString 格式):

(big, 2)
(a, 1)
(data, 3)
(streams, 1)
(cross, 1)

六、双后端对比:Java 与 Spark local

6.1 集成 wayang-spark

在另一个工程(或同一工程追加依赖)引入 wayang-spark,并排除其传递的 hadoop-client:

<dependency>
    <groupId>org.apache.wayang</groupId>
    <artifactId>wayang-spark</artifactId>
    <version>1.1.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
        </exclusion>
    </exclusions>
</dependency>

为什么必须排除(实测定位过程):wayang-spark 1.1.1 传递引入未 shaded 的 hadoop 3.3.6 全家桶,而 Spark 3.4.4 自带 shaded 的 hadoop-client-api/runtime 3.3.4,两套同名类混在 classpath 上时运行报错:

java.lang.NoSuchMethodError: 'org.apache.hadoop.fs.FSBuilder org.apache.hadoop.fs.FutureDataInputStreamBuilder.optLong(java.lang.String, long)'

排除后 classpath 只剩 Spark 自带的一套(hadoop-client-api-3.3.4.jar、hadoop-client-runtime-3.3.4.jar),冲突消失。

6.2 同一计划切换到 Spark

与 2.3 节相比只有三处差异——导入、插件注册、spark.master 注入,处理逻辑零改动:

package com.example;

import org.apache.wayang.api.JavaPlanBuilder;
import org.apache.wayang.basic.data.Tuple2;
import org.apache.wayang.core.api.Configuration;
import org.apache.wayang.core.api.WayangContext;
import org.apache.wayang.spark.Spark;

import java.util.Arrays;
import java.util.Collection;

public class WordCountSpark {
    public static void main(String[] args) {
        Configuration configuration = new Configuration();
        configuration.setProperty("spark.master", "local[2]");
        WayangContext wayang = new WayangContext(configuration)
                .withPlugin(Spark.basicPlugin());

        Collection<Tuple2<String, Integer>> result = new JavaPlanBuilder(wayang)
                .withJobName("WordCountSpark")
                .withUdfJarOf(WordCountSpark.class)
                .readTextFile("file:///root/wayang-data/input.txt")
                .flatMap(line -> Arrays.asList(line.toLowerCase().split("\\W+")))
                .filter(word -> !word.isEmpty())
                .map(word -> new Tuple2<>(word, 1))
                .reduceByKey(Tuple2::getField0,
                        (t1, t2) -> new Tuple2<>(t1.getField0(), t1.getField1() + t2.getField1()))
                .collect();

        result.forEach(t -> System.out.println("SPARK>> " + t.getField0() + ": " + t.getField1()));
    }
}

6.3 Spark local 运行

Java 17 上必须追加 --add-opens(否则报 IllegalAccessError ... sun.nio.ch.DirectBuffer),实测完整命令:

java -Xms256m -Xmx512m \
  --add-opens java.base/java.lang=ALL-UNNAMED \
  --add-opens java.base/java.lang.invoke=ALL-UNNAMED \
  --add-opens java.base/java.lang.reflect=ALL-UNNAMED \
  --add-opens java.base/java.io=ALL-UNNAMED \
  --add-opens java.base/java.net=ALL-UNNAMED \
  --add-opens java.base/java.nio=ALL-UNNAMED \
  --add-opens java.base/java.util=ALL-UNNAMED \
  --add-opens java.base/java.util.concurrent=ALL-UNNAMED \
  --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED \
  --add-opens java.base/sun.nio.ch=ALL-UNNAMED \
  --add-opens java.base/sun.util.calendar=ALL-UNNAMED \
  --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED \
  -cp "target/wayang-spark-demo-1.0.0.jar:target/lib/*" com.example.WordCountSpark

实测回显(节选,共 19 行):

SPARK>> can: 1
SPARK>> big: 2
SPARK>> cross: 1
SPARK>> spark: 2
SPARK>> a: 1
SPARK>> is: 1

6.4 输出对比

对比项Java 后端(WordCount)Spark local 后端(WordCountSpark)
处理逻辑完全相同完全相同
代码差异无导入 Spark、注册 Spark.basicPlugin()、注入 spark.master 共三处
词频结果wayang:4、data:3、big:2、spark:2 等与 Java 后端完全一致(19 个词条相同)
外部依赖仅 JDKSpark 3.4.4 依赖约 1 GB(Maven 缓存)
启动开销亚秒级有 local 模式 JVM 初始化开销

结论:同一计划在两个后端输出一致,Wayang 的"一次编写、多端执行"在 local 单进程场景下可完整跑通。

七、JVM 堆验证

任务运行期间另开会话执行:

ps -ef | grep "[j]ava -Xms"

实测回显:

root        5820    5818 99 14:48 ?        00:00:02 java -Xms256m -Xmx512m -cp target/wayang-demo-1.0.0.jar:target/lib/* com.example.WordCount
jps -v

实测回显:

5820 WordCount -Xms256m -Xmx512m

两处回显确认 -Xms256m -Xmx512m 生效。Maven 构建期的堆由 MAVEN_OPTS 控制(推荐写入 /etc/profile.d/wayang-maven.sh),应用运行期的堆由命令行或 JAVA_OPTS 控制。

八、常见问题

**Q1:**编译报 package org.apache.wayang.api does not exist? 补上 wayang-api-scala-java 依赖(版本与 wayang-core 同为 1.1.1)。1.x 把 JavaPlanBuilder 移到了该模块,实测补依赖后编译通过。

**Q2:**Spark 后端报 NoSuchMethodError,方法签名涉及 org.apache.hadoop.fs.FutureDataInputStreamBuilder? hadoop 依赖冲突:排除 wayang-spark 传递的 org.apache.hadoop:hadoop-client,让 Spark 自带的 hadoop-client-api/runtime 3.3.4 生效,实测排除后运行正常。

**Q3:**Spark 后端报 IllegalAccessError ... cannot access class sun.nio.ch.DirectBuffer? Java 17 模块封装限制,为 JVM 追加 6.3 节列出的 --add-opens 参数后实测跑通。

Q4:writeTextFile 编译报 no suitable method found? 1.1.1 的签名是三参数 (uri, formatter, encoding),补上编码实参如 "UTF-8" 即可,实测通过。

**Q5:**引入 Spark 依赖后构建很慢? wayang-spark 连带 Spark 3.4.4 与 hadoop 依赖,Maven 首次下载约 1 GB,属正常现象;java 后端工程(约 150 MB)不受影响,可分别建工程管理。

九、参考链接

  1. Apache Wayang 官网:https://wayang.apache.org/
  2. 快速上手指南:https://wayang.apache.org/docs/guide/getting-started
  3. GitHub 仓库(apache/wayang):https://github.com/apache/wayang
  4. Maven Central 上 wayang-core 构件:https://repo1.maven.org/maven2/org/apache/wayang/wayang-core/
  5. Maven Central 上 wayang-spark 构件:https://repo1.maven.org/maven2/org/apache/wayang/wayang-spark/
  6. Sonatype Central(版本检索):https://central.sonatype.com/artifact/org.apache.wayang/wayang-core