SpringBoot教程(十五) | SpringBoot集成RabbitMq

news2024/11/13 7:57:47

SpringBoot教程(十五) | SpringBoot集成RabbitMq

RabbitMq是我们在开发过程中经常会使用的一种消息队列。今天我们来研究研究rabbitMq的使用。

rabbitMq的官网: rabbitmq.com/

rabbitMq的安装这里先略过,因为我尝试了几次都失败了,后面等我安装成功了会把详细的文章发出来。目前是使用公司的环境进行的调试。

1. 一些概念

RabbitMQ是一个开源的消息代理和队列服务器,用来实现各个应用服务间的数据共享(跨平台 ,跨语言)。RabbitMQ是使用erlang语言编写的,并且基于AMQP协议实现。

所有的消息队列产品模型抽象上来说,都是类似的过程。生产者创建消息,然后发布到消息队列中,由消费者进行消费。

而rabbitMQ也是类似的,有生产者,消费者角色。其内部结构如下图所示。

image.png

那么接下来我们就来介绍一下RabbitMQ中的这些概念。

1. Message:

消息,就是我们需要传递和共享的信息,消息由一些列的可选属性组成,包括路由键,优先级,是否持久化等信息

2. Publisher

消息的生产者,也是一个向交换机发布消息的客户端应用程序。

3. Exchange:

交换机,这是RabbitMQ中的一个非常重要的概念,在rabbitMq中,生产者产生的消息都不是直接发送到队列中去的,而是发送到了交换机中,交换机会通过一定的规则绑定队列,交换机会根据相应的路由规则发送给对服务器中的队列。

4. Binding:

绑定, 用于交换机和消息列队之间的关联。一个绑定就是基于路由键(routing-key)将交换机和消息队列连接起来的路由规则。所以可以将交换机理解成一个有绑定有成的路由表。

5. Queue:

消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可以投入一个或多个队列中。消息一直在对队列里边,等待消费者连接到这个队列将其消费。

6. Connection:

网络连接,比如一个TCP连接。

7. Channel

信道,多路复用连接中的一条独立的双向数据流通道。信道是简历在真实的TCP连接内的虚拟连接。AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成的。因为对于操作系统过来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。

8. Consumer

消息的消费者,表示一个从消息队列中取得消息的客户端应用。

9. Virtual Host

虚拟主机,标识一批交换机、消息队列和相关对象。 虚拟主机是相同的身份认证和加密环境的独立服务器域。 每个vhost本质就是一个mini版的rabbitMQ服务器,拥有自己的队列,交换机,绑定和权限机制。vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ的默认vhost是/.

10. Broker

标识消息队列服务器实体。

2. Exchange类型

Exchange分发消息的时候根据类型的不同分发策略有所区别,目前常见的有四种类型: direct、fanout、topic、headers。 headers匹配AMQP消息的header而不是路由键,此外headers交换机和direct交换机完成一直但是性能差很多,几乎用不到了,所以直接看另外三种类型。

2.1 direct交换机

image.png

消息中的路由键(routing key)如果和Binding中的bing key一致,交换机就将消息发送到队列的队列中。路由键要完全匹配,单个传播。

2.2 fanout

image.png

每个发到fanout类型交换机的消息都会分到所有绑定的队列上去。fanout交换器不处理路由键,只是简单的将队列绑定到交换机上,每个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout类型转发消息是最快的。

2.3 topic

image.png

topic交换机通过模式匹配分配路由的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用.隔开。它同样会识别两个通配符: # 和* 。 #匹配0个或多个单词, * 匹配一个单词

3. springBoot集成RabbitMQ

SpringBoot集成rabbitMQ还是比较简单的,因为springBoot使用RabbitTemplate对常用操作进行了封装。

接下来我们来看一下集成过程。首先导入依赖。

xml复制代码<!-- 无需在parent的配置文件中添加 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

然后在springBoot的配置文件 application.yml中配置rabbitMQ连接信息

yml复制代码server:
  port: 7890

