从原理到实战,手把手教你在项目中使用RabbitMQ

news2024/11/23 16:18:24

大家好呀,我是楼仔。

RabbitMQ 的文章之前写过,但是当时给的示例是 Demo 版的,这篇文章主要是结合之前写的理论知识,将 RabbitMQ 集成到技术派项目中。

不 BB,上文章目录:

图片

下面我们先回顾一下理论知识,如果对这块知识已经清楚的同学,可以直接跳到实战部分。

1. 消息队列

1.1 消息队列模式

消息队列目前主要 2 种模式,分别为“点对点模式”和“发布/订阅模式”。

点对点模式

一个具体的消息只能由一个消费者消费,多个生产者可以向同一个消息队列发送消息,但是一个消息在被一个消息者处理的时候,这个消息在队列上会被锁住或者被移除并且其他消费者无法处理该消息。

需要额外注意的是,如果消费者处理一个消息失败了,消息系统一般会把这个消息放回队列,这样其他消费者可以继续处理。

图片

 如果你想学习自动化测试,我这边给你推荐一套视频,这个视频可以说是B站播放全网第一的自动化测试教程,同时在线人数到达1000人,并且还有笔记可以领取及各路大神技术交流:798478386   

【已更新】B站讲的最详细的Python接口自动化测试实战教程全集(实战最新版)_哔哩哔哩_bilibili【已更新】B站讲的最详细的Python接口自动化测试实战教程全集(实战最新版)共计200条视频,包括:1、接口自动化之为什么要做接口自动化、2、接口自动化之request全局观、3、接口自动化之接口实战等,UP主更多精彩视频,请关注UP账号。https://www.bilibili.com/video/BV17p4y1B77x/?spm_id_from=333.337.search-card.all.click 

发布/订阅模式

单个消息可以被多个订阅者并发的获取和处理。一般来说,订阅有两种类型:

  • 临时(ephemeral)订阅:这种订阅只有在消费者启动并且运行的时候才存在。一旦消费者退出,相应的订阅以及尚未处理的消息就会丢失。

  • 持久(durable)订阅:这种订阅会一直存在,除非主动去删除。消费者退出后,消息系统会继续维护该订阅,并且后续消息可以被继续处理。

图片

1.2 RabbitMQ 特征

  • 消息路由(支持):RabbitMQ可以通过不同的交换器支持不同种类的消息路由;

  • 消息有序(不支持):当消费消息时,如果消费失败,消息会被放回队列,然后重新消费,这样会导致消息无序;

  • 消息时序(非常好):通过延时队列,可以指定消息的延时时间,过期时间TTL等;

  • 容错处理(非常好):通过交付重试和死信交换器(DLX)来处理消息处理故障;

  • 伸缩(一般):伸缩其实没有非常智能,因为即使伸缩了,master queue还是只有一个,负载还是只有这一个master queue去抗,所以我理解RabbitMQ的伸缩很弱(个人理解)。

  • 持久化(不太好):没有消费的消息,可以支持持久化,这个是为了保证机器宕机时消息可以恢复,但是消费过的消息,就会被马上删除,因为RabbitMQ设计时,就不是为了去存储历史数据的。

  • 消息回溯(支持):因为消息不支持永久保存,所以自然就不支持回溯。

  • 高吞吐(中等):因为所有的请求的执行,最后都是在master queue,它的这个设计,导致单机性能达不到十万级的标准。

2. RabbitMQ 原理初探

RabbitMQ 2007 年发布,是使用 Erlang 语言开发的开源消息队列系统,基于 AMQP 协议来实现。

2.1 基本概念

提到RabbitMQ,就不得不提AMQP协议。AMQP协议是具有现代特征的二进制协议。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。

先了解一下AMQP协议中间的几个重要概念:

  • Server:接收客户端的连接,实现AMQP实体服务。

  • Connection:连接,应用程序与Server的网络连接,TCP连接。

  • Channel:信道,消息读写等操作在信道中进行。客户端可以建立多个信道,每个信道代表一个会话任务。

  • Message:消息,应用程序和服务器之间传送的数据,消息可以非常简单,也可以很复杂。由Properties和Body组成。Properties为外包装,可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body就是消息体内容。

  • Virtual Host:虚拟主机,用于逻辑隔离。一个虚拟主机里面可以有若干个Exchange和Queue,同一个虚拟主机里面不能有相同名称的Exchange或Queue。

  • Exchange:交换器,接收消息,按照路由规则将消息路由到一个或者多个队列。如果路由不到,或者返回给生产者,或者直接丢弃。RabbitMQ常用的交换器常用类型有direct、topic、fanout、headers四种,后面详细介绍。

  • Binding:绑定,交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个RoutingKey。

  • RoutingKey:路由键,生产者将消息发送给交换器的时候,会发送一个RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。路由键通常为一个“.”分割的字符串,例如“com.rabbitmq”。

  • Queue:消息队列,用来保存消息,供消费者消费。

2.2 工作原理

AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下:

  1. 生产者是连接到 Server,建立一个连接,开启一个信道。

  2. 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。

  3. 消费者也需要进行建立连接,开启信道等操作,便于接收消息。

  4. 生产者发送消息,发送到服务端中的虚拟主机。

  5. 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。

  6. 订阅了消息队列的消费者就可以获取到消息,进行消费。

图片

2.3 常用交换器

RabbitMQ常用的交换器类型有direct、topic、fanout、headers四种:

  • Direct Exchange:见文知意,直连交换机意思是此交换机需要绑定一个队列,要求该消息与一个特定的路由键完全匹配。简单点说就是一对一的,点对点的发送。

图片

  • Fanout Exchange:这种类型的交换机需要将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。简单点说就是发布订阅。

图片

  • Topic Exchange:直接翻译的话叫做主题交换机,如果从用法上面翻译可能叫通配符交换机会更加贴切。这种交换机是使用通配符去匹配,路由到对应的队列。通配符有两种:"*" 、 "#"。需要注意的是通配符前面必须要加上"."符号。

    • *符号:有且只匹配一个词。比如 a.*可以匹配到"a.b"、"a.c",但是匹配不了"a.b.c"。

    • #符号:匹配一个或多个词。比如"rabbit.#"既可以匹配到"rabbit.a.b"、"rabbit.a",也可以匹配到"rabbit.a.b.c"。

图片

  • Headers Exchange:这种交换机用的相对没这么多。它跟上面三种有点区别,它的路由不是用routingKey进行路由匹配,而是在匹配请求头中所带的键值进行路由。创建队列需要设置绑定的头部信息,有两种模式:全部匹配和部分匹配。如上图所示,交换机会根据生产者发送过来的头部信息携带的键值去匹配队列绑定的键值,路由到对应的队列。

图片

3. RabbitMQ环境搭建

因为我用的是Mac,所以直接可以参考官网:

https://www.rabbitmq.com/install-homebrew.html

需要注意的是,一定需要先执行:

brew update

然后再执行:

brew install rabbitmq

之前没有执行brew update,直接执行brew install rabbitmq时,会报各种各样奇怪的错误,其中“403 Forbidde”居多。

但是在执行“brew install rabbitmq”,会自动安装其它的程序,如果你使用源码安装Rabbitmq,因为启动该服务依赖erlang环境,所以你还需手动安装erlang,但是目前官方已经一键给你搞定,会自动安装Rabbitmq依赖的所有程序,是不是很棒!

图片

最后执行成功的输出如下:

图片

启动服务:

# 启动方式1:后台启动
brew services start rabbitmq
# 启动方式2:当前窗口启动
cd /usr/local/Cellar/rabbitmq/3.8.19
rabbitmq-server

在浏览器输入:

http://localhost:15672/

会出现RabbitMQ后台管理界面(用户名和密码都为guest):

图片

通过brew安装,一行命令搞定,真香!

4. RabbitMQ 集成

4.1 前置工作

添加账号:

## 添加账号
./rabbitmqctl add_user admin admin
## 添加访问权限
./rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
## 设置超级权限
./rabbitmqctl set_user_tags admin administrator

pom 引入依赖:

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.5.1</version>
</dependency>

 

4.2 代码实现

核心代码

先整一个 ConnectionFactory 单例,每台机器都有自己的 ConnectionFactory,防止每次都初始化(在后面的迭代中,我会把这个去掉,整成连接池)。

/**
 * @author Louzai
 * @date 2023/5/10
 */
public class RabbitmqUtil {

    /**
     * 每个key都有自己的工厂
     */
    private static Map<String, ConnectionFactory> executors = new ConcurrentHashMap<>();

    /**
     * 初始化一个工厂
     *
     * @param host
     * @param port
     * @param username
     * @param passport
     * @param virtualhost
     * @return
     */
    public static ConnectionFactory init(String host,
                                  Integer port,
                                  String username,
                                  String passport,
                                  String virtualhost) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(host);
        factory.setPort(port);
        factory.setUsername(username);
        factory.setPassword(passport);
        factory.setVirtualHost(virtualhost);
        return factory;
    }

    /**
     * 工厂单例,每个key都有属于自己的工厂
     *
     * @param key
     * @param host
     * @param port
     * @param username
     * @param passport
     * @param virtualhost
     * @return
     */
    public static ConnectionFactory getOrInitConnectionFactory(String key,
                                                               String host,
                                                               Integer port,
                                                               String username,
                                                               String passport,
                                                               String virtualhost) {
        ConnectionFactory connectionFactory = executors.get(key);
        if (null == connectionFactory) {
            synchronized (RabbitmqUtil.class) {
                connectionFactory = executors.get(key);
                if (null == connectionFactory) {
                    connectionFactory = init(host, port, username, passport, virtualhost);
                    executors.put(key, connectionFactory);
                }
            }
        }
        return connectionFactory;
    }
}

获取 RabbitmqClient:

/**
 * @author Louzai
 * @date 2023/5/10
 */
@Component
public class RabbitmqClient {

    @Autowired
    private RabbitmqProperties rabbitmqProperties;

    /**
     * 创建一个工厂
     * @param key
     * @return
     */
    public ConnectionFactory getConnectionFactory(String key) {
        String host = rabbitmqProperties.getHost();
        Integer port = rabbitmqProperties.getPort();
        String userName = rabbitmqProperties.getUsername();
        String password = rabbitmqProperties.getPassport();
        String virtualhost = rabbitmqProperties.getVirtualhost();
        return RabbitmqUtil.getOrInitConnectionFactory(key, host, port, userName,password, virtualhost);
    }
}

重点!敲黑板!!!这里就是 RabbmitMQ 的核心逻辑了。

我们使用的交换机类型是 Direct Exchange,此交换机需要绑定一个队列,要求该消息与一个特定的路由键完全匹配,简单点说就是一对一的,点对点的发送。

至于为什么不用广播和主题交换机模式,因为技术派的使用场景就是发送单个消息,点到点发送和消费的模式完全可以满足我们的需求。

下面 3 个方法都很简单:

  • 发送消息:拿到工厂 -> 创建链接 -> 创建通道 -> 声明交换机 -> 发送消息 -> 关闭链接;

  • 消费消息:拿到工厂 -> 创建链接 -> 创建通道 -> 确定消息队列 -> 绑定队列到交换机 -> 接受并消费消息;

  • 消费消息永动模式:非阻塞模式消费 RabbitMQ 消息。

@Component
public class RabbitmqServiceImpl implements RabbitmqService {

    @Autowired
    private RabbitmqClient rabbitmqClient;

    @Autowired
    private NotifyService notifyService;

    @Override
    public void publishMsg(String exchange,
                           BuiltinExchangeType exchangeType,
                           String toutingKey,
                           String message) throws IOException, TimeoutException {
        ConnectionFactory factory = rabbitmqClient.getConnectionFactory(toutingKey);

        // TODO: 这种并发量起不来,需要改造成连接池

        //创建连接
        Connection connection = factory.newConnection();
        //创建消息通道
        Channel channel = connection.createChannel();

        // 声明exchange中的消息为可持久化,不自动删除
        channel.exchangeDeclare(exchange, exchangeType, true, false, null);

        // 发布消息
        channel.basicPublish(exchange, toutingKey, null, message.getBytes());

        System.out.println("Publish msg:" + message);
        channel.close();
        connection.close();
    }

    @Override
    public void consumerMsg(String exchange,
                            String queue,
                            String routingKey) throws IOException, TimeoutException {
        ConnectionFactory factory = rabbitmqClient.getConnectionFactory(routingKey);

        // TODO: 这种并发量起不来,需要改造成连接池

        //创建连接
        Connection connection = factory.newConnection();
        //创建消息信道
        final Channel channel = connection.createChannel();
        //消息队列
        channel.queueDeclare(queue, true, false, false, null);
        //绑定队列到交换机
        channel.queueBind(queue, exchange, routingKey);

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Consumer msg:" + message);

                // 获取Rabbitmq消息,并保存到DB
                // 说明:这里仅作为示例,如果有多种类型的消息,可以根据消息判定,简单的用 if...else 处理,复杂的用工厂 + 策略模式
                notifyService.saveArticleNotify(JsonUtil.toObj(message, UserFootDO.class), NotifyTypeEnum.PRAISE);

                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 取消自动ack
        channel.basicConsume(queue, false, consumer);
    }

    @Override
    public void processConsumerMsg() {
        System.out.println("Begin to processConsumerMsg.");

        Integer stepTotal = 1;
        Integer step = 0;

        // TODO: 这种方式非常 Low,后续会改造成阻塞 I/O 模式
        while (true) {
            step ++;
            try {
                System.out.println("processConsumerMsg cycle.");
                consumerMsg(CommonConstants.EXCHANGE_NAME_DIRECT, CommonConstants.QUERE_NAME_PRAISE,
                        CommonConstants.QUERE_KEY_PRAISE);
                if (step.equals(stepTotal)) {
                    Thread.sleep(10000);
                    step = 0;
                }
            } catch (Exception e) {

            }
        }
    }
}

这里只是给个示例,如果要真正用到生产环境,你觉得有哪些问题呢? 你自己先想想,文末再告诉你。

调用入口

其实之前我们是通过 Java 的内置异步调用方式,为了方便验证,我把文章点赞的功能迁移到 RabbitMQ 中,只要是点赞,就走 RabbitMQ 模式。

// 点赞消息走 RabbitMQ,其它走 Java 内置消息机制
if (notifyType.equals(NotifyTypeEnum.PRAISE) && rabbitmqProperties.getSwitchFlag()) {
    rabbitmqService.publishMsg(
            CommonConstants.EXCHANGE_NAME_DIRECT,
            BuiltinExchangeType.DIRECT,
            CommonConstants.QUERE_KEY_PRAISE,
            JsonUtil.toStr(foot));
} else {
    Optional.ofNullable(notifyType).ifPresent(notify -> SpringUtil.publishEvent(new NotifyMsgEvent<>(this, notify, foot)));
}

那消费入口放哪里呢?其实是在程序启动的时候,我们就启动 RabbitMQ 进行消费,然后整个进程一直在程序中跑。

@Override
public void run(ApplicationArguments args) {
    // 设置类型转换, 主要用于mybatis读取varchar/json类型数据据,并写入到json格式的实体Entity中
    JacksonTypeHandler.setObjectMapper(new ObjectMapper());
    // 应用启动之后执行
    GlobalViewConfig config = SpringUtil.getBean(GlobalViewConfig.class);
    if (webPort != null) {
        config.setHost("http://127.0.0.1:" + webPort);
    }
    // 启动 RabbitMQ 进行消费
    if (rabbitmqProperties.getSwitchFlag()) {
        taskExecutor.execute(() -> rabbitmqService.processConsumerMsg());
    }
    log.info("启动成功,点击进入首页: {}", config.getHost());
}

4.3 演示一下

我们多次点击“点赞”按钮,触发 RammitMQ 消息发送。

图片

可以通过日志,也可以看到发送和消费过的消息。

图片

我靠!好多没有关闭的链接。。。

图片

还有一堆没有关闭的 channel。。。

图片

估计再多跑一会,内存全部吃光,机器就死机了,怎么破?答案是连接池!

4.4 代码分支

为了方便大家学习功能演变的过程,每个模块都会单独开个分支,包括后面的升级版:

  • 代码仓库:https://github.com/itwanger/paicoding

  • 代码分支:feature/add_rabbitmq_20230506

如果需要运行 RabbitMQ,下面的配置需要改成 true,因为代码默认是 false。

图片

5 后记

这篇文章,让大家知道 RabbitMQ 的基本原理,以及如何去集成 RabbitMQ,但是还不能用到实际生产环境,但是这个确实是我写的第一个版本,存粹是搞着玩的,因为里面存在的问题还非常多。

我简单列举一下:

  1. 需要给 Connection 加个连接池,否则内存会持续消耗,机器肯定扛不住;

  2. 需要对 RabbitMQ 的消费方式进行改造,因为 while + sleep 的方式过于简单粗暴;

  3. 假如消费的任务挂掉了,你需要有重启 RabbitMQ 的消费机制;

  4. 假如机器挂了,重启后,RabbitMQ 内部的消息不能丢失。

如果你对上面的问题也非常感兴趣,可以直接基于分支 feature/add_rabbitmq_20230506,然后给我提 PR,技术嘛,我喜欢边玩边学。

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

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

相关文章

【Hadoop】DataNode 详解

&#x1f341; 博主 "开着拖拉机回家"带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——&#x1f390;开着拖拉机回家_Linux,Java基础学习,大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341; 希望本文能够给您带来一定的…

Python爬虫框架之快速抓取互联网数据详解

概要 Python爬虫框架是一个能够帮助我们快速抓取互联网数据的工具。在互联网时代&#xff0c;信息爆炸式增长&#xff0c;人们越来越需要一种快速获取信息的方式。而Python爬虫框架就能够帮助我们完成这个任务&#xff0c;它可以帮助我们快速地从互联网上抓取各种数据&#xf…

promethues监控postgres,emqx

1、安装监控 docker pull wrouesnel/postgres_exporter2、执行 docker run -d -p 9187:9187 --name postgres_exporter --nethost -d -e DATA_SOURCE_NAME"postgresql://postgres:123456192.168.12.116:5432/rcc-manage?sslmodedisable" wrouesnel/postgres_expor…

Java读(配置)文件 根目录下、s r c 和resources目录下的区别

1. 通过File类、InputStream读文件 在普通java项目中&#xff0c;当使用java io&#xff08;Fie类、FileInputStream类等&#xff09;读文件&#xff0c;传入相对路径时&#xff1a; 这种方式读文件&#xff0c;文件路径必须是项目的根路径&#xff0c;将文件放在其他任何目录…

皕杰报表(BIOS Report)中设置序号的方法之二

在皕杰报表如何设置序号系列之一里&#xff0c;我们用ds.#0来实现了序号&#xff0c;用ds.#0得到的数据库中选取的记录的序号。有些情况下&#xff0c;记录序号在报表中不是按照顺序显示的&#xff0c;而是在报表中又通过排序或分组后的结果显示的&#xff0c;例如&#xff1a;…

陪诊小程序|陪诊软件开发功能|陪诊平台优势

随着人们生活水平的提高&#xff0c;对健康的关注度也在不断增加。尤其是在疫情过后&#xff0c;人们对自己和家人的健康问题更加重视。因此陪诊系统应运而生&#xff0c;为用户提供便捷、高效的陪诊陪护和跑腿服务。那么陪诊系统包含哪些功能呢&#xff1f; 首先&#xff0c;陪…

2023年7月京东护发市场数据分析(京东数据产品)

如今&#xff0c;与面部护肤相比&#xff0c;多数消费者认为头皮也需要认真对待&#xff0c;这在年轻消费群体中体现的较为明显。 随着消费者对护发理念的认同感不断加深&#xff0c;人们日常居家洗护的步骤也更加精细、使用产品品类也愈加多样化。除传统的护发素、发膜等护发…

Dynamic ReLU:根据输入动态确定的ReLU

这是我最近才看到的一篇论文&#xff0c;它提出了动态ReLU (Dynamic ReLU, DY-ReLU)&#xff0c;可以将全局上下文编码为超函数&#xff0c;并相应地调整分段线性激活函数。与传统的ReLU相比&#xff0c;DY-ReLU的额外计算成本可以忽略不计&#xff0c;但表示能力明显增强&…

【数据结构】 队列(Queue)与队列的模拟实现

文章目录 &#x1f340;队列(Queue)的概念&#x1f38b;队列的使用&#x1f38d;队列的模拟实现&#x1f6a9;创建队列&#x1f6a9;入队列&#x1f6a9;出队列&#x1f6a9;获取队头元素&#x1f6a9;获取队列长度&#x1f6a9;判断是否为空&#x1f6a9;完整代码 &#x1f33…

2023-08-28 小练习_环形字符串的某子串出现次数

老林的C语言新课, 想快速入门点此 <C 语言编程核心突破> 小练习_环形字符串的某子串出现次数 前言一、具体算法描述输入输出 二、代码总结 前言 最近看了一个提问, 问如何计算环形字符串的某子串出现次数, 其实算法比较简单, 增加在字符串结尾增加子字符串长度减一的字…

服务器数据恢复-reiserfs文件系统损坏如何恢复数据?

服务器数据恢复环境&#xff1a; 一台IBM X系列服务器&#xff0c;4块SAS硬盘组建一组RAID5阵列&#xff0c;采用的reiserfs文件系统。服务器操作系统分区结构&#xff1a;boot分区LVM卷swap分区&#xff08;按照前后顺序&#xff09;。LVM卷中直接划分了一个reiserfs文件系统&…

CSS基础选择器及常见属性

文章目录 一、CSS1、CSS简介2、CSS语法规范 二、CSS基础选择器1、选择器的作用2、选择器分类3、基础选择器标签选择器类选择器id选择器通配符选择器 三、CSS常见属性1、字体属性字体系列字体大小字体粗细文字样式 2、文本属性文本颜色对齐文本装饰文本文本缩进行间距 四、CSS引…

09微服务架构常见框架

一句话导读 微服务架构是一种分布式系统架构&#xff0c;可以将大型应用系统拆分为多个小型服务的一种架构方法。在微服务架构中&#xff0c;每个服务都是独立的逻辑单元&#xff0c;他们通过轻量级的通讯机制进行交互。为了更好的实现微服务架构&#xff0c;开发人员可以依赖一…

PMP P-10 Purchase Management

竟然不让上传图片了。。为啥&#xff1f;&#xff1f;&#xff1f;【上传过于频繁】行吧

搭建Serv-U FTP服务器实现外网远程访问,无需公网IP的文件共享方法

文章目录 1. 前言2. 本地FTP搭建2.1 Serv-U下载和安装2.2 Serv-U共享网页测试2.3 Cpolar下载和安装 3. 本地FTP发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 科技日益发展的今天&#xff0c;移动电子设备似乎成了我们生活的主角&#xff0c;智能…

2023最新官网下载安装配置JDK8(1.8u381)的详细步骤

目录 前言 一、下载 二、安装 三、配置环境变量 四、查看 前言 JDK11版本&#xff1a;官网下载JAVA的JDK11版本&#xff08;下载、安装、配置环境变量&#xff09;_java11下载_云边的快乐猫的博客-CSDN博客JDK17版本&#xff1a; Java官网下载JDK17版本详细教程&#xff…

Compose pager分页器入门使用 HorizontalPager与VerticalPager(2023/8)

Compose pager分页器入门使用 前言依赖概念介绍参数介绍 使用基础使用规范使用跳转指定分页器 pagerState.scrollToPage()添加指示器 pagerState.currentPage 完整代码总结 前言 阅读本文需要一定compose基础&#xff0c;如果没有请移步Jetpack Compose入门详解&#xff08;实…

说说我最近招人的感受。。

大家好&#xff0c;我是鱼皮。 都说现在行情不好、找工作难&#xff0c;但招人又谈何容易&#xff1f;&#xff01; 最近我们公司在招开发&#xff0c;实习社招都有。我收到的简历很多&#xff0c;但认真投递的、符合要求的却寥寥无几&#xff0c;而且都是我自己看简历、选人…

【EI检索稳定】第六届电力电子与控制工程国际学术会议(ICPECE 2023)

第六届电力电子与控制工程国际学术会议 2023 6th International Conference on Power Electronics and Control Engineering (ICPECE 2023) 第六届电力电子与控制工程国际学术会议由广西大学主办&#xff0c;重庆大学、华东交通大学、长春理工大学、大连交通大学联合主办。电…

伦敦金走势多变怎么办

投资知识比较丰富的朋友&#xff0c;应该知道一个品种的价格过于波动&#xff0c;对投资者来说并是一件不友好的事情&#xff0c;因为频繁的价格变化&#xff0c;对于收益的稳定性会产生负面的影响&#xff0c;也可能让投资者的持仓陷入进退维谷的尴尬境地。 黄金作为贵金属市场…