基于SPI的插件式开发实现方案之@AutoService+ServiceLoader介绍及Dolphinscheduler中的实际应用

1.插件化开发概述

插件化开发模式正在很多编程语言或技术框架中得以广泛的应用实践,比如大家熟悉的jenkins,docker可视化管理平台rancher,以及日常编码使用的编辑器idea,vscode等。

实现服务模块之间解耦的方式有很多,但是插件来说,其解耦的程度似乎更高,而且更灵活,可定制化、个性化更好。

以spring来说,之所以具备如此广泛的生态,与其自身内置的各种可扩展的插件机制是分不开的。spring框架提供了很多基于插件化的扩展点。插件化机制让系统的扩展性得以提升,从而可以丰富系统的周边应用生态

2.插件化开发常见思路

以java为例,这里结合实际经验,整理一些常用的插件化实现思路:

  • spi机制;
  • 约定配置和目录,利用反射配合实现;
  • springboot中的Factories机制;
  • java agent(探针)技术;
  • spring内置扩展点;
  • 第三方插件包,例如:spring-plugin-core;
  • spring aop技术;

3.基于AutoService进行组件化开发

使用AutoService+ServiceLoader,本篇博客主要就是介绍此方案

缺点:使用的反射去实例化对象

优点:易配置,易调试,上手快

3.1. AutoService介绍

Github地址:https://github.com/google/auto/tree/master/service

AutoService是Google开源的用来方便生成符合ServiceLoader规范的开源库,使用非常的简单。官方的介绍是java.util.ServiceLoader 风格的服务提供者的配置/元数据生成器。

翻译成中文就是自动服务,这个程序能自动做什么?Java 注释处理器和其他系统使用 java.util.ServiceLoader 来注册使用 META-INF 元数据的已知类型的实现。但是,开发人员很容易忘记更新或正确指定服务描述符。

人工维护配置/元数据的过程
什么意思, 就是我们手动进行SPI插件开发的时候, 都需要手动在类加载路径classpath目录创建两级目录META-INF/services, 然后创建一个以需要扩展的接口的全限定路径名的名称的文件(javax.annotation.processing.Processor), 然后在文件中写入该接口的实现类的全限定路径名(com.yanyelai.MyProcessor)。

人工维护配置/元数据的弊端
如果类路径更新了或者接口的名称定义改变了,包名修改了等等,开发任务忘记了更新对应的配置文件,是不是就会发生问题,可能你会说这怎么可能,怎么会犯这种低级错误,不一定的, 团队越大,开发人员的水平也是良莠不齐, 不能保证所有人都能不出错。

AutoService就是用来解决以上问题的,使用了这个AutoService就不同手动创建这个配置文件了, 在插件编译打包会自动生成这个配置数据,不用再手动创建和维护这个配置文件了。

3.2. AutoService使用示例

在插件中引入AutoService服务

  <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.1.0</version>
            <scope>provided</scope>
        </dependency>

首先定义一个接口

package com.yanyelai;

import javax.annotation.processing.Processor;

@AutoService(Processor.class)
final class CustomProcessor implements Processor {
  // …
}

AutoService 将在输出类文件夹中生成文件
META-INF/services/javax.annotation.processing.Processor 。该文件将包含:

com.yanyelai.CustomProcessor

对于 javax.annotation.processing.Processor,如果此元数据文件包含在 jar 中,并且该 jar 位于 javac 的类路径上,则 javac 将自动加载它,并将其包含在其正常的注解处理环境中。java.util.ServiceLoader 的其他用户可能会出于不同的目的使用基础结构,但此元数据将适当地提供自动加载。

4.Dolphinscheduler中AutoService的实际应用案例

4.1.Task插件中AutoService的实际应用案例解读

海豚调度Dolphinscheduler中使用了插件化开发数据源、注册中心、告警插件及任务插件, 解读一个其他的都是类似的, 我们这里用Task插件为例来进行说明。

首先,dolphinscheduler的源码中肯定引用了AutoService依赖, 验证一下:
在这里插入图片描述
在这里插入图片描述