spring:
  rabbitmq:
    host: 172.15.33.52
    port: 5672
    username: root
    password: 123456

接下来我们我们分三种交换机进行演示。

3.1 direct

首先是配置类,在配置类中我们需要声明交换机,队列和绑定关系。

java复制代码@Configuration
public class DirectExchangeConfig {

    public static final String DIRECT_QUEUE = "directQueue";
    public static final String DIRECT_QUEUE2 = "directQueue2";
    public static final String DIRECT_EXCHANGE = "directExchange";
    public static final String DIRECT_ROUTING_KEY = "direct";

    @Bean
    public Queue directQueue() {
        return new Queue(DIRECT_QUEUE, true);
    }

    @Bean
    public Queue directQueue2() {
        return new Queue(DIRECT_QUEUE2, true);
    }

    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange(DIRECT_EXCHANGE, true, false);
    }

    @Bean
    public Binding bindingDirectExchange(Queue directQueue, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue).to(directExchange).with(DIRECT_ROUTING_KEY);
    }

    @Bean
    public Binding bindingDirectExchange2(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with(DIRECT_ROUTING_KEY);
    }

}

这里我们创建了一个叫directExchange的交换机,绑定了directQueue和directQueue2两个队列,路由键是direct.

消息的生产者,我们通过一个Controller来进行模拟,直接引用rabbitTemplate

java复制代码@RestController
@Slf4j
@RequestMapping("/direct")
public class DirectController {

    private final RabbitTemplate rabbitTemplate;

    public DirectController(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * direct交换机为直连模式交换机
     *      根据消息携带的路由键将消息投递给对应队列
     *
     *
     * @return
     */
    @GetMapping("send")
    public Object sendMsg() {
        rabbitTemplate.convertAndSend(DirectExchangeConfig.DIRECT_EXCHANGE, DirectExchangeConfig.DIRECT_ROUTING_KEY, "发送一条测试消息:direct");
        return "direct消息发送成功!!";
    }

当我在浏览器访问对应连接的时候,就会生产一条消息发送到directExchange交换机,路由key为:direct, 消息内容为:发送一条测试消息:direct

接下来我们来看消息的消费者。

java复制代码package com.lsqingfeng.action.rabbitmq.direct;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @className: DirectQueueListener
 * @description: 直连交换机的监听器
 * @author: sh.Liu
 * @date: 2021-08-23 16:03
 */
@Slf4j
@Component
public class DirectQueueListener {

    /**
     * 尽管设置了两个消费者,但是只有一个能够消费成功
     * 多次发送则轮训消费:
     * DirectReceiver消费者收到消息1  : 发送一条测试消息:direct
     * DirectReceiver消费者收到消息2  : 发送一条测试消息:direct
     * DirectReceiver消费者收到消息1  : 发送一条测试消息:direct
     * DirectReceiver消费者收到消息2  : 发送一条测试消息:direct
     *
     * 一个交换机可以绑定多个队列。如果通过路由key可以匹配到多个队列,消费的时候也只能有一个进行消费
     * @param testMessage
     */
    @RabbitHandler
    @RabbitListener(queues = DirectExchangeConfig.DIRECT_QUEUE)
    public void process(String testMessage) {
        System.out.println("DirectReceiver消费者收到消息1  : " + testMessage);
    }

    @RabbitHandler
    @RabbitListener(queues = DirectExchangeConfig.DIRECT_QUEUE)
    public void process2(String testMessage) {
        System.out.println("DirectReceiver消费者收到消息2  : " + testMessage);
    }

    @RabbitHandler
    @RabbitListener(queues = DirectExchangeConfig.DIRECT_QUEUE2)
    public void process3(String testMessage) {
        System.out.println("DirectReceiver消费者收到消息3  : " + testMessage);
    }

}

当我们访问浏览器生产消息会,观察控制台结果:

DirectReceiver消费者收到消息1 : 发送一条测试消息:direct DirectReceiver消费者收到消息3 : 发送一条测试消息:direct

在发送一次:

DirectReceiver消费者收到消息3 : 发送一条测试消息:direct DirectReceiver消费者收到消息2 : 发送一条测试消息:direct

由于我们又两个队列都绑定了交换机,且routeKey一样,所以会打印两条。要注意direct只有routeKey完全匹配的时候才能被消费,同时每个队列中的消息只会 被消费一次。

3.2 fanout

配置类:

java复制代码@Configuration
public class FanoutExchangeConfig {

    public static final String FANOUT_QUEUE = "fanoutQueue";
    public static final String FANOUT_QUEUE2 = "fanoutQueue2";
    public static final String FANOUT_QUEUE3 = "fanoutQueue3";
    public static final String FANOUT_EXCHANGE = "fanoutExchange";
    public static final String FANOUT_ROUTING_KEY = "fanout";

    @Bean
    public Queue fanoutQueue() {
        return new Queue(FANOUT_QUEUE, true);
    }

    @Bean
    public Queue fanoutQueue2() {
        return new Queue(FANOUT_QUEUE2, true);
    }

    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE, true, false);
    }

