并发编程 - Event Driven 设计模式(EDA)

news2024/9/22 15:52:11

文章目录

  • EDA 概述
  • 初体验
    • Event
    • Event Handlers
    • Event Loop
  • 如何设计一个Event-Driven框架
    • 同步EDA框架设计
      • Message
      • Channel
      • Dynamic Router
      • Event
      • EventDispatcher
      • 测试
      • 同步EDA架构类图
    • 异步EDA框架设计
      • 抽象基类 AsyncChannel
      • AsyncEventDispatcher 并发分发消息
      • 测试

在这里插入图片描述


EDA 概述

EDA(Event-Driven Architecture)是一种实现组件之间松耦合、易扩展的架构方式。

一个最简单的EDA设计需要包含如下几个组件:

  • Events:需要被处理的数据。

  • Event Handlers:处理Events的方式方法。

  • Event Loop:维护Events和Event Handlers之间的交互流程。

举个例子 :

在这里插入图片描述

Event A将被Handler A处理,而Event B将被Handler B处理,这一切的分配都是由Event Loop所控制的。

Events是EDA中的重要角色,一个Event至少需要包含两个属性:类型和数据

  • Event的类型决定了它会被哪个Handler处理
  • Data 是在Handler中代加工的材料

初体验

Event

package com.artisan.eventdriven.event;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 * @desc:Event只包含了该Event所属的类型和所包含的数据
 */


public class Event {
    private final String type;
    private final String data;

    public Event(String type, String data) {
        this.type = type;
        this.data = data;
    }

    public String getType() {
        return type;
    }

    public String getData() {
        return data;
    }
}
    

Event Handlers

Event Handlers主要用于处理Event,比如一些filtering或者transforming数据的操作等,下面我们写两个比较简单的方法


    /**
     * 用于处理A 类型的Event
     *
     * @param e
     */
    public static void handleEventA(Event e) {
        System.out.println(e.getData().toLowerCase());
    }

    /**
     * 用于处理B类型的Event
     *
     * @param e
     */
    public static void handleEventB(Event e) {
        System.out.println(e.getData().toUpperCase());
    }
  • handleEventA方法只是简单地将Event中的data进行了lowerCase之后的输出
  • handleEventB方法也是足够的简单,直接将Event中的字符串数据变成大写进行了控制台输出

Event Loop

Event Loop处理接收到的所有Event,并且将它们分配给合适的Handler去处理

 Event e;
 while (!events.isEmpty()) {
     //从消息队列中不断移除,根据不同的类型进行处理
     e = events.remove();
     switch (e.getType()) {
         case "A":
             handleEventA(e);
             break;
         case "B":
             handleEventB(e);
             break;
     }
 }

在EventLoop中,每一个Event都将从Queue中移除出去,通过类型匹配交给合适的Handler去处理。


package com.artisan.eventdriven.demo;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */

import com.artisan.eventdriven.event.Event;

import java.util.LinkedList;
import java.util.Queue;

public class FooEventDrivenExample {

    /**
     * 用于处理A 类型的Event
     *
     * @param e
     */
    public static void handleEventA(Event e) {
        System.out.println(e.getData().toLowerCase());
    }

    /**
     * 用于处理B类型的Event
     *
     * @param e
     */
    public static void handleEventB(Event e) {
        System.out.println(e.getData().toUpperCase());
    }

    public static void main(String[] args) {
        Queue<Event> events = new LinkedList<>();
        events.add(new Event("A", "Hello"));
        events.add(new Event("A", "I am Event A"));
        events.add(new Event("B", "I am Event B"));
        events.add(new Event("B", "World"));

        Event e;
        while (!events.isEmpty()) {
            //从消息队列中不断移除,根据不同的类型进行处理
            e = events.remove();
            switch (e.getType()) {
                case "A":
                    handleEventA(e);
                    break;
                case "B":
                    handleEventB(e);
                    break;
            }
        }
    }
}

在这里插入图片描述

虽然这个EDA的设计足够简单,但是通过它我们可以感受到EDA中三个重要组件之间的交互关系。


如何设计一个Event-Driven框架