我们一起来看看dolphinscheduler中是如何实现插件加载的

dolphinscheduler-api的启动类ApiApplicationServer.java中进行了任务插件初始化加载,当Spring容器准备时,会触发任务插件安装的方法执行, 源码如下图:
在这里插入图片描述

    @EventListener
    public void run(ApplicationReadyEvent readyEvent) {
        logger.info("Received spring application context ready event will load taskPlugin and write to DB");
        // install task plugin 安装任务插件,这个方法执行会动态加载所有的任务插件注册到Spring容器
        taskPluginManager.loadPlugin();
        for (Map.Entry<String, TaskChannelFactory> entry : taskPluginManager.getTaskChannelFactoryMap().entrySet()) {
            String taskPluginName = entry.getKey();
            TaskChannelFactory taskChannelFactory = entry.getValue();
            List<PluginParams> params = taskChannelFactory.getParams();
            String paramsJson = PluginParamsTransfer.transferParamsToJson(params);

            PluginDefine pluginDefine = new PluginDefine(taskPluginName, PluginType.TASK.getDesc(), paramsJson);
            pluginDao.addOrUpdatePluginDefine(pluginDefine);
        }
    }

dolphinscheduler-service中的TaskPluginManager类中看看loadPlugin方法是如何实现任务插件加载的

   /**
     * 从classpath加载任务插件
     */
    public void loadPlugin() {
        if (!loadedFlag.compareAndSet(false, true)) {
            logger.warn("The task plugin has already been loaded");
            return;
        }
        // 创建了PrioritySPIFactory工厂类,加载TaskChannelFactory的实现类就在这里进行
        PrioritySPIFactory<TaskChannelFactory> prioritySPIFactory = new PrioritySPIFactory<>(TaskChannelFactory.class);
        for (Map.Entry<String, TaskChannelFactory> entry : prioritySPIFactory.getSPIMap().entrySet()) {
            String factoryName = entry.getKey();
            TaskChannelFactory factory = entry.getValue();

            logger.info("Registering task plugin: {} - {}", factoryName, factory.getClass());

            taskChannelFactoryMap.put(factoryName, factory);
            taskChannelMap.put(factoryName, factory.create());

            logger.info("Registered task plugin: {} - {}", factoryName, factory.getClass());
        }
    }

dolphinscheduler-spi中看看PrioritySPIFactory的有参构造方法

    public PrioritySPIFactory(Class<T> spiClass) {
    	// 这里调用了ServiceLoader.load动态从classpath中加载TaskChannelFactory的实现类,使用反射注入到Spring容器,
    	// 那么肯定所有的TaskChannelFactory的实现类上都加了@AutoService(TaskChannelFactory.class)注解。
        for (T t : ServiceLoader.load(spiClass)) {
            if (map.containsKey(t.getIdentify().getName())) {
                resolveConflict(t);
            } else {
                map.put(t.getIdentify().getName(), t);
            }
        }
    }

看到了吧,这里就是用ServiceLoader类实现TaskChannelFactory接口实现类的加载。

验证一下所有的TaskChannelFactory的实现类上都是用了@AutoService(TaskChannelFactory.class)注解

dolphinsscheduler中当前已经提供的TASK的插件列表如下:

TaskChannelFactory
├─ TaskChannelFactory               // TaskChannelFactory接口
│  └─ SubProcessTaskChannelFactory	// SubProcessTask接口实现工厂类
│  └─ PythonTaskChannelFactory 		// PythonTask接口实现工厂类 
│  └─ SqlTaskChannelFactory			// SqlTask接口实现工厂类 
│  └─ JupyterTaskChannelFactory		// JupyterTask接口实现工厂类 
│  └─ DependentTaskChannelFactory	// DependentTask接口实现工厂类
│  └─ DataxTaskChannelFactory		// DataxTask接口实现工厂类
│  └─ HttpTaskChannelFactory		// HttpTask接口实现工厂类
│  └─ PigeonTaskChannelFactory      // PigeonTask接口实现工厂类
│  └─ ShellTaskChannelFactory       // ShellTask接口实现工厂类
│  └─ ZeppelinTaskChannelFactory    // ZeppelinTask接口实现工厂类
│  └─ MlflowTaskChannelFactory      // MlflowTask接口实现工厂类
│  └─ DinkyTaskChannelFactory       // DinkyTask接口实现工厂类
│  └─ FlinkTaskChannelFactory       // FlinkTask接口实现工厂类
│  └─ SparkTaskChannelFactory       // SparkTask接口实现工厂类
│  └─ SagemakerTaskChannelFactory   // SagemakerTask接口实现工厂类
│  └─ EmrTaskChannelFactory         // EmrTask接口实现工厂类
│  └─ K8sTaskChannelFactory         // K8sTask接口实现工厂类
│  └─ SeatunnelTaskChannelFactory   // SeatunnelTask接口实现工厂类
│  └─ FlinkStreamTaskChannelFactory // FlinkStreamTask接口实现工厂类
│  └─ ConditionsTaskChannelFactory  // ConditionsTask接口实现工厂类
│  └─ DvcTaskChannelFactory         // DvcTask接口实现工厂类
│  └─ OpenmldbTaskChannelFactory    // OpenmldbTask接口实现工厂类
│  └─ ChunJunTaskChannelFactory     // ChunJunTask接口实现工厂类
│  └─ SqoopTaskChannelFactory       // SqoopTask口实现工厂类
│  └─ DataQualityTaskChannelFactory // DataQualityTask接口实现工厂类
│  └─ BlockingTaskChannelFactory    // BlockingTask接口实现工厂类
│  └─ PytorchTaskChannelFactory     // PytorchTask接口实现工厂类
│  └─ SwitchTaskChannelFactory      // SwitchTask接口实现工厂类
│  └─ HiveCliTaskChannelFactory     // HiveCliTask接口实现工厂类
│  └─ MapReduceTaskChannelFactory   // MapReduceTask接口实现工厂类
│  └─ ProcedureTaskChannelFactory   // ProcedureTask接口实现工厂类

我们找一个TaskChannelFactory实现类,这里就拿SqlTaskChannelFactory实现类来解释(其他的实现类都是大同小异)

@AutoService(TaskChannelFactory.class)
public class SqlTaskChannelFactory implements TaskChannelFactory {
    @Override
    public String getName() {
        return "SQL";
    }

    @Override
    public List<PluginParams> getParams() {
        return null;
    }

    @Override
    public TaskChannel create() {
        return new SqlTaskChannel();
    }
}

发现了没,实现类都是用@AutoService进行注解,那么打包之后肯定会自动在classpath的META-INF\services目录下生成一个以接口为名称org.apache.dolphinscheduler.plugin.task.api.TaskChannelFactory
的文件,里面的内容就是SqlTaskChannelFactory实现类的全限定路径名称,

在这里插入图片描述
SqlTask的插件打成的jar包中也包含了这个元数据文件,如果此元数据文件包含在 jar 中,并且该 jar 位于 javac 的类路径上,则 javac 将自动加载它,并将其包含在其正常的注解处理环境中。java.util.ServiceLoader 的其他用户可能会出于不同的目的使用基础结构,但此元数据将适当地提供自动加载。
在这里插入图片描述
通过以上方式, 就实实现了任务插件的自动加载,通过源码解读之后, 是不是发现其实插件化开发也挺简单的, 没想象的那么复杂。

4.2.dollphinscheduler的Task插件开发