    @Bean
    public Binding bindingFanoutExchange(Queue fanoutQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
    }

    @Bean
    public Binding bindingFanoutExchange2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }

这里也是用一个Fanout类型的交换机绑定了两个队列,要注意在这种模式下,是不需要指定routing-Key的,因为所有绑定的队列都会收到消息。

生产者代码如下:

java复制代码@RestController
@Slf4j
@RequestMapping("/fanout")
public class FanoutController {

    private final RabbitTemplate rabbitTemplate;

    public FanoutController(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * fanout交换机为扇形模式交换机
     *      消息会发送到所有绑定的队列上。
     * @return
     */
    @GetMapping("send")
    public Object sendMsg() {
        rabbitTemplate.convertAndSend(FanoutExchangeConfig.FANOUT_EXCHANGE, null, "发送一条测试消息:fanout");
        return "fanout消息发送成功!!";
    }
}

消息的消费者:

java复制代码@Slf4j
@Component
public class FanoutQueueListener {

    /**
     * fanout交换机: 扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列
     * 同一个队列监听多次,只会消费一次。
     * 交换机绑定的多个队列都可以收到消息
     * @param testMessage
     */
    @RabbitHandler
    @RabbitListener(queues = FanoutExchangeConfig.FANOUT_QUEUE)
    public void process(String testMessage) {
        System.out.println("FanoutReceiver消费者收到消息1  : " + testMessage);
    }

    @RabbitHandler
    @RabbitListener(queues = FanoutExchangeConfig.FANOUT_QUEUE)
    public void process2(String testMessage) {
        System.out.println("FanoutReceiver消费者收到消息2  : " + testMessage);
    }

    @RabbitHandler
    @RabbitListener(queues = FanoutExchangeConfig.FANOUT_QUEUE2)
    public void process3(String testMessage) {
        System.out.println("FanoutReceiver消费者收到消息3  : " + testMessage);
    }

}

打印结果:

FanoutReceiver消费者收到消息1 : 发送一条测试消息:fanout FanoutReceiver消费者收到消息3 : 发送一条测试消息:fanout

因为方法1和方法2监听的是同一个队列,只有一个可以消费成功。多次执行,两个方法交替执行。

3.3 topic

主题交换机,会根据routing-Key的匹配规则,将消息发送到符合规则的队列中。

配置类:

java复制代码/**
 * @className: TopicExchangeConfig
 * @description:
 * *  (星号) 用来表示一个单词 (必须出现的)
 * #  (井号) 用来表示任意数量(零个或多个)单词
 * @author: sh.Liu
 * @date: 2021-08-23 15:49
 */
@Configuration
public class TopicExchangeConfig {

    public static final String TOPIC_QUEUE = "topicQueue";
    public static final String TOPIC_QUEUE2 = "topicQueue2";
    public static final String TOPIC_QUEUE3 = "topicQueue3";
    public static final String TOPIC_EXCHANGE = "topicExchange";
    public static final String TOPIC_ROUTING_KEY = "topic*";

    @Bean
    public Queue topicQueue() {
        return new Queue(TOPIC_QUEUE, true);
    }

    @Bean
    public Queue topicQueue2() {
        return new Queue(TOPIC_QUEUE2, true);
    }

    @Bean
    public Queue topicQueue3() {
        return new Queue(TOPIC_QUEUE3, true);
    }

    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE, true, false);
    }

    @Bean
    public Binding bindingTopicExchange(Queue topicQueue, TopicExchange topicExchange) {
        return BindingBuilder.bind(topicQueue).to(topicExchange).with("topic.#");
    }

    @Bean
    public Binding bindingTopicExchange2(Queue topicQueue2, TopicExchange topicExchange) {
        return BindingBuilder.bind(topicQueue2).to(topicExchange).with("test.#");
    }

    @Bean
    public Binding bindingTopicExchange3(Queue topicQueue3, TopicExchange topicExchange) {
        return BindingBuilder.bind(topicQueue3).to(topicExchange).with("#");
    }
}

这里要注意我们的绑定管关系。分别是topic.#, test.*, #

#: 代表所有,* 代表有且只有一个。

消息的发送者,我们将routingKey作为参数方便我们看效果:

java复制代码@RestController
@Slf4j
@RequestMapping("/topic")
public class TopicController {

    private final RabbitTemplate rabbitTemplate;

    public TopicController(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    @GetMapping("send")
    public Object sendMsg(String routingKey) {
        rabbitTemplate.convertAndSend(TopicExchangeConfig.TOPIC_EXCHANGE, routingKey, "发送一条测试消息:topic");
        return "topic消息发送成功!!";
    }

消息的消费者:

java复制代码/**
 * @className: TopicQueueListener
 * @description: 主题交换机的监听器
 * @author: sh.Liu
 * @date: 2021-08-23 16:03
 */
@Slf4j
@Component
public class TopicQueueListener {

    /**
     * topic: 主题交换机
     * @param testMessage
     */
    @RabbitHandler
    @RabbitListener(queues = TopicExchangeConfig.TOPIC_QUEUE)
    public void process(String testMessage) {
        System.out.println("TopicReceiver消费者收到消息1  : " + testMessage);
    }

    @RabbitHandler
    @RabbitListener(queues = TopicExchangeConfig.TOPIC_QUEUE)
    public void process2(String testMessage) {
        System.out.println("TopicReceiver消费者收到消息2  : " + testMessage);
    }

    @RabbitHandler
    @RabbitListener(queues = TopicExchangeConfig.TOPIC_QUEUE2)
    public void process3(String testMessage) {
        System.out.println("TopicReceiver消费者收到消息3  : " + testMessage);
    }

    @RabbitHandler
    @RabbitListener(queues = TopicExchangeConfig.TOPIC_QUEUE3)
    public void process4(String testMessage) {
        System.out.println("TopicReceiver消费者收到消息4  : " + testMessage);
    }

}

请求:http://localhost:7890/topic/send?routingKey=test.a

结果:

TopicReceiver消费者收到消息3 : 发送一条测试消息:topic TopicReceiver消费者收到消息4 : 发送一条测试消息:topic

代表: test.* 和 # 与路由key匹配成功

请求:http://localhost:7890/topic/send?routingKey=topic.123

TopicReceiver消费者收到消息1 : 发送一条测试消息:topic TopicReceiver消费者收到消息4 : 发送一条测试消息:topic

代表: topic.# 和 # 匹配成功

请求: http://localhost:7890/topic/send?routingKey=test

TopicReceiver消费者收到消息4 : 发送一条测试消息:topic

test.* 后面必须要有一个单词

请求: http://localhost:7890/topic/send?routingKey=test.aaa

TopicReceiver消费者收到消息4 : 发送一条测试消息:topic TopicReceiver消费者收到消息3 : 发送一条测试消息:topic

test.*和 #匹配成功

请求:http://localhost:7890/topic/send?routingKey=test.aaa.b

TopicReceiver消费者收到消息4 : 发送一条测试消息:topic

只对# 匹配成功, 因为test.*只能匹配一个单词,aaa.b代表两个

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

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

相关文章

【数据结构】详谈队列的顺序存储及C语言实现

循环队列及其基本操作的C语言实现 前言一、队列的顺序存储1.1 队尾指针与队头指针1.2 基本操作实现的底层逻辑1.2.1 队列的创建与销毁1.2.2 队列的增加与删除1.2.3 队列的判空与判满1.2.4 逻辑的局限性 二、循环队列2.1 循环队列的实现逻辑一2.2 循环队列的实现逻辑二2.3 循环队…

西瓜书读书笔记整理(十二) —— 第十二章 计算学习理论

第十二章 计算学习理论&#xff08;上&#xff09; 12.1 基础知识12.1.1 什么是计算学习理论&#xff08;computational learning theory&#xff09;12.1.2 什么是独立同分布&#xff08;independent and identically distributed, 简称 i . i . d . i.i.d. i.i.d.&#xff0…

USRP相关报错解决办法

文章目录 前言一、本地环境二、相关报错信息二、解决办法1、更换电脑操作系统2、升级最新版固件 前言 在进行 USRP 开发时遇到了一些报错&#xff0c;这里做个记录解决问题的方法。 一、本地环境 电脑操作系统&#xff1a;Windows11MATLAB 版本&#xff1a;MATLAB 2021aUSRP …

JVM:Java类加载机制

Java类加载机制的全过程&#xff1a; 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的&#xff0c;类型的加载过程必须按照这种顺序按部就班地开始&#xff0c;而解析阶段则不一定&#xff1a;它在某些情况下可以在初始化阶段之后再开始&#xff0c; 这是为了支持Java…

竞赛保研 机器视觉 opencv 深度学习 驾驶人脸疲劳检测系统 -python

文章目录 0 前言1 课题背景2 Dlib人脸识别2.1 简介2.2 Dlib优点2.3 相关代码2.4 人脸数据库2.5 人脸录入加识别效果 3 疲劳检测算法3.1 眼睛检测算法3.2 打哈欠检测算法3.3 点头检测算法 4 PyQt54.1 简介4.2相关界面代码 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#x…

CSS明显比XPATH更性感!CSS再学一点儿

在selenium应用&#xff0c;CSS比XPATH更性感 To style an element, Front end developers need to locate the element first and then apply styling rules. It looks like this: #logo{ color: white; background: black; }That CSS snippet says, apply color and backgro…

深入探索 Android 中的 Runtime

深入探索 Android 中的 Runtime 一、什么是 Runtime二、Android 中的 Runtime 类型2.1. Dalvik Runtime2.2. ART&#xff08;Android Runtime&#xff09; 三、Runtime 的作用和特点3.1. 应用程序执行环境3.2. 跨平台支持3.3. 性能优化3.4. 应用程序优化 四、与应用开发相关的重…

Unity3D学习之Unity基础——3D数学

文章目录 1. 前言2 Mathf和Math基础2.1 一般用于只计算一次的函数2.1.1 PI Π PI2.1.2 取绝对值 Abs2.1.3 向上取整 CeilToInt2.1.4 向下取整 FloorToInt2.1.5 钳制函数 Clamp2.1.6 获取最大值 Max2.1.7 获取最小值 Min2.1.8 一个数的n次幂 Pow2.1.9 四舍五入 RoundToInt2.1.10…

MySQL主从复制原理与实践:从配置到故障监控

文章目录 前言主从复制原理复制源主节点的工作从节点的工作复制流程的设计 主从复制环境搭建一、主从节点配置二、从节点开启复制步骤1、备份主节点的数据2、将数据同步到从节点3、从节点复制参数配置 三、验证复制环境 主从复制故障监控监控主从复制状态监控主从复制延迟 总结…

Rancher部署k8s集群测试安装nginx(节点重新初始化方法,亲测)

目录 一、安装前准备工作计算机升级linux内核时间同步Hostname设置hosts设置关闭防火墙&#xff0c;selinux关闭swap安装docker 二、安装rancher部署rancher 三、安装k8s安装k8s集群易错点&#xff0c;重新初始化 四、安装kutectl五、测试安装nginx工作负载 一、安装前准备工作…

Python中二维数据(数组、列表)索引和切片的Bug

Python中有关数据结构索引和切片引起的Bug 一维数据索引和切片一维数组一维列表 二维数据的索引和切片二维数组二维(错误)列表 一维数据索引和切片 一维数组 对于一维数据进行索引和切片操作&#xff0c;大家都比较熟悉通过下面代码进行实现 import numpy as np data np.ra…

实验七 RMAN恢复管理器

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的很重要&…

Linux的权限(3)

目录 文件类型 ​d目录文件 -普通文件 l链接文件 b块设备文件 p管道文件 c字符设备文件 文件权限 目录权限 umask 粘滞位 Q1umask权限默认值664/775 Q2"可执行性"权限 Q3"删除"权限 Q4怎么共享一批文件 【1】粘滞位 【2】添加交互人员到所…

SpringBoot跨域问题解决

前端访问后台接口时&#xff0c;浏览器报错&#xff0c;跨域无法访问。 报错信息如下&#xff1a; Response to preflight request doesnt pass access control check: No Access-Control-Allow-Origin header is present on the requested resource. 经过一番百度之后&#…

(2024,VMamba,交叉扫描,线性复杂度,全局感受野,动态权重)视觉状态空间模型

VMamba: Visual State Space Model 公和众和号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 目录 0. 摘要 3. 方法 3.1 基础概念 3.2 2D 选择性扫描 3.3 VMamba 模型 3.3.1 整体架构 3.3.2 VSS…

如何用H5+CSS+JS写一个简单的招聘网站

大家好&#xff0c;我是猿码叔叔&#xff0c;一个 Java 语言开发者。应网友要求&#xff0c;写一个简单的招聘页面。由于技术原因&#xff0c;页面相对简单&#xff0c;朋友们可以选择性的阅读&#xff0c;如果对您有帮助&#xff0c;也可直接拿去使用&#xff0c;因为接下来除…

Linux ---- 小玩具

目录 一、安装&#xff1a; 1、佛祖保佑&#xff0c;永不宕机&#xff0c;永无bug 2、小火车 3、艺术字和其它 天气预报 艺术字 4、会说话的小牦牛 5、其他趣味图片 我爱你 腻害 英雄联盟 帅 忍 龙 你是猪 福 好运连连 欢迎 加油 想你 忘不了你 我错了 你…

介绍几个免费的国内chatgpt网站

概述&#xff1a;水点文章。 第一&#xff1a;chataa网站 chataa (chat778.com) 进去之后注册一下&#xff0c;即可免费使用。 第二&#xff1a;AlchatOS网站 AIchatOS 第三&#xff1a;ChatGPT在线聊天 ChatGPT在线聊天 (zxf7460.cn) 第四&#xff1a;说我真帅&#xff0…

重构改善既有代码的设计-学习(一):封装

1、封装记录&#xff08;Encapsulate Record&#xff09; 一些记录性结构&#xff08;例如hash、map、hashmap、dictionary等&#xff09;&#xff0c;一条记录上持有什么字段往往不够直观。如果其使用范围比较宽&#xff0c;这个问题往往会造成许多困扰。所以&#xff0c;记录…

pytest + allure(windows)安装

背景 软硬件环境&#xff1a; windows11&#xff0c;已安装anaconda&#xff0c;python&#xff0c;pycharm用途&#xff1a;使用pytest allure 生成报告allure 依赖java&#xff0c;点击查看java安装教程 allure 下载与安装 从 allure下载网址下载最新版本.zip文件 放在自…