一个基于事件驱动的架构设计,总体来讲会涉及如下几个重要组件:

  • 事件消息(Event)
  • 针对该事件的具体处理器(Handler)
  • 接受事件消息的通道(上个Demo中的queue)
  • 以及对事件消息如何进行分配(Event Loop)

同步EDA框架设计

我们先设计开发一个高度抽象的同步EDA框架,后续再考虑增加异步功能

Message

在基于Message的系统中,每一个Event也可以被称为Message,Message是对Event更高一个层级的抽象,每一个Message都有一个特定的Type用于与对应的Handler做关联

package com.artisan.eda.intf;

public interface Message {
    /**
     * 返回Message的类型
     */
    Class<? extends Message> getType();
}

Channel

第二个比较重要的概念就是Channels,Channel主要用于接受来自Event Loop分配的消息,每一个Channel负责处理一种类型的消息(当然这取决于我们对消息如何进行分配)

package com.artisan.eda.intf;

public interface Channel<E extends Message> {

    /**
     * dispatch方法用于负责Message的调度
     */
    void dispatch(E message);
}


Dynamic Router

Router的作用类似于上面的Event Loop,其主要是帮助Event找到合适的Channel并且传送给它

package com.artisan.eda.intf;

 
/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public interface DynamicRouter<E extends Message> {

    /**
     * 针对每一种Message类型注册相关的Channel,只有找到合适的Channel该Message才会被处理
     */
    void registerChannel(Class<? extends E> messageType,
                         Channel<? extends E> channel);

    /**
     * 为相应的Channel分配Message
     */
    void dispatch(E message);
}

Router如何知道要将Message分配给哪个Channel呢?换句话说,Router需要了解到Channel的存在,因此registerChannel()方法的作用就是将相应的Channel注册给Router,dispatch方法则是根据Message的类型进行路由匹配。


Event

Event是对Message的一个最简单的实现,在以后的使用中,将Event直接作为其他Message的基类即可(这种做法有点类似于适配器模式)

package com.artisan.eda.event;

import com.artisan.eda.intf.Message;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class Event implements Message {
    @Override
    public Class<? extends Message> getType() {
        return getClass();
    }
}

EventDispatcher

EventDispatcher是对DynamicRouter的一个最基本的实现,适合在单线程的情况下进行使用,因此不需要考虑线程安全的问题

package com.artisan.eda.router;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */

import com.artisan.eda.intf.Channel;
import com.artisan.eda.intf.DynamicRouter;
import com.artisan.eda.intf.Message;

import java.util.HashMap;
import java.util.Map;

/**
 * EventDispatcher不是一个线程安全的类
 * @author artisan
 */
public class EventDispatcher implements DynamicRouter<Message> {
    /**
     * 用于保存Channel和Message之间的关系
     */
    private final Map<Class<? extends Message>, Channel> routerTable;

    public EventDispatcher() {
        //初始化RouterTable,但是在该实现中,我们使用HashMap作为路由表
        this.routerTable = new HashMap<>();
    }

    @Override
    public void dispatch(Message message) {
        if (routerTable.containsKey(message.getType())) {
            //直接获取对应的Channel处理Message
            routerTable.get(message.getType()).dispatch(message);
        } else
            throw new MessageMatcherException("Can't match the channel for [" + message.getType() + "] type");
    }

    @Override
    public void registerChannel(Class<? extends Message> messageType,
                                Channel<? extends Message> channel) {
        this.routerTable.put(messageType, channel);
    }
}

在EventDispatcher中有一个注册表routerTable,主要用于存放不同类型Message对应的Channel,如果没有与Message相对应的Channel,则会抛出无法匹配的异常。

package com.artisan.eda.exceptions;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class MessageMatcherException extends RuntimeException {

    public MessageMatcherException(String message) {
        super(message);
    }
}
    

测试

package com.artisan.eventdriven.eda;

import com.artisan.eda.event.Event;
import com.artisan.eda.intf.Channel;
import com.artisan.eda.router.EventDispatcher;