以上我们讲述了插件加载的所有过程,下面我们再讲讲怎么进行Task的插件开发, 如果不会, 我们可以找一个现有的插件看看它的实现思路,因为所有的插件的主体框架肯定是一样, 去源码里面验证一下:
dolphinscheduler的task插件都在dolphinscheduler-task-plugin模块下
在这里插入图片描述
我们就用SQL的任务插件来看看,插件应该怎么写
在这里插入图片描述
看到没有, 插件的开发是不是很简单, 基本见名知意:

  • SqlTaskChannelFactory
    这个就是实现TaskChannelFactory接口的实现类,最主要的方法肯定包含获取任务类型名称及创建SqlTaskChannel的方法

  • SqlTaskChannel
    这个就是实现TaskChannel接口的实现类,最主要的方法肯定包含创建任务、解析任务参数、获取资源信息(数据源等)

  • SqlTask
    Sql任务,主要就是围绕Sql类型的任务的处理, 包含SQL任务中参数的获取、SQL任务的处理逻辑、任务的取消、查询和修改的SQL语句的处理逻辑等等, 这块应该是整个自定义插件开发的核心, 其中包含了不同于其他任务插件的逻辑处理,这里基本都是可以定制处理业务的。

  • SqlSplitter
    这个应该是进行SQL语句的分割处理的, SQL任务插件应该支持自定义SQL语句执行,所以如果用户自定义输入了多条SQL语句, 肯定需要按照某种特定的规则进行解析处理,然后再执行。

  • SqlBinds
    这个应该也很明显, SQL中语句跟输入参数的关系绑定, 然后在任务处理时再处理这种绑定关系,完成参数变量和参数值的替换, 获取到真正需要执行的SQL语句,在进行下一步处理。

通过SQL任务插件的源码解读, 我们知道如果我需要二开一个自己的任务插件,

首先创建一个插件模块dolphinscheduler-task-demo,

pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ Licensed to the Apache Software Foundation (ASF) under one or more
  ~ contributor license agreements.  See the NOTICE file distributed with
  ~ this work for additional information regarding copyright ownership.
  ~ The ASF licenses this file to You under the Apache License, Version 2.0
  ~ (the "License"); you may not use this file except in compliance with
  ~ the License.  You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<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>
    <parent>
        <groupId>org.apache.dolphinscheduler</groupId>
        <artifactId>dolphinscheduler-task-plugin</artifactId>
        <version>3.1.5</version>
    </parent>
    <artifactId>dolphinscheduler-task-demo</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.apache.dolphinscheduler</groupId>
            <artifactId>dolphinscheduler-spi</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.dolphinscheduler</groupId>
            <artifactId>dolphinscheduler-task-api</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
</project>

先定义一个实现了TaskChannelFactoryMyTaskChannelFactory类, 如下:

@AutoService(TaskChannelFactory.class)
public class MyTaskChannelFactory implements TaskChannelFactory {
    @Override
    public String getName() {
        return "DEMO";
    }

    @Override
    public List<PluginParams> getParams() {
        return null;
    }

    @Override
    public TaskChannel create() {
        return new MyTaskChannel();
    }
}

再定义一个实现了TaskChannelMyTaskChannel类, 如下:

public class MyTaskChannel implements TaskChannel {
    @Override
    public void cancelApplication(boolean status) {

    }

    @Override
    public AbstractTask createTask(TaskExecutionContext taskRequest) {
        return new MyTask(taskRequest);
    }

    @Override
    public AbstractParameters parseParameters(ParametersNode parametersNode) {
        // 这里就是从任务定义中获取参数并解析方法, 如果我们的任务插件中有一些定制的参数输入要求
        // 需要自定义一个参数类继承AbstractParameters,然后增加自定义的任务输入参数字段即可
        return JSONUtils.parseObject(parametersNode.getTaskParams(), MyParameters.class);
    }

    @Override
    public ResourceParametersHelper getResources(String parameters) {
    	// 如果你的参数是字符串,可以使用下面的这种方式进行解析,然后在解析出来的对象中获取属性信息
        return JSONUtils.parseObject(parameters, MyParameters.class).getResources();
    }

}

再定义一个实现了org.apache.dolphinscheduler.plugin.task.api.AbstractTaskMyTask类,如果你的自定义任务需要提交给Yarn去进行资源调度,那个则需要实现 org.apache.dolphinscheduler.plugin.task.api.AbstractYarnTask。以下三个方法必须重写,其他的集成方法可以按需重写。

public class MyTask implements AbstractTask{

    public abstract void handle(TaskCallBack taskCallBack) throws TaskException {
		  // 任务的逻辑处理
    }

    public abstract void cancel() throws TaskException {
        // 任务的取消
    }

    public abstract AbstractParameters getParameters() {
        // 任务的参数获取
    };

}

然后在任务处理的方法中如果需要一些额外的工具方法, 可以创建一些辅助工具类,这样基本一个组件就开发好了。然后前端根据我们自定义组件的输入参数要求进行前端页面的开发然后对接调试就可以了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/315046.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录二刷 |二叉树 | 二叉搜索树的最小绝对差

代码随想录二刷 &#xff5c;二叉树 &#xff5c; 二叉搜索树的最小绝对差 题目描述解题思路 & 代码实现递归法迭代法 题目描述 530.二叉搜索树的最小绝对差 给你一棵所有节点为非负值的二叉搜索树&#xff0c;请你计算树中任意两节点的差的绝对值的最小值。 示例&#…

10款热门的企业报表工具软件,看看哪款最适合?

1. Microsoft Office Excel&#xff1a;这款软件一般比较简单&#xff0c;适合处理小量数据&#xff0c;常被用来制作报表。 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 2. VeryReport&#xff1a;这是一款由纯Java编写的报表软件&#xff0c;兼具数…

[易语言]使用易语言部署工业级人脸检测模型

【框架地址】 https://github.com/ShiqiYu/libfacedetection 【算法介绍】 Libfacedetection是一个开源的计算机视觉库&#xff0c;主要用于实时的人脸检测。它利用深度学习技术&#xff0c;特别是卷积神经网络&#xff08;CNN&#xff09;&#xff0c;实现了高精度的脸部定位…

知识库系统搭建不用愁,有这些工具就够了

对于企业来说&#xff0c;知识库不仅是存储和管理知识的出色工具&#xff0c;更是建立有效知识共享和团队合作的有力助手。好的知识库工具可以实现知识的分类、检索和分享&#xff0c;提升工程效率&#xff0c;降低内部沟通成本。对于追求效率的你&#xff0c;下面介绍的三款知…

每天刷两道题——第十四天

1.1矩阵置零 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用原地算法。 输入&#xff1a;matrix [[0,1,2,0],[3,4,5,2],[1,3,1,5]] 输出&#xff1a;[[0,0,0,0],[0,4,5,0],[0,3,1,0]] 原地算法&#xff08;…

Jetson_yolov8_解决模型导出.engine遇到的问题、使用gpu版本的torch和torchvision、INT8 FP16量化加快推理

1、前情提要 英伟达Jetson搭建Yolov8环境过程中遇到的各种报错解决&#xff08;涉及numpy、scipy、torchvision等&#xff09;以及直观体验使用Yolov8目标检测的过程&#xff08;CLI命令行操作、无需代码&#xff09;-CSDN博客和YOLOv8_测试yolov8n.pt&#xff0c;yolov8m.pt训…

Java十大经典算法—KMP

字符串匹配问题&#xff1a; 1.暴力匹配 public class ViolenceMatch {public static void main(String[] args) {String str1 "硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好";String str2 "尚硅谷你尚硅你好";int index violenceMatch(str1, str2);S…

十二、QProgressBar的简单使用与样式优化(Qt5 GUI系列)

目录 一、设计需求 二、实现代码 三、代码解析 四、总结 五、扩展(自定义QProgressBar样式) 一、设计需求 在很多应用程序中&#xff0c;在执行费时操作时都会展示一个进度条来展示操作进行的进度。常见的场景&#xff0c;如&#xff1a;拷贝操作、安装操作以及卸载操作。…

JAVA安卓无线点餐系统源码

JAVA安卓无线点餐系统源码 本项目是带后台管理和客户端和SQL server数据库的完整项目&#xff0c;后台用SSH框架

【方法】PDF文件如何设置密码?

PDF文件可以通过浏览器打开查看&#xff0c;但如果想要设置密码保护&#xff0c;就需要用到相关的软件&#xff0c;下面分享两种常用的软件。 1. PDF编辑器 PDF编辑器除了可以编辑修改PDF文件&#xff0c;还可以用来设置密码。 以小编使用的PDF编辑器为例&#xff0c;通过PD…

“具身智能”浪潮中,达闼机器人的商业化“奇点”已然到来?

当前&#xff0c;人形机器人产业正在快速发展&#xff0c;而2023年必将会是载入史册的一年。 具体来看&#xff0c;2023年&#xff0c;AI技术大爆发&#xff0c;可在语言、视觉、运动控制、降低研发成本等多方面赋能人形机器人产业发展。与此同时&#xff0c;特斯拉、波士顿动…

基础面试题整理1

1.面向对象的特点 继承&#xff08;复用性&#xff09;、封装&#xff08;复用性&#xff09;、多态&#xff08;可移植性、灵活性&#xff09; 2.ArrayList与LinkedList区别 ArrayList和LinkedList都是实现了List接口 ArrayList底层是动态数组 LinkedList底层是链表&#…

Windows开机后,Docker失败:Commoncauses include access rights issues

这种错误看似已经跟你说很清楚了&#xff0c;但是看国外docker社区也提到这个问题&#xff0c;一大堆回答解决了别人的问题&#xff0c;但未必解决你的。我写自己的方案&#xff0c;可能也未必适合你&#xff0c;如果要说Root Cause根源就是windows的虚拟化功能开启的问题。 An…

基于SSM的驾校预约管理系统

基于SSM的驾校预约管理系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringSpringMVCMyBatis工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 主页 详情 管理员界面 摘要 随着社会的不断发展&#xff0c;驾驶技能的需求逐渐增…

老师的课堂行为包括什么内容

课堂行为对于学生的学习体验和成长至关重要。我在课堂上的一举一动&#xff0c;不仅影响着学生的学习效果&#xff0c;还关系着学生的心理健康和人格发展。那么&#xff0c;老师的课堂行为究竟包括哪些内容呢&#xff1f;接下来&#xff0c;我将以知乎老师的口吻&#xff0c;为…

【软件测试】路径覆盖

题目要求&#xff1a; a) 流程图如下&#xff1a; b) Consider test cases ti (n 3) and t2 ( n 5). Although these tour the same prime paths in printPrime(), they dont necessarily find the same faults. Design a simple fault that t2 would be more lik…

UE4运用C++和框架开发坦克大战教程笔记(十四)(第43~45集)

UE4运用C和框架开发坦克大战教程笔记&#xff08;十四&#xff09;&#xff08;第43~45集&#xff09; 43. 单个加载 UObject 功能获取资源 URL 链接实现异步加载单个 UObject 类型资源 44. 批量加载 UObject 功能测试加载单个 UObject 资源批量加载多个同类的 UObject 资源 45…

Win10系统读不出U盘的四种解决方法

有用户特别喜欢用U盘来保存重要的内容&#xff0c;但有用户反映自己的Win10电脑读取不了U盘&#xff0c;这样用户就不能将Win10电脑上的内容传输到U盘了。下面小编带来四种简单有效的解决方法&#xff0c;解决后Win10电脑上的U盘就能被正常识别&#xff0c;从而恢复对U盘的使用…

【Linux笔记】进程等待与程序替换

一、进程的终止 1、进程退出码 在讲解进程的终止之前&#xff0c;先要普及一下进程的退出码概念。 我们父进程之所以要创建子进程&#xff0c;就是为了让子进程运行不一样的任务&#xff0c;那么对于子进程执行的这个任务执行完毕后的结果是否正确或者是否出差错&#xff0c…

QT上位机开发(利用tcp/ip访问plc)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 plc是工控领域很重要的一个器件。简单的plc一般就是对io进行控制&#xff0c;但是复杂的plc&#xff0c;还可以控制电机、变频器&#xff0c;在工业…