小议CompletableFuture 链式执行和响应式编程

相关文章:

  • 用最简单的方式理解同步和异步、阻塞与非阻塞
  • CompletableFuture 实战

背景

昨晚和一个朋友讨论了他在开发过程中遇到的一个场景设计问题。这个场景可以简化为:服务接收到一个需要处理的任务请求,然后立即返回。这个任务需要经过四个处理器的处理,每个处理器的处理都依赖于前一个处理器的处理结果。

这个场景与我最近处理 GitLab WebHook 的工作流程非常相似。例如,我从 GitLab 接收到一个事件后,需要对数据进行清洗、解析、调用 AI 大模型和发布评论等操作,每个操作都依赖于前一个操作的结果。

对于这种场景的设计,我目前采用的是基于链式设计的方法。我定义了一个抽象的处理器类和链式工厂:

/**
 * @author dongguabai
 * @date 2024-01-09 13:52
 */
public abstract class MergeRequestHandler {

    private MergeRequestHandler next;

    public MergeRequestHandler(MergeRequestHandler next) {
        this.next = next;
    }

    public void handle(Task task) {
        if (run(task) && next != null) {
            next.handle(task);
        }
    }

    public abstract boolean run(Task task);
}
/**
 * @author dongguabai
 * @date 2024-01-09 15:53
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MergeRequestHandlerFactory {

    public static MergeRequestHandler createHandlerChain() {
        return new ActionHandler(new MergeRequestRetrievalHandler(new CommentHandler(new FinalHandler())));
    }
}

在链式工厂里面指定了执行顺序。

当然,也可以参考 ZooKeeper 的实现:org.apache.zookeeper.server.quorum.LeaderZooKeeperServer#setupRequestProcessors

@Override
    protected void setupRequestProcessors() {
        RequestProcessor finalProcessor = new FinalRequestProcessor(this);
        RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(
                finalProcessor, getLeader().toBeApplied);
        commitProcessor = new CommitProcessor(toBeAppliedProcessor,
                Long.toString(getServerId()), false);
        commitProcessor.start();
        ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this,
                commitProcessor);
        proposalProcessor.initialize();
        firstProcessor = new PrepRequestProcessor(this, proposalProcessor);
        ((PrepRequestProcessor)firstProcessor).start();
    }

然后,朋友提出要将其改为异步处理。实际上,异步处理也是可以很简单实现的,只需在执行 HandlerChain 时将其放入线程池中即可。但在这里,我完全可以使用 CompletableFuture 来实现,这也是我当前代码改造的主要方向。

关于CompletableFuture的实现,我设计了两套方案。现在来看一下最终实现的方案。

抽象 Processor

@Log4j
abstract class Processor {

    public CompletableFuture<Task> process(Task task, Executor executor) {
        return CompletableFuture.supplyAsync(() -> doProcess(task), executor);
    }

    protected abstract Task doProcess(Task task);
}

实现类:

@Log4j
class Processor1 extends Processor {
    @Override
    protected Task doProcess(Task task) {
        // 你的处理逻辑
        log.info("Processor1 is processing");
        ProcessorChain.sleep(1);
        log.info("Processor1 end");
        if (task.isCancelled()) {
            throw new CancellationException("Task was cancelled");
        }
        return task;
    }
}
@Log4j
class Processor2 extends Processor {
    @Override
    protected Task doProcess(Task task) {
        // 你的处理逻辑
        log.info("Processor2 is processing");
        ProcessorChain.sleep(2);
        log.info("Processor2 end");
        if (task.isCancelled()) {
            throw new CancellationException("Task was cancelled");
        }
        return task;
    }
}

核心的处理链:

@Log4j
class ProcessorChain {
    private final List<Processor> processors;

    public ProcessorChain(List<Processor> processors) {
        this.processors = processors;
    }

    public CompletableFuture<Void> process(Task task, Executor executor) {
        CompletableFuture<Task> future = CompletableFuture.completedFuture(task);
        for (Processor processor : processors) {
            future = future.thenComposeAsync(t -> processor.process(t, executor), executor);
        }
        return future.thenAccept(result ->log.info("处理完成,结果是:" + result));
    }

    public static void sleep(Integer i){
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试代码:

    public static void main(String[] args) {
        Executor executor = Executors.newCachedThreadPool();
        ProcessorChain processorChain = new ProcessorChain(Arrays.asList(new Processor1(), new Processor2(), new Processor3(), new Processor4()));
        Task task = new Task("1");
        CompletableFuture<Void> exceptionally = processorChain.process(task, executor).exceptionally(ex -> {
            System.out.println("处理过程中出现错误:" + ex.getMessage());
            return null;
        });
        log.info("处理完成");
    }

输出:

22:38:35.276 [main] INFO demo.sb1.ProcessorChainTest - 处理完成
22:38:35.276 [pool-1-thread-2] INFO demo.sb1.Processor1 - Processor1 is processing
22:38:36.285 [pool-1-thread-2] INFO demo.sb1.Processor1 - Processor1 end
22:38:36.285 [pool-1-thread-3] INFO demo.sb1.Processor2 - Processor2 is processing
22:38:38.290 [pool-1-thread-3] INFO demo.sb1.Processor2 - Processor2 end
22:38:38.290 [pool-1-thread-3] INFO demo.sb1.Processor3 - Processor3 is processing
22:38:41.294 [pool-1-thread-3] INFO demo.sb1.Processor3 - Processor3 end
22:38:41.294 [pool-1-thread-3] INFO demo.sb1.Processor4 - Processor4 is processing
22:38:45.299 [pool-1-thread-3] INFO demo.sb1.Processor4 - Processor4 end
22:38:45.299 [pool-1-thread-3] INFO demo.sb1.ProcessorChain - 处理完成,结果是:demo.sb1.Task@6dfb9646

效果挺符合诉求的,但这里也有几个疑问。

ProcessorChain#process中的执行顺序问题

先看 ProcessorChain#proccess

    public CompletableFuture<Void> process(Task task, Executor executor) {
        CompletableFuture<Task> future = CompletableFuture.completedFuture(task);
        for (Processor processor : processors) {
            future = future.thenComposeAsync(t -> processor.process(t, executor), executor);
        }
        return future.thenAccept(result -> System.out.println("处理完成,结果是:" + result));
    }

朋友原话是:“他期望的执行顺序是 process1process2process3。但由于 thenComposeAsync是异步执行的,那么在循环到 process2 时,它会异步执行。此时,循环可能已经到了 process3,并开始执行 thenComposeAsync。这样,process3process2可能会并行执行,这跟期望结果不一致”。

其实这里是不会的,thenComposeAsync 可以翻译为 “然后异步组合”。

这里的 “组合” 指的是将当前的 CompletableFuture 与另一个 CompletableFuture 进行组合。“异步” 指的是这里的 Function 操作会在一个单独的线程中执行,不会阻塞当前的线程。

我这里的 Processor#process 函数返回的是 CompletableFuture,所以这里 thenComposeAsync 的流程是:等待当前的 CompletableFuture 完成后,异步执行 FunctionFunction 会返回一个新的 CompletableFuture,然后 thenComposeAsync 会返回这个 CompletableFuture

这里要注意 thenApplyAsyncthenComposeAsync 不要混淆了。

ProcessorChain#process中的阻塞与非阻塞、同步和异步问题

上文已经说明了 ProcessorChain#process 中的执行顺序问题。接下来看一下这段代码中的阻塞与非阻塞、同步和异步问题。

为了查看方便,我这里再贴一下测试代码和 ProcessorChain#proccess 的实现。

测试代码:

    public static void main(String[] args) {
        Executor executor = Executors.newSingleThreadExecutor();
        ProcessorChain processorChain = new ProcessorChain(Arrays.asList(new Processor1(), new Processor2(), new Processor3(), new Processor4()));
        Task task = new Task("1");
        CompletableFuture<Void> c = processorChain.process(task, executor).exceptionally(ex -> {
            System.out.println("处理过程中出现错误:" + ex.getMessage());
            return null;
        });
        log.info("处理完成");
    }

ProcessorChain#proccess

    public CompletableFuture<Void> process(Task task, Executor executor) {
        CompletableFuture<Task> future = CompletableFuture.completedFuture(task);
        for (Processor processor : processors) {
            future = future.thenComposeAsync(t -> processor.process(t, executor), executor);
        }
        return future.thenAccept(result -> System.out.println("处理完成,结果是:" + result));
    }

执行流程如下:

  1. main 线程开始执行,并调用 ProcessorChainprocess 方法。
  2. ProcessorChainprocess 方法中,main 线程创建了一个已经完成的 CompletableFuture,并开始遍历 Processor 对象。
  3. main 线程调用 thenComposeAsync 方法,此时 main 线程将 Processor1process 方法提交给 Executor,并立即返回一个新的 CompletableFuturemain 线程不会等待 Processor1process 方法完成,因此 main 线程是非阻塞的。
  4. Executor 的一个线程(称之为 Thread_1)开始执行 Processor1process 方法。由于 thenComposeAsync 的链式调用,Processor2process 方法会等待 Processor1process 方法完成后才开始执行,以此类推。
  5. main 线程继续遍历 Processor 对象,并对每个 Processor 重复步骤 3 和 4。每次调用 thenComposeAsync 方法时,main 线程都会立即返回一个新的 CompletableFuture,并将 Processorprocess 方法提交给 Executor。这意味着 main 线程是非阻塞的,而 Executor 的线程会按照 Processor 的顺序执行 process 方法。
  6. 当所有的 Processorprocess 方法都完成后,其中一个 Executor 的线程会执行 thenAccept 方法,处理最后一个 Processorprocess 方法返回的结果。

其实这里的 CompletableFuture<Task> future = CompletableFuture.completedFuture(task); 只是作为一个“CompletableFuture 的启动器”(这么一说是不是就更好理解了)。

响应式编程

其实上面的内容让我想起了响应式编程,引入维基百科的一个说明:

例如,在命令式编程环境中,a:=b+c表示将表达式的结果赋给a,而之后改变b或c的值不会影响a。但在响应式编程中,a的值会随着b或c的更新而更新。电子表格程序就是响应式编程的一个例子。单元格可以包含字面值或类似"=B1+C1"的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化 。

其实这里的 CompletableFuture 实际上是响应式编程的一个简单例子,因为它表示一个异步计算的结果,我们可以在它上面“提前”注册回调函数(我觉得这个“提前”是精髓),这些回调函数会在 CompletableFutureFunction 完成时被调用。而我这里就是提前准备好了一个结果,即 CompletableFuture,然后不断的循环在其上面增加回调。

可以增加日志看一下:

    public CompletableFuture<Void> process(Task task, Executor executor) {
        CompletableFuture<Task> future = CompletableFuture.completedFuture(task);
        for (Processor processor : processors) {
            log.info(future);
            future = future.thenComposeAsync(t -> processor.process(t, executor), executor);
            log.info(future);
          // future.thenComposeAsync(t -> processor.process(t, executor), executor);
        }
        return future.thenAccept(result ->log.info("处理完成,结果是:" + result));
    }

再次执行测试代码:

22:56:42.512 [main] INFO demo.sb1.ProcessorChain - java.util.concurrent.CompletableFuture@3fa77460[Completed normally]
22:56:42.611 [main] INFO demo.sb1.ProcessorChain - java.util.concurrent.CompletableFuture@e2d56bf[Not completed]
22:56:42.611 [main] INFO demo.sb1.ProcessorChain - java.util.concurrent.CompletableFuture@e2d56bf[Not completed]
22:56:42.612 [main] INFO demo.sb1.ProcessorChain - java.util.concurrent.CompletableFuture@244038d0[Not completed]
22:56:42.612 [main] INFO demo.sb1.ProcessorChain - java.util.concurrent.CompletableFuture@244038d0[Not completed]
22:56:42.612 [main] INFO demo.sb1.ProcessorChain - java.util.concurrent.CompletableFuture@5680a178[Not completed]
22:56:42.612 [main] INFO demo.sb1.ProcessorChain - java.util.concurrent.CompletableFuture@5680a178[Not completed]
22:56:42.612 [main] INFO demo.sb1.ProcessorChain - java.util.concurrent.CompletableFuture@5fdef03a[Not completed]
22:56:42.614 [main] INFO demo.sb1.ProcessorChainTest - 处理完成
22:56:42.615 [pool-1-thread-2] INFO demo.sb1.Processor1 - Processor1 is processing
22:56:43.620 [pool-1-thread-2] INFO demo.sb1.Processor1 - Processor1 end
22:56:43.620 [pool-1-thread-2] INFO demo.sb1.Processor2 - Processor2 is processing
22:56:45.625 [pool-1-thread-2] INFO demo.sb1.Processor2 - Processor2 end
22:56:45.625 [pool-1-thread-2] INFO demo.sb1.Processor3 - Processor3 is processing
22:56:48.630 [pool-1-thread-2] INFO demo.sb1.Processor3 - Processor3 end
22:56:48.631 [pool-1-thread-2] INFO demo.sb1.Processor4 - Processor4 is processing
22:56:52.635 [pool-1-thread-2] INFO demo.sb1.Processor4 - Processor4 end
22:56:52.635 [pool-1-thread-2] INFO demo.sb1.ProcessorChain - 处理完成,结果是:demo.sb1.Task@58d4f75a

重新梳理执行流程:

  1. 最开始的启动器是 3fa77460,这是一个已经完成的 CompletableFuture 对象,用于启动异步操作链。

  2. 第一次循环:3fa77460 执行 thenComposeAsync 方法,返回新的 CompletableFuture 对象 e2d56bf。此时,Processor1process 方法正在异步执行。

  3. 第二次循环:e2d56bf 执行 thenComposeAsync 方法,返回新的 CompletableFuture 对象 244038d0。此时,Processor2process 方法正在等待 Processor1process 方法完成。

  4. 第三次循环:244038d0 执行 thenComposeAsync 方法,返回新的 CompletableFuture 对象 5680a178。此时,Processor3process 方法正在等待 Processor2process 方法完成。

  5. 第四次循环:5680a178 执行 thenComposeAsync 方法,返回新的 CompletableFuture 对象 5fdef03a。此时,Processor4process 方法正在等待 Processor3process 方法完成。

  6. 所有循环结束后,返回 5fdef03a。当所有的 Processorprocess 方法都完成后,5fdef03a 会执行 thenAccept 方法,处理最后一个 Processorprocess 方法返回的结果。

这个过程中,每个 CompletableFuture 对象都会等待前一个 CompletableFuture 对象完成,然后开始执行自己的异步操作。这就形成了一个 CompletableFuture 链,每个 CompletableFuture 对象都会按照 Processor 的顺序执行 process 方法。

CompletableFuture链

当调用thenComposeAsync方法时,会创建一个新的CompletableFuture对象:

    public <U> CompletableFuture<U> thenComposeAsync(
        Function<? super T, ? extends CompletionStage<U>> fn,
        Executor executor) {
        return uniComposeStage(screenExecutor(executor), fn);
    }
    private <V> CompletableFuture<V> uniComposeStage(
        Executor e, Function<? super T, ? extends CompletionStage<V>> f) {
        if (f == null) throw new NullPointerException();
        Object r; Throwable x;
        if (e == null && (r = result) != null) {
            // try to return function result directly
            if (r instanceof AltResult) {
                if ((x = ((AltResult)r).ex) != null) {
                    return new CompletableFuture<V>(encodeThrowable(x, r));
                }
                r = null;
            }
            try {
                @SuppressWarnings("unchecked") T t = (T) r;
                CompletableFuture<V> g = f.apply(t).toCompletableFuture();
                Object s = g.result;
                if (s != null)
                    return new CompletableFuture<V>(encodeRelay(s));
                CompletableFuture<V> d = new CompletableFuture<V>();
                UniRelay<V> copy = new UniRelay<V>(d, g);
                g.push(copy);
                copy.tryFire(SYNC);
                return d;
            } catch (Throwable ex) {
                return new CompletableFuture<V>(encodeThrowable(ex));
            }
        }
        CompletableFuture<V> d = new CompletableFuture<V>();
        //构建 UniCompose
        UniCompose<T,V> c = new UniCompose<T,V>(e, d, this, f);
        push(c);
        c.tryFire(SYNC);
        return d;
    }

构造的 UniCompose对象持有前一个CompletableFuture对象的引用,也就是 UniComposesrc 字段。所以即使循环中出现 CompletableFuture 被覆盖,也不用担心被 GC 的问题。

总结

本文以一个实际的场景设计问题为出发点,详细探讨了 CompletableFuture 的链式执行机制,对执行流程和线程切换进行了深入的分析,并对响应式编程的概念和应用进行了简单的讨论。

References

  • https://zh.wikipedia.org/zh-cn/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B

欢迎关注公众号:
在这里插入图片描述

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

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

相关文章

【Oracle】数据库查询与SQL语句

Oracle查询 一、单表查询 1、简单条件查询 1&#xff09;精确查询 SELECT* FROMT_OWNERS WHEREwatermeter 304082&#xff09;模糊查询 SELECT* FROMt_owners WHEREname LIKE %刘%3&#xff09;and运算符 SELECT* FROMt_owners WHEREname LIKE %刘% AND housenumb…

梦想贩卖机升级版知识付费源码,包含前后端源码,非线传,修复最新登录接口问题

梦想贩卖机升级版&#xff0c;变现宝吸收了资源变现类产品的许多优势&#xff0c;并剔除了那些无关紧要的元素&#xff0c;使得本产品在运营和变现能力方面实现了质的飞跃。多领域素材资源知识变现营销裂变独立版本。 支持&#xff1a;视频、音频、图文、文档、会员、社群、用…

【android】rk3588-android-bt

文章目录 蓝牙框架HCI接口蓝牙VENDORLIBvendorlib是什么 代码层面解读vendorlib1、 vendorlib实现&#xff0c;协议栈调用2、协议栈实现&#xff0c;vendorlib调用&#xff08;回调函数&#xff09;2.1、 init函数2.2、BT_VND_OP_POWER_CTRL对应处理2.3、BT_VND_OP_USERIAL_OPE…

基于 NFS 的文件共享实现

NFS&#xff08;Network File System&#xff09;即网络文件系统&#xff0c;它允许网络中的计算机之间通过 TCP/IP 网络共享文件资源&#xff0c;服务端通过 NFS 共享文件目录&#xff0c;客户端将该文件目录挂载在本地文件系统中&#xff0c;就可以像操作本地文件一样读写服务…

【AI视野·今日Robot 机器人论文速览 第七十二期】Mon, 8 Jan 2024

AI视野今日CS.Robotics 机器人学论文速览 Mon, 8 Jan 2024 Totally 13 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Deep Reinforcement Learning for Local Path Following of an Autonomous Formula SAE Vehicle Authors Harvey Merton, Thoma…

wav2lip中文语音驱动人脸训练

1 Wav2Lip介绍 1.1 Wav2Lip概述 2020年&#xff0c;来自印度海德拉巴大学和英国巴斯大学的团队&#xff0c;在ACM MM2020发表了的一篇论文《A Lip Sync Expert Is All You Need for Speech to Lip Generation In The Wild 》&#xff0c;在文章中&#xff0c;他们提出一个叫做…

ChatGPT可以帮你做什么?

学习 利用ChatGPT学习有很多&#xff0c;比如&#xff1a;语言学习、编程学习、论文学习拆解、推荐学习资源等&#xff0c;使用方法大同小异&#xff0c;这里以语言学习为例。 在开始前先给GPT充分的信息&#xff1a;&#xff08;举例&#xff09; 【角色】充当一名有丰富经验…

vue3、vue2文件导入事件

一、vue3写法 1、html部分 <el-buttontype"info"plainicon"Upload"click"handleImport"v-hasPermi"[system:user:import]">导入</el-button><!-- 导入对话框 --><el-dialog :title"upload.title" v-…

性能分析与调优: Linux 磁盘I/O 观测工具

目录 一、实验 1.环境 2.iostat 3.sar 4.pidstat 5.perf 6. biolatency 7. biosnoop 8.iotop、biotop 9.blktrace 10.bpftrace 11.smartctl 二、问题 1.如何查看PSI数据 2.iotop如何安装 3.smartctl如何使用 一、实验 1.环境 &#xff08;1&#xff09;主机 …

【漏洞复现】先锋WEB燃气收费系统文件上传漏洞 1day

漏洞描述 /AjaxService/Upload.aspx 存在任意文件上传漏洞 免责声明 技术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律,遵守公共秩序,尊重社会公德,不得利用网络从事危害国家安全、荣誉和利益,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作…

ubuntu20固定串口名称

查看串口的详细信息 udevadm info --name/dev/ttyUSB0结果&#xff1a; P: /devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0/tty/ttyUSB0 N: ttyUSB0 L: 0 S: serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UAR…

Arrow:在项目中进行时间处理的强大工具

目录 一、Arrow简介 二、安装与配置 三、基础功能与使用 1. 日期和时间格式转换 2. 时区处理 3. 时间序列分析 四、进阶应用与案例分析 五、性能与优化 六、最佳实践与经验分享 七、总结与展望 在处理日期和时间时&#xff0c;我们经常需要一个精确、可靠的库来帮助我…

FinClip SaaS 平台——小程序转APP操作指南及其实现

目录 前言 优势 操作指南 IDE生成APP 打开导出目录查看APP 使用AS打开导出的项目 Application初始化sdk MainActivity启动小程序并finish掉当前页面 &#xff0c;可查看前面文章进行操作&#xff0c;本文介绍FinClip SaaS 平台推出的新功能 生成APP 前言 通过这个「生…

pytorch11:模型加载与保存、finetune迁移训练

目录 一、模型加载与保存1.1 序列化与反序列化概念1.2 pytorch中的序列化与反序列化1.3 模型保存的两种方法1.4 模型加载两种方法 二、断点训练2.1 断点保存代码2.2 断点恢复代码 三、finetune3.1 迁移学习3.2 模型的迁移学习3.2 模型微调步骤3.2.1 模型微调步骤3.2.2 模型微调…

计算机毕业设计 基于SpringBoot的物资综合管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

mysql原理--redo日志2

1.redo日志文件 1.1.redo日志刷盘时机 我们前边说 mtr 运行过程中产生的一组 redo 日志在 mtr 结束时会被复制到 log buffer 中&#xff0c;可是这些日志总在内存里呆着也不是个办法&#xff0c;在一些情况下它们会被刷新到磁盘里&#xff0c;比如&#xff1a; (1). log buffer…

ROS2——常见的指令

在使用source ∼/bookros_ws/install/setup.bash后&#xff0c;可以让ROS2找到这个工作空间&#xff0c;进而可以调用相关的命令 概述 ros2 <command> <verb> [<params>|<option>]*这是ROS2与系统交互的方式 在终端输入ros2&#xff0c;即可查看相关…

YOLOv5改进 | 检测头篇 | ASFFHead自适应空间特征融合检测头(全网首发)

一、本文介绍 本文给大家带来的改进机制是利用ASFF改进YOLOv5的检测头形成新的检测头Detect_ASFF,其主要创新是引入了一种自适应的空间特征融合方式,有效地过滤掉冲突信息,从而增强了尺度不变性。经过我的实验验证,修改后的检测头在所有的检测目标上均有大幅度的涨点效果,…

065:vue中将一维对象数组转换为二维对象数组

第065个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

Sqoop入门指南:安装和配置

Sqoop是一个强大的工具&#xff0c;用于在Hadoop和关系型数据库之间高效传输数据。在本篇文章中&#xff0c;将深入探讨如何安装和配置Sqoop&#xff0c;以及提供详细的示例代码。 安装Java和Hadoop 在开始安装Sqoop之前&#xff0c;首先确保已经成功安装了Java和Hadoop。Sqo…
最新文章