/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class EventDispatcherExample
{
    /**
     * InputEvent中定义了两个属性X和Y,主要用于在其他Channel中的运算
     */
    static class InputEvent extends Event
    {
        private final int x;
        private final int y;

        public InputEvent(int x, int y)
        {
            this.x = x;
            this.y = y;
        }

        public int getX()
        {
            return x;
        }

        public int getY()
        {
            return y;
        }
    }
    /**
     * 用于存放结果的Event
     */
    static class ResultEvent extends Event
    {
        private final int result;

        public ResultEvent(int result)
        {
            this.result = result;
        }

        public int getResult()
        {
            return result;
        }
    }
    /**
     * 处理ResultEvent的Handler(Channel),只是简单地将计算结果输出到控制台
     */
    static class ResultEventHandler implements Channel<ResultEvent>
    {
        @Override
        public void dispatch(ResultEvent message)
        {
            System.out.println("The result is:" + message.getResult());
        }
    }

    /**
     * InputEventHandler需要向Router发送Event,因此在构造的时候需要传入Dispatcher
     */
    static class InputEventHandler implements Channel<InputEvent>
    {
        private final EventDispatcher dispatcher;

        public InputEventHandler(EventDispatcher dispatcher)
        {
            this.dispatcher = dispatcher;
        }

        /**
         *将计算的结果构造成新的Event提交给Router
         */
        @Override
        public void dispatch(InputEvent message)
        {
            System.out.printf("X:%d,Y:%d\n", message.getX(), message.getY());
            int result = message.getX() + message.getY();
            dispatcher.dispatch(new ResultEvent(result));
        }
    }

    public static void main(String[] args)
    {
        //构造Router
        EventDispatcher dispatcher = new EventDispatcher();
        //将Event和Handler(Channel)的绑定关系注册到Dispatcher
        dispatcher.registerChannel(InputEvent.class, new InputEventHandler(dispatcher));
        dispatcher.registerChannel(ResultEvent.class, new ResultEventHandler());
        dispatcher.dispatch(new InputEvent(1, 2));
    }
}

在这里插入图片描述

由于所有的类都存放于一个文件中,因此看起来测试代码比较多,其实结构还是非常清晰的,

  • InputEvent是一个Message,它包含了两个Int类型的属性,
  • 而InputEventHandler是对InputEvent消息的处理,接收到了InputEvent消息之后,分别对X和Y进行相加操作,然后将结果封装成ResultEvent提交给EventDispatcher,
  • ResultEvent相对比较简单,只包含了计算结果的属性,ResultEventHandler则将计算结果输出到控制台上。

通过上面这个例子的运行会发现,不同数据的处理过程之间根本无须知道彼此的存在,一切都由EventDispatcher这个Router来控制,它会给你想要的一切,这是一种稀疏耦合(松耦合)的设计

EDA的设计除了松耦合特性之外,扩展性也是非常强的,比如Channel非常容易扩展和替换,另外由于Dispatcher统一负责Event的调配,因此在消息通过Channel之前可以进行很多过滤、数据验证、权限控制、数据增强(Enhance)等工作。

同步EDA架构类图

在这里插入图片描述

异步EDA框架设计

上面的同步EDA框架,在应对高并发的情况下还是存在一些问题的,具体如下。

  • EventDispatcher不是线程安全的类,在多线程的情况下,registerChannel方法会引起数据不一致的问题。

  • 就目前而言,我们实现的所有Channel都无法并发消费Message,比如InputEventHandler只能逐个处理Message,低延迟的消息处理还会导致Dispatcher出现积压。

抽象基类 AsyncChannel

我们继续对EDA框架进行扩充,使其可支持并发任务的执行,下面定义了一个新的AsyncChannel作为基类,该类中提供了Message的并发处理能力。

 package com.artisan.eda.async;

import com.artisan.eda.event.Event;
import com.artisan.eda.intf.Channel;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public abstract class AsyncChannel implements Channel<Event> {
    /**
     * 在AsyncChannel 中将使用ExecutorService多线程的方式提交给Message
     */
    private final ExecutorService executorService;

    /**
     * 默认构造函数,提供了CPU的核数×2的线程数量
     */
    public AsyncChannel() {
        this(Executors.newFixedThreadPool(Runtime.getRuntime()
                .availableProcessors() * 2));
    }

    /**
     * 用户自定义的ExecutorService
     *
     * @param executorService
     */
    public AsyncChannel(ExecutorService executorService) {
        this.executorService = executorService;
    }

    /**
     * 重写dispatch方法,并且用final修饰,避免子类重写
     *
     * @param message
     */
    @Override
    public final void dispatch(Event message) {
        executorService.submit(() -> this.handle(message));
    }

    /**
     * 提供抽象方法,供子类实现具体的Message处理
     *
     * @param message
     */
    protected abstract void handle(Event message);

    /**
     * 提供关闭ExecutorService的方法
     */
    public void stop() {
        if (null != executorService && !executorService.isShutdown())
            executorService.shutdown();
    }
}

    
  • 为了防止子类在继承AsyncChannel基类的时候重写dispatch方法,用final关键字对其进行修饰(Template Method Design Pattern),
  • handle方法用于子类对Message进行具体的处理,
  • stop方法则用来停止ExecutorService。

