news 2026/10/20 16:07:41

Flink SQL报错找不到FileSystemFormatFactory的根因与修复

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flink SQL报错找不到FileSystemFormatFactory的根因与修复

1. 项目概述:Flink SQL读写文件时突然报错“找不到FileSystemFormatFactory”的真实场景还原

你正在用Flink SQL做实时数仓的轻量级ETL,本地调试一切正常,但一提交到YARN或Standalone集群就炸了——控制台第一行红字就是:Could not find any factories that implement org.apache.flink.table.factories.FileSystemFormatFactory。不是ClassNotFound,不是NoClassDefFoundError,而是“找不到实现类”。这说明Flink的SPI机制启动了,也扫描了jar包,但就是没扫到你要的那个工厂类。它不像语法错误那样一眼能定位,也不像内存溢出那样有明确堆栈指向资源,而是一种“明明该有的东西,系统却说不存在”的诡异状态。这个报错在Flink 1.14~1.18版本中高频出现,尤其集中在使用INSERT INTO ... SELECT ...写入HDFS、S3、OSS或本地文件系统(如file:///tmp/output)的场景里。它不挑Flink部署模式(YARN/K8s/Standalone),也不挑存储类型(Parquet/CSV/JSON),只要涉及FileSystem作为Table Sink,就可能中招。核心关键词是Flink SQL、FileSystemFormatFactory、Table API、格式工厂、SPI加载失败、flink-sql-jar缺失。这篇文章就是为你写的——不是泛泛而谈“加个依赖”,而是带你从Flink的表工厂加载机制底层出发,逐层拆解为什么找不到、在哪找、谁该负责提供、为什么本地跑得通集群就挂、以及如何用三步法精准定位并永久解决。无论你是刚接触Flink SQL的新手,还是已在线上踩过三次坑的运维同学,都能在这里找到可直接复现的诊断路径和经过生产验证的修复方案。

2. 核心机制拆解:Flink Table SPI工厂体系到底怎么工作?

2.1 Flink的“插件化”设计哲学:为什么非得用Factory模式?

Flink Table API和SQL模块从1.9开始彻底重构为可插拔架构,核心思想是:计算逻辑(Table/SQL)与数据源/目标(Connector)、序列化格式(Format)、执行环境(Planner)完全解耦。这种设计让Flink能支持上百种外部系统(Kafka、MySQL、Elasticsearch、Doris……),而无需把所有连接器代码都打进Flink主包。实现这一目标的关键机制,就是Java标准的SPI(Service Provider Interface)。简单说,Flink定义了一组接口(比如DynamicTableSourceFactory、DynamicTableSinkFactory、FileSystemFormatFactory),而每个Connector或Format的jar包,必须在自己的META-INF/services/目录下,放一个以接口全限定名为名的文本文件,里面写上自己实现类的完整类名。Flink启动时,会通过ServiceLoader去扫描所有classpath下的这些文件,自动加载并注册所有可用的实现。FileSystemFormatFactory正是负责将'format' = 'parquet'这类配置,翻译成具体Parquet读写器的工厂。它不直接干活,只负责“生孩子”——生成真正的ParquetRowDataInputFormat或ParquetRowDataOutputFormat实例。

提示:这不是Flink独创,Hadoop的FileSystem、Spark的DataSourceV2、甚至JDBC的DriverManager,底层都是同一套SPI逻辑。理解这一点,你就抓住了整个问题的命门。

2.2 报错根源定位:为什么“找不到”,而不是“找不到类”?

关键区别就在这里。ClassNotFoundException意味着JVM连.class文件都没见着;而Could not find any factories that implement ...意味着:

  • JVM成功加载了flink-table-common.jar(里面有FileSystemFormatFactory接口定义);
  • ServiceLoader也成功触发了扫描流程;
  • 但它扫描完所有jar后,发现没有任何一个META-INF/services/org.apache.flink.table.factories.FileSystemFormatFactory文件里,写了有效的实现类名。

换句话说:接口存在,扫描动作发生,但实现类的“注册声明”丢失了。这通常由三个原因导致:

  1. 缺失Format实现jar:比如你要用Parquet,但flink-parquet这个jar根本没进classpath;
  2. jar包未正确打包SPI文件:某些自定义或老旧版本的connector jar,META-INF/services/目录下压根没放对应文件,或文件名拼写错误(大小写敏感!);
  3. ClassLoader隔离导致扫描范围受限:这是最隐蔽也最常被忽略的一点。Flink的TaskManager启动时,会创建多层ClassLoader:System ClassLoader(加载Flink核心jar)、PluginClassLoader(加载用户jar)、UserCodeClassLoader(加载作业jar)。而ServiceLoader默认只扫描当前线程的ContextClassLoader所能看到的jar。如果Flink核心代码(比如TableEnvironmentImpl)是在System ClassLoader下运行的,它调用ServiceLoader.load()时,只会扫描flink-table-common.jar、flink-runtime_*.jar等Flink自带jar,而不会自动去扫描你上传的flink-parquet-1.17.1.jar——除非你显式告诉它“去那里找”。

2.3 版本兼容性陷阱:Flink 1.15+的“格式分离”改革

从Flink 1.15开始,官方对Format模块做了重大重构:将原本内置于flink-connectors-*中的Format实现,全部剥离到独立的flink-formats-*模块中。例如:

  • Flink 1.14及以前:flink-connector-hdfs_2.12-1.14.6.jar里同时包含HDFS Connector代码和Parquet Format代码;
  • Flink 1.15+:flink-connector-hdfs_2.12-1.15.0.jar只负责HDFS文件系统交互,而flink-format-parquet_2.12-1.15.0.jar才提供Parquet序列化能力。

这意味着,如果你升级了Flink版本但没同步更新依赖,或者直接复制了旧项目的pom.xml,就会出现“Connector有了,Format没了”的经典断链。更麻烦的是,flink-format-parquet本身又依赖parquet-avro、parquet-column等第三方库,这些传递依赖如果版本冲突(比如你的项目里引了parquet-avro:1.12.0,而Flink要求1.13.1),会导致flink-format-parquet的SPI文件虽在,但其内部类因依赖缺失而无法初始化,ServiceLoader在尝试newInstance()时抛异常,最终静默跳过该实现——结果还是“找不到”。

3. 实操诊断四步法:从日志到代码,精准锁定问题源头

3.1 第一步:确认报错发生的精确上下文

不要一看到红字就慌。先看完整堆栈,定位是哪个环节触发的。典型报错位置有两个:

  • DDL建表阶段:执行CREATE TABLE my_sink (...) WITH ('connector' = 'filesystem', 'format' = 'parquet', ...)时立即报错。这说明Flink在解析WITH参数时,就试图根据'format'值去查找对应的FileSystemFormatFactory,此时连作业图都没构建。
  • DML执行阶段:INSERT INTO my_sink SELECT * FROM source_table提交后,在JobManager解析ExecutionPlan时才报错。这说明DDL没问题,但Flink在组装Sink算子时,需要为filesystemconnector绑定具体的Format实现,此时才触发Factory查找。

注意:如果是DDL阶段报错,问题一定出在作业提交客户端(即你运行sql-client.sh或flink run命令的机器)的classpath;如果是DML阶段报错,则问题出在TaskManager的classpath(即集群节点上)。二者排查路径完全不同。

3.2 第二步:检查Flink发行版自带的Format支持

Flink官方二进制包(flink-1.17.1-bin-scala_2.12.tgz)在lib/目录下,预置了部分常用Format jar。进入你的Flink安装目录,执行:

ls lib/ | grep -i "format\|parquet\|csv\|json"

你应该看到类似:

flink-format-csv-1.17.1.jar flink-format-json-1.17.1.jar flink-format-avro-1.17.1.jar # 但很可能没有 flink-format-parquet-1.17.1.jar

这是因为Parquet属于“可选格式”,官方包默认不包含。而CSV和JSON是基础格式,所以自带。如果你用的是CSV,这个报错几乎不可能出现;但凡用了Parquet、ORC、Avro,就必须手动补全。

3.3 第三步:深度剖析ClassLoader加载路径(核心难点)

这才是区分新手和老手的关键。我们来模拟Flink的查找过程。假设你用flink run -c MyJob ./my-job.jar提交作业,且my-job.jar里包含了flink-format-parquet-1.17.1.jar(通过maven-shade-plugin打进去)。那么:

  • my-job.jar会被加载到UserCodeClassLoader;
  • flink-table-planner_2.12-1.17.1.jar(含TableEnvironmentImpl)在System ClassLoader;
  • 当TableEnvironmentImpl调用ServiceLoader.load(FileSystemFormatFactory.class)时,它用的是Thread.currentThread().getContextClassLoader(),而这个CL在TaskManager中默认是System ClassLoader(不是UserCodeClassLoader!)。

所以,即使你的flink-format-parquet.jar在my-job.jar里,ServiceLoader也看不到它。解决方案只有两个:

  1. 把Format jar放到Flink的lib/目录下(推荐,一劳永逸);
  2. 在代码中显式指定ClassLoader(不推荐,侵入性强):
    // 在你的main方法里,作业启动前 Thread.currentThread().setContextClassLoader( MyJob.class.getClassLoader() // 指向UserCodeClassLoader );
    但这会影响整个Flink运行时,风险极高,仅作技术原理演示,生产环境禁用。

3.4 第四步:验证SPI文件是否存在(终极证据)

找到你认为应该提供FileSystemFormatFactory实现的jar包(比如flink-format-parquet-1.17.1.jar),用jar -tf命令解压查看其内部结构:

jar -tf flink-format-parquet-1.17.1.jar | grep -i "services.*filesystemformatfactory"

正确输出应为:

META-INF/services/org.apache.flink.table.factories.FileSystemFormatFactory

再查看该文件内容:

jar -xf flink-format-parquet-1.17.1.jar META-INF/services/org.apache.flink.table.factories.FileSystemFormatFactory cat META-INF/services/org.apache.flink.table.factories.FileSystemFormatFactory

你应该看到一行:

org.apache.flink.formats.parquet.ParquetFileSystemFormatFactory

如果文件不存在、路径拼错(比如写成org/apache/flink/...)、或内容为空/注释掉/类名写错,那就是jar包本身有问题。这种情况常见于:

  • 使用了非官方编译的snapshot版本;
  • 从Maven仓库下载的jar被网络中断损坏;
  • 某些国内镜像源同步不及时,提供了残缺jar。

4. 完整修复方案与生产级配置清单

4.1 方案一:集群级统一修复(推荐给运维/平台团队)

这是最稳定、最易维护的方式,适用于所有作业共享同一套Format能力的场景。步骤如下:

  1. 下载正确的Format jar:访问 Flink Maven仓库 ,按你的Flink版本(如1.17.1)和Scala版本(如_2.12)下载:

    • flink-format-parquet_2.12-1.17.1.jar
    • flink-format-orc_2.12-1.17.1.jar(如果用ORC)
    • flink-format-avro_2.12-1.17.1.jar(如果用Avro)

    注意:务必核对<artifactId>和<version>,Flink 1.17.x的Format jar版本号必须严格匹配,不能混用1.16或1.18的jar。

  2. 校验jar完整性:

    # 下载后立即校验SHA256 sha256sum flink-format-parquet_2.12-1.17.1.jar # 对比Maven仓库页面提供的checksum
  3. 拷贝到所有TaskManager节点的lib/目录:

    # 假设Flink安装在 /opt/flink cp flink-format-parquet_2.12-1.17.1.jar /opt/flink/lib/ # 同步到所有节点(使用rsync或Ansible) rsync -av flink-format-parquet_2.12-1.17.1.jar user@tm-node2:/opt/flink/lib/
  4. 重启Flink集群:

    # 先停JobManager /opt/flink/bin/jobmanager.sh stop # 再停所有TaskManager /opt/flink/bin/taskmanager.sh stop # 最后启动(顺序不能错) /opt/flink/bin/jobmanager.sh start /opt/flink/bin/taskmanager.sh start
  5. 验证是否生效:提交一个最简测试作业:

    -- 创建一个只写本地文件的测试表 CREATE TABLE test_parquet ( id BIGINT, name STRING ) WITH ( 'connector' = 'filesystem', 'path' = 'file:///tmp/flink-test', 'format' = 'parquet', 'sink.parallelism' = '1' ); INSERT INTO test_parquet VALUES (1, 'test');

    如果不再报FileSystemFormatFactory错,且作业成功完成,说明修复成功。

4.2 方案二:作业级隔离修复(推荐给开发/数据工程师)

当你的集群由其他团队管理,无法修改lib/目录,或不同作业需要不同版本的Format时,必须采用此方案。核心是:确保Format jar在作业提交时,被Flink正确识别为“系统依赖”而非“用户代码”。有三种可靠方式:

方式A:使用-C参数指定额外classpath(最简洁)
flink run \ -C file:///path/to/flink-format-parquet_2.12-1.17.1.jar \ -c com.example.MyJob \ ./my-job.jar

-C参数会让Flink在启动TaskManager时,将指定jar加入System ClassLoader的搜索路径,从而被ServiceLoader扫描到。注意:

  • 路径必须是file://协议,不能是相对路径;
  • 所有节点上该jar路径必须一致(建议放在NFS或HDFS上);
  • 可同时指定多个-C,用空格分隔。
方式B:构建Uber Jar并启用ClassLoader委托(Maven最佳实践)

在pom.xml中配置maven-shade-plugin,关键点在于:

  • 将Format jar的META-INF/services/文件合并,避免覆盖;
  • 设置<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>;
  • 最重要:在<configuration>中添加:
    <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers>
    这样打包后的my-job.jar,其META-INF/services/目录下会包含所有依赖jar的SPI声明,并正确合并。然后提交时加参数:
    flink run -D classloader.checkClassLoading=false ./my-job.jar
    classloader.checkClassLoading=false会关闭Flink的类加载安全检查,允许UserCodeClassLoader加载Flink核心类(这是SPI合并生效的前提)。
方式C:SQL Client专用配置(适合Ad-hoc分析)

如果你用sql-client.sh交互式执行,编辑conf/sql-client-defaults.yaml:

execution: planner: blink type: batch result-mode: table max-table-result-rows: 1000000 # 关键:添加自定义jar路径 configuration: pipeline.jars: ["file:///opt/flink/ext-jars/flink-format-parquet_2.12-1.17.1.jar"] pipeline.classpaths: ["file:///opt/flink/ext-jars/"]

然后重启sql-client即可。pipeline.jars会强制将这些jar加入System ClassLoader。

4.3 生产环境避坑清单(血泪经验总结)

我在线上集群维护三年,踩过所有你能想到的坑,这里列出最致命的5条:

  1. 绝对不要混用Flink主版本与Format版本:flink-runtime_2.12-1.16.1.jar+flink-format-parquet_2.12-1.17.0.jar= 必然失败。Flink的SPI接口在小版本间可能有breaking change,必须严格对齐。
  2. Hadoop版本冲突是隐形杀手:flink-format-parquet依赖parquet-hadoop,而后者又依赖特定版本的hadoop-client。如果你的集群是Hadoop 3.3.4,但flink-format-parquet里带的是hadoop-client:3.2.1,就会在运行时抛NoSuchMethodError。解决方案:用mvn dependency:tree -Dincludes=org.apache.hadoop:hadoop-client检查传递依赖,必要时在pom中<exclusion>旧版,显式引入集群匹配版。
  3. S3/OSS等对象存储需额外依赖:filesystemconnector写S3时,除了flink-format-parquet,还必须提供hadoop-aws和aws-java-sdk-bundle(注意是bundle版,不是单独的aws-java-sdk-core)。漏掉任何一个,都会在运行时才报错,且错误信息与FileSystemFormatFactory无关,极易误判。
  4. 本地IDE调试成功的假象:IntelliJ默认用Application ClassLoader运行main方法,它会扫描整个module classpath,所以本地永远不报这个错。必须用flink run命令在伪分布式模式下测试,才能暴露真实问题。
  5. 日志级别调太低会错过关键线索:Flink默认log4j.properties中org.apache.flink.table.factories的日志级别是WARN,而SPI加载失败的DEBUG日志被过滤掉了。临时排查时,务必在conf/log4j-cli.properties中添加:
    logger.table-factory.name = org.apache.flink.table.factories logger.table-factory.level = DEBUG
    然后看DEBUG日志里ServiceLoader到底扫描了哪些jar,哪些jar被跳过了,一目了然。

5. 常见问题速查表与故障树分析

问题现象可能原因排查命令/步骤解决方案
报错发生在CREATE TABLE时DDL解析阶段就失败1. 检查sql-client或Flink CLI的classpath
2.echo $FLINK_HOME/lib/下是否有对应format jar
将format jar拷贝到$FLINK_HOME/lib/,重启CLI
报错发生在INSERT INTO提交后TaskManager加载失败1.ps aux | grep TaskManager确认进程启动
2. 查看log/taskmanager.out中是否有Loading service providers for ...日志
检查$FLINK_HOME/lib/或-C参数指定的jar路径是否正确
jar -tf能看到SPI文件,但依然报错SPI文件内容错误或类名无法加载1.cat META-INF/services/...确认类名拼写
2.jar -tf jar | grep ParquetFileSystemFormatFactory确认类文件存在
重新下载官方jar;或检查是否有同名类被其他jar覆盖
加了-C参数仍无效路径协议错误或节点不一致1.flink run -C file:///tmp/a.jar ...(必须3个/)
2.ssh tm-node2 ls -l /tmp/a.jar确认存在
改用HDFS路径:-C hdfs:///flink-jars/flink-format-parquet.jar
修复后新报NoClassDefFoundError: org/apache/parquet/hadoop/ParquetInputFormatParquet底层依赖缺失jar -tf flink-format-parquet.jar | grep parquet确认是否包含parquet类
flink run --help | grep "classpath"确认参数用法
下载parquet-hadoop-1.12.2.jar等传递依赖,同样放入lib/

故障树根因分析:90%的FileSystemFormatFactory问题,最终都指向ClassLoader隔离与版本错配两大根源。前者是架构设计使然,后者是工程管理疏忽。没有“万能修复”,只有“精准归因”。每一次报错,都是Flink在提醒你:检查你的依赖树,审视你的类加载路径。

6. 高级技巧:自动化检测脚本与CI/CD集成

靠人肉检查太慢,我们把它变成自动化流程。以下是一个Bash脚本,可在CI流水线中运行,提前拦截问题:

#!/bin/bash # check-flink-format.sh FLINK_VERSION="1.17.1" FORMAT_JAR="flink-format-parquet_2.12-${FLINK_VERSION}.jar" # 1. 检查jar是否存在且可读 if [ ! -f "$FORMAT_JAR" ]; then echo "ERROR: $FORMAT_JAR not found in current dir" exit 1 fi # 2. 检查SPI文件是否存在且非空 SPI_FILE=$(jar -tf "$FORMAT_JAR" 2>/dev/null | grep "services.*FileSystemFormatFactory") if [ -z "$SPI_FILE" ]; then echo "ERROR: SPI file missing in $FORMAT_JAR" exit 1 fi # 3. 检查SPI文件内容是否有效 CLASS_NAME=$(jar -xf "$FORMAT_JAR" "$SPI_FILE" 2>/dev/null && cat "$SPI_FILE" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ -z "$CLASS_NAME" ] || [[ "$CLASS_NAME" == *"#"* ]]; then echo "ERROR: SPI file empty or contains only comments in $FORMAT_JAR" exit 1 fi # 4. 检查目标类是否在jar中 if ! jar -tf "$FORMAT_JAR" 2>/dev/null | grep -q "$CLASS_NAME"; then echo "ERROR: Implementation class $CLASS_NAME not found in $FORMAT_JAR" exit 1 fi echo "SUCCESS: $FORMAT_JAR is valid for Flink $FLINK_VERSION"

将此脚本加入你的Mavenpom.xml的maven-antrun-plugin,或GitLab CI的before_script,就能在每次构建时自动验证Format jar的完整性。更进一步,可以将其封装为一个Flink插件检测服务,通过HTTP API接收jar包,返回结构化检测报告,供数据平台统一管控。

7. 我的实际操作体会:从焦头烂额到胸有成竹的心路历程

第一次遇到这个报错,是在凌晨两点上线一个实时报表任务。当时只想着“加个依赖就行”,在pom里加了flink-format-parquet,本地跑通就提交了。结果集群报错,重启三次,查文档、翻GitHub issue、问群友,折腾到早上六点,最后发现是版本号写成了1.17.0而不是1.17.1——就差一个小数点。那种挫败感,至今记得。后来我养成了一个铁律:任何Flink相关依赖,必须打开Maven仓库页面,手动复制<version>,绝不手敲。再后来,我把上面那个检测脚本做成了团队标配,现在新人入职第一天,就要跑一遍check-flink-format.sh,确保环境干净。最深的体会是:Flink的错误信息看似晦涩,但它的设计逻辑极其严谨。每一个报错,都是系统在告诉你“这里有个契约没被满足”。FileSystemFormatFactory找不到,本质是SPI契约断裂;而修复它,就是亲手把这条契约重新焊牢。当你能对着日志,说出“这个错是因为ClassLoader没扫到那个SPI文件”,而不是“百度一下加个依赖”,你就真正跨过了Flink的门槛。这条路没有捷径,只有一次又一次地直面日志、拆解jar、验证假设。但每解决一个问题,你对Flink的理解就下沉一寸,直到某天,你看到新的报错,第一反应不再是慌,而是微笑:“哦,又一个SPI没对齐,让我看看是哪个jar在捣鬼。”

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 16:05:29

QoS流量监管与整形实战:保障VoIP和视频会议的关键技术

1. 项目概述&#xff1a;为什么今天还在认真搞QoS&#xff1f;不是早该被SD-WAN和云网络淘汰了吗&#xff1f;“QoS流量监管和流量整形配置实例”——看到这个标题&#xff0c;不少刚接触网络运维的朋友第一反应可能是&#xff1a;这玩意儿是不是老古董了&#xff1f;现在都上云…

作者头像 李华
网站建设 2026/10/20 16:04:45

PSD设计稿到前端像素级还原:UI一致性工程实践

简介&#xff1a;本资源是面向HTML与CSS初学者及前端入门学习者的完整网页开发实践包&#xff0c;源自黑马程序员「学成在线」课程体系&#xff0c;聚焦真实项目还原与设计-代码协同训练。压缩包共11个文件&#xff0c;含4个PSD源文件&#xff08;涵盖首页、课程列表页、职业规…

作者头像 李华
网站建设 2026/10/20 16:03:35

context-mode:一种面向上下文感知的系统设计范式

项目标题中仅出现“context-mode”这一短语&#xff0c;无上下文、无领域指向、无功能描述、无技术栈提示&#xff0c;亦无任何补充信息&#xff08;项目正文为空、关键词未提供、摘要描述缺失&#xff09;。该字符串本身在主流技术生态中并非标准术语&#xff1a;它不是Linux内…

作者头像 李华
网站建设 2026/10/20 16:03:29

潮汕春节民俗全攻略:营老爷、英歌舞与烟火年味深度体验

每年春节前&#xff0c;总有人在朋友圈发类似的问句&#xff1a;“过年越来越没意思了&#xff0c;有没有哪里能找回小时候那种年味&#xff1f;”我以前也不知道怎么回&#xff0c;直到自己连着几个春节往潮汕跑&#xff0c;才敢说一句&#xff1a;要论春节氛围感&#xff0c;…

作者头像 李华
网站建设 2026/10/20 16:03:27

AI Agent改代码总翻车?问题出在工具层而非模型

1. 从一个让人头疼的现象说起 但凡用过 AI Agent 帮忙改代码的人&#xff0c;大概率都经历过这种崩溃时刻&#xff1a;你让它修一个空指针异常&#xff0c;它顺手把整个异常处理框架重构了&#xff1b;你让它加一个日志埋点&#xff0c;它把配置文件、依赖版本、目录结构全给你…

作者头像 李华
网站建设 2026/10/20 16:03:23

打破数据孤岛构建数据基石:制造业数据化实战指南

1. 破题&#xff1a;制造业转型真正的拦路虎&#xff0c;不是技术&#xff0c;是数据做了这么多年制造业数据项目&#xff0c;我最大的感触是&#xff1a;很多企业上ERP、上MES、上SCADA&#xff0c;钱花了不少&#xff0c;系统上了一堆&#xff0c;但老板看报表还是得靠Excel层…

作者头像 李华