AsyncEventDispatcher 并发分发消息

其次,还需要提供新的EventDispatcher类AsyncEventDispatcher负责以并发的方式dispatch Message,其中Event对应的Channel只能是AsyncChannel类型,并且也对外暴露了shutdown方法

package com.artisan.eda.router;

import com.artisan.eda.async.AsyncChannel;
import com.artisan.eda.event.Event;
import com.artisan.eda.exceptions.MessageMatcherException;
import com.artisan.eda.intf.Channel;
import com.artisan.eda.intf.DynamicRouter;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class AsyncEventDispatcher implements DynamicRouter<Event> {

    /**
     * 使用线程安全的ConcurrentHashMap替换HashMap
     */
    private final Map<Class<? extends Event>, AsyncChannel> routerTable;

    public AsyncEventDispatcher() {
        this.routerTable = new ConcurrentHashMap<>();
    }

    @Override
    public void registerChannel(Class<? extends Event> messageType,
                                Channel<? extends Event> channel) {
        //在AsyncEventDispatcher中,Channel必须是AsyncChannel类型
        if (!(channel instanceof AsyncChannel)) {
            throw new IllegalArgumentException("The channel must be AsyncChannel Type.");
        }
        this.routerTable.put(messageType, (AsyncChannel) channel);
    }

    @Override
    public void dispatch(Event message) {
        if (routerTable.containsKey(message.getType())) {
            routerTable.get(message.getType()).dispatch(message);
        } else {
            throw new MessageMatcherException("Can't match the channel for ["
                    + message.getType() + "] type");
        }

    }

    public void shutdown() {
        //关闭所有的Channel以释放资源
        routerTable.values().forEach(AsyncChannel::stop);
    }

}
    

在AsyncEventDispatcher中,routerTable使用线程安全的Map定义,在注册Channel的时候,如果其不是AsyncChannel的类型,则会抛出异常。


测试

package com.artisan.eventdriven.eda;

import com.artisan.eda.async.AsyncChannel;
import com.artisan.eda.event.Event;
import com.artisan.eda.router.AsyncEventDispatcher;

import java.util.concurrent.TimeUnit;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class AsyncEventDispatcherExample {

    //主要用于处理InputEvent,但是需要继承AsyncChannel
    static class AsyncInputEventHandler extends AsyncChannel {
        private final AsyncEventDispatcher dispatcher;

        AsyncInputEventHandler(AsyncEventDispatcher dispatcher) {
            this.dispatcher = dispatcher;
        }

        //不同于以同步的方式实现dispatch,异步的方式需要实现handle
        @Override
        protected void handle(Event message) {
            EventDispatcherExample.InputEvent inputEvent =
                    (EventDispatcherExample.InputEvent) message;
            System.out.printf("X:%d,Y:%d\n", inputEvent.getX(), inputEvent.getY());
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int result = inputEvent.getX() + inputEvent.getY();
            dispatcher.dispatch(new EventDispatcherExample.ResultEvent(result));
        }
    }

    //主要用于处理InputEvent,但是需要继承AsyncChannel
    static class AsyncResultEventHandler extends AsyncChannel {
        @Override
        protected void handle(Event message) {
            EventDispatcherExample.ResultEvent resultEvent =
                    (EventDispatcherExample.ResultEvent) message;
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("The result is:" + resultEvent.getResult());
        }
    }

    public static void main(String[] args) {
        //定义AsyncEventDispatcher
        AsyncEventDispatcher dispatcher = new AsyncEventDispatcher();

        //注册Event和Channel之间的关系
        dispatcher.registerChannel(EventDispatcherExample.InputEvent.class, new AsyncInputEventHandler(dispatcher));
        dispatcher.registerChannel(EventDispatcherExample.ResultEvent.class, new AsyncResultEventHandler());
        //提交需要处理的Message
        dispatcher.dispatch(new EventDispatcherExample.InputEvent(1, 2));
    }
}
    

当dispatcher分配一个Event的时候,如果执行非常缓慢也不会影响下一个Event被dispatch,这主要得益于我们采用了异步的处理方式(ExecutorService本身存在的任务队列可以允许异步提交一定数量级的数据)

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【计算机网络】第 2 课 - 计算机网络的性能指标

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 目录 1、缘起 2、性能指标 2.1、速率 2.2、带宽 2.3、吞吐量 2.4、时延 2.5、时延带宽积 2.6、往返时间 2.7、利用率 2.8、丢…

【Cartopy学习系列】Cartopy中的投影类型总结

一、PlateCarree&#xff08;圆柱投影&#xff09; PlateCarree 是Cartopy的默认投影&#xff0c;投影将地物投影到圆柱面上再展开&#xff0c;常用来绘制世界地图。该投影具有经线或纬线方向等度数的特点&#xff0c;亦称等经纬度投影。 class cartopy.crs.PlateCarree(cent…

【Kafka】Kafka消费者

【Kafka】Kafka消费者 文章目录 【Kafka】Kafka消费者1. 消费方式1.1 消费者工作流程1.2 消费者组原理1.3 消费者组初始化流程1.4 消费者组详细消费流程1.5 消费者重要参数 2. 消费者API2.1 独立消费者案例2.2 订阅分区2.3 消费者组案例 1. 消费方式 pull(拉)模式&#xff1a;…

Linux上查看外接USB设备类型

最近遇到一个问题&#xff0c;需要在shell脚本中识别当前显示器的USB触屏线是否插入&#xff0c;并读取显示器名称&#xff0c;以确定是否是想要的。 解决思路&#xff1a; lsusb命令可以列出所有的外接USB设备&#xff1a; 其中 “Atmel Corp. Atmel maXTouch Digitizer” 即为…

rabbitmq使用springboot实现direct模式

一、 Direct模式 类型&#xff1a;direct特点&#xff1a;Direct模式是fanout模式上的一种叠加&#xff0c;增加了路由RoutingKey的模式。 二、coding Ⅰ 生产者 1、引入相应的pom文件 pom.xml <?xml version"1.0" encoding"UTF-8"?> <pro…

Linux 学习记录48(QT篇待完成)

Linux 学习记录48(QT篇) 本文目录 Linux 学习记录48(QT篇)一、1.2. 二、三、四、练习1. 自制文本编辑器(0. main.cpp(1. txt_window.h(2. txt_window.cpp 2. 登录界面完善 一、 1. 2. 二、 三、 四、 练习 1. 自制文本编辑器 (0. main.cpp #include "txt_window.h…

JavaWeb 笔记——5

JavaWeb 笔记——5 一、Filter1.1、概述1.2、Filter快速入门1.3、Filter执行流程1.4、Filter使用细节1.5、Filter-案例-登陆验证 二、Listener2.1、Listener概述与分类2.2、ServletContextListener使用 三、AJAX3.1、AJAX概述3.2、AJAX快速入门3.3、使用Ajax验证用户名是否存在…

《阿里大数据之路》研读笔记(3)事实表

不理解可以先看看这个例子 例子里的start_time可以看成下单时间 end看成确认收货时间 这个例子中累计快照事实表和拉链表类似 图解HIVE累积型快照事实表_累积快照事实表_小基基o_O的博客-CSDN博客 累计快照事实表 我的理解是 根据上面的例子 就是一行代表多个业务过程 每个…

day18 哈希表

题目一&#xff1a;两个数组的交集 题目描述 int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize){//哈希表 int arr_hash[1000] {0};int *arr_result (int *)malloc(sizeof(int)* nums1Size);*returnSize 0;for(int i 0;i < nu…

强化学习|底层逻辑与本质 引导式学习

强化学习的本质是什么&#xff0c;底层逻辑是什么&#xff1f; 强化学习的本质是一个智能体通过与环境的交互&#xff0c;通过尝试和错误的方式学习如何采取行动来最大化累积奖励。它的底层逻辑基于马尔可夫决策过程&#xff08;Markov Decision Process&#xff0c;MDP&#x…

【C++进阶之路】模拟实现string类

前言 本文所属专栏——【C进阶之路】 上一篇,我们讲解了string类接口的基本使用&#xff0c;今天我们就实战从底层实现自己的string类&#xff0c;当然实现所有的接口难度很大&#xff0c;我们今天主要实现的常用的接口~ 一、String类 ①要点说明 1.为了不与库里面的string冲…

※Redis的事务、乐观锁和悲观锁

1.是神魔 在高并发的环境下&#xff0c;多个线程去竞争同一个资源&#xff0c; 比较常见的有高铁抢票系统&#xff0c;商品秒杀系统等&#xff0c;我们需要保证数据正确&#xff0c;同时系统的吞吐也要尽可能高。2.解决方案 1. 一般多线程同步我们就会想到加锁&#xff0c;用…

c语言进阶-文件的打开和读写

本节重点知识点 为什么使用文件 什么是文件 文件名的组成 操作文件的基本过程 文件的打开与关闭 文件打开函数&#xff1a; 参数介绍 打开文件的方式&#xff1a; 使用绝对路径和相对路径都可以打开文件 文件的顺序读写函数&#xff1a; 写文件模式下&#xff0c;在打开文件fo…

-XX:NewSize=20m -XX:MaxNewSize=40m,-Xmn30m,-XX:NewRatio=5

高优先级&#xff1a;-XX:NewSize -XX:MaxNewSize设置新生代初始值&#xff0c;最大值中优先级&#xff1a;-Xmn&#xff08;默认等效 -Xmn-XX:NewSize-XX:MaxNewSize?&#xff09;设置新生代初始值-XX:NewRatio设置老年代和新生代的比例&#xff1b;例如&#xff1a;-XX:NewR…

CMake+OpenMP加速运算测试

目录 写在前面代码编译运行关于加速效果参考完 写在前面 1、本文内容 cmake编译测试openmp的效果 2、平台/环境 windows/linux均可&#xff0c;cmake 3、转载请注明出处&#xff1a; https://blog.csdn.net/qq_41102371/article/details/131629705 代码 代码包含同样的for循…

Dockerfile自定义镜像 - 基于 java:8-alpine 镜像,将一个Java项目构建为镜像

目录 一、前置知识 1.镜像结构 2.Dockerfile是什么 二、自定义一个 java 项目镜像 1.创建一个空目录&#xff0c;在这个空目录中创建一个文件&#xff0c;命名为 DockerFile&#xff0c;最后将 java 项目打包成 jar 包&#xff0c;放到这个目录中 2.编写 Dockerfile 文件 …

Vue3+Vite项目引入Element-plus并配置按需自动导入

一、安装Element-plus # 选择一个你喜欢的包管理器# NPM $ npm install element-plus --save# Yarn $ yarn add element-plus# pnpm $ pnpm install element-plus我使用的是 pnpm&#xff0c;并且顺便将 element-plus/icons一起引入 pnpm install element-plus element-plus/…

Python开启Http Server

用 Python 部署了一个具有 FTP 功能的服务器&#xff0c;电脑在局域网内通过 FTP 下载想要传输的文件。 注&#xff1a;这种方法不仅在自己家的路由器上可行&#xff0c;亲测在下面两种场景也可行&#xff1a; 需要用手机验证码连接的公共 WIFI 上&#xff1b;用手机开热点&a…

Kubernetes的Pod中进行容器初始化

Kubernetes的Pod中进行容器初始化 在很多应用场景中&#xff0c;应用在启动之前都需要进行如下初始化操作&#xff1a; 等待其他关联组件正确运行(例如数据库或某个后台服务)。 基于环境变量或配置模板生成配置文件。 从远程数据库获取本地所需配置&#xff0c;或者将自身注…

将一个3x3的OpenCV旋转矩阵转换为Eigen的Euler角

代码将一个3x3的OpenCV旋转矩阵转换为Eigen的Euler角。 #include <iostream> #include <Eigen/Core> #include <Eigen/Geometry> #include <opencv2/core.hpp>using