开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Springboot整合RabbitMq,看完这一篇就够了!

发表于 2021-08-28

前言

RabbiMQ介绍

一、使用场景

RabbitMQ是一个消息中间件,所以最主要的作用就是:信息缓冲区,实现应用程序的异步和解耦。
RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。RabbitMQ主要是为了实现系统之间的双向解耦而实现的。当生产者大量产生数据时,消费者无法快速消费,那么需要一个中间层。保存这个数据。
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。详细概念可以参考官方指南 RabbitMQ

二、相关概念

通常我们谈到队列服务, 会有三个概念:发消息者、队列、收消息者,RabbitMQ 在这个基本概念之上, 多做了一层抽象, 在发消息者和 队列之间, 加入了交换器 (Exchange). 这样发消息者和队列就没有直接联系, 转而变成发消息者把消息给交换器, 交换器根据调度策略再把消息再给队列。

那么,其中比较重要的概念有 4 个,分别为:虚拟主机,交换机,队列,和绑定。

  • 虚拟主机v-host:一个虚拟主机持有一组交换机、队列和绑定。为什么需要多个虚拟主机呢?很简单,RabbitMQ当中,用户只能在虚拟主机的粒度进行权限控制。因此,如果需要禁止A组访问B组的交换机/队列/绑定,必须为A和B分别创建一个虚拟主机。每一个RabbitMQ服务器都有一个默认的虚拟主机。
  • 交换机:Exchange 用于转发消息,但是它不会做存储 ,如果没有 Queue bind 到 Exchange 的话,它会直接丢弃掉 Producer 发送过来的消息。这里有一个比较重要的概念:路由键 。消息到交换机的时候,交互机会转发到对应的队列中,那么究竟转发到哪个队列,就要根据该路由键。
  • 绑定:也就是交换机需要和队列相绑定,这其中如上图所示

交换机(Exchange)

交换机的功能主要是接收消息并且转发到绑定的队列,交换机不存储消息,在启用ack模式后,交换机找不到队列会返回错误。交换机有四种类型:Direct, topic, Headers and Fanout

  • Direct:direct 类型的行为是”先匹配, 再投送”. 即在绑定时设定一个 routingkey, 消息的routingkey 匹配时, 才会被交换器投送到绑定的队列中去.
  • Topic:按规则转发消息(最灵活)
  • Headers:设置header attribute参数类型的交换机
  • Fanout:转发消息到所有绑定队列

Direct Exchange

Direct Exchange是RabbitMQ默认的交换机模式,也是最简单的模式,根据key全文匹配去寻找队列。

第一个 X - Q1 就有一个 binding key,名字为 orange;X - Q2 就有 2 个 binding key,名字为 black 和 green。当消息中的 路由键 和 这个 binding key 对应上的时候,那么就知道了该消息去到哪一个队列中。

Ps:为什么 X 到 Q2 要有 black,green,2个 binding key呢,一个不就行了吗?- 这个主要是因为可能又有 Q3,而Q3只接受 black 的信息,而Q2不仅接受black 的信息,还接受 green 的信息。

Topic Exchange

根据通配符转发消息到队列,在这种交换机下,队列和交换机的绑定会定义一种路由模式,那么,通配符就要在这种路由模式和路由键之间匹配后交换机才能转发消息。

  • *(星号)可以替代一个单词。
  • #(hash)可以替换零个或多个单词。

Headers Exchange

headers 也是根据规则匹配, 相较于 direct 和 topic 固定地使用 routing_key , headers 则是一个自定义匹配规则的类型. 在队列与交换器绑定时, 会设定一组键值对规则, 消息中也包括一组键值对( headers 属性), 当这些键值对有一对, 或全部匹配时, 消息被投送到对应队列.

Fanout Exchange

消息广播的模式,也就是我们的发布订阅模式。Fanout Exchange 消息广播的模式,不管路由键或者是路由模式,会把消息发给绑定给它的全部队列,如果配置了routing_key会被忽略。

消息确认

消息消费者如何通知 Rabbit 消息消费成功?

消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK 自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息 如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失 如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者 如果某个服务忘记 ACK 了,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限 ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟 消息确认模式有:

  • AcknowledgeMode.NONE:自动确认
  • AcknowledgeMode.AUTO:根据情况确认
  • AcknowledgeMode.MANUAL:手动确认

SpringBoot集成RabbitMQ

  1. 配置pom,主要添加spring-boot-starter-amqp支持,springboot基于2.1.4版本
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-amqp</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-web</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-test</artifactId>
  13. <scope>test</scope>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.projectlombok</groupId>
  17. <artifactId>lombok</artifactId>
  18. <optional>true</optional>
  19. </dependency>

配置springboot的yaml文件
1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
markdown复制代码1. `server:`
2. `servlet:`
3. `context-path:/rabbitmq`
4. `port:9004`
5. `spring:`
6. `application:`
7. `name: rabbitmq`
8. `rabbitmq:`
9. `host: localhost`
10. `virtual-host:/crawl`
11. `username: xxxx`
12. `password: xxx`
13. `port:5672`
14. `# 消息失败返回,比如路由不到队列时触发回调`
15. `publisher-returns:true`
16. `# 消息正确发送确认`
17. `publisher-confirms:true`
18. `template:`
19. `retry:`
20. `enabled:true`
21. `initial-interval:2s`
22. `listener:`
23. `simple:`
24. `# 手动ACK 不开启自动ACK模式,目的是防止报错后未正确处理消息丢失 默认 为 none`
25. `acknowledge-mode: manual`

另外我们还要配置ACK确认回调的配置,通过实现RabbitTemplate.ConfirmCallback接口,消息发送到Broker后触发回调,也就是只能确认是否正确到达Exchange中。

  1. import lombok.extern.slf4j.Slf4j;

  2. import org.springframework.amqp.rabbit.connection.CorrelationData;

  3. import org.springframework.amqp.rabbit.core.RabbitTemplate;

  4. import org.springframework.beans.factory.annotation.Autowired;

  5. import org.springframework.stereotype.Component;

  6. import javax.annotation.PostConstruct;

  7. /**

  8. * @author lijianqing

  9. * @version 1.0

  10. * @ClassName RabbitTemplateConfirmCallback

  11. * @date 2019/4/23 12:55

  12. */

  13. @Component

  14. @Slf4j

  15. public class RabbitTemplateConfirmCallback implements RabbitTemplate.ConfirmCallback {

  16. @Autowired

  17. private RabbitTemplate rabbitTemplate;

  18. @PostConstruct

  19. public void init() {

  20. //指定 ConfirmCallback

  21. 1
    kotlin复制代码    `rabbitTemplate.setConfirmCallback(this);`
  22. }

  23. @Override

  24. public void confirm(CorrelationData correlationData, boolean ack, String cause) {

  25. 1
    c复制代码    `log.info("消息唯一标识:{},确认结果:{},失败原因:{}", correlationData, ack, cause);`
  26. }

  27. }

消息失败返回,比如路由步到队列就会触发,如果西区奥西发送到交换器成功,但是没有匹配的队列就会触发回调

  1. import lombok.extern.slf4j.Slf4j;

  2. import org.springframework.amqp.core.Message;

  3. import org.springframework.amqp.rabbit.core.RabbitTemplate;

  4. import org.springframework.beans.factory.annotation.Autowired;

  5. import org.springframework.stereotype.Component;

  6. import javax.annotation.PostConstruct;

  7. /**

  8. * @author lijianqing

  9. * @version 1.0

  10. * @ClassName RabbitTemplateReturnCallback

  11. * @date 2019/4/23 12:55

  12. */

  13. @Component

  14. @Slf4j

  15. public class RabbitTemplateReturnCallback implements RabbitTemplate.ReturnCallback {

  16. @Autowired

  17. private RabbitTemplate rabbitTemplate;

  18. @PostConstruct

  19. public void init() {

  20. //指定 ReturnCallback

  21. 1
    kotlin复制代码    `rabbitTemplate.setReturnCallback(this);`
  22. 1
    arduino复制代码    `rabbitTemplate.setMandatory(true);`
  23. }

  24. @Override

  25. public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {

  26. 1
    c复制代码    `log.info("消息主体 message : "` `+ message);`
  27. 1
    c复制代码    `log.info("消息主体 message : "` `+ replyCode);`
  28. 1
    c复制代码    `log.info("描述:"` `+ replyText);`
  29. 1
    c复制代码    `log.info("消息使用的交换器 exchange : "` `+ exchange);`
  30. 1
    c复制代码    `log.info("消息使用的路由键 routing : "` `+ routingKey);`
  31. }

  32. }

一、简单的开始-简单队列

如下图:

“P”是我们的生产者,“C”是我们的消费者。中间的框是一个队列 - RabbitMQ代表消费者保留的消息缓冲区。

新增SimpleConfig,创建我们要投放的队列:代码如下

  1. /**
  2. * 队列直接投放
  3. * @author lijianqing
  4. * @version 1.0
  5. * @ClassName SimpleConfig
  6. * @date 2019/4/26 15:11
  7. */
  8. @Configuration
  9. public class SimpleConfig {
  10. @Bean
  11. public Queue simpleQueue() {
  12. return new Queue("simple");
  13. }
  14. }

再分别创建消息发送者与消息接收者:

  • 消息发送者
  1. import lombok.extern.slf4j.Slf4j;

  2. import org.springframework.amqp.rabbit.connection.CorrelationData;

  3. import org.springframework.amqp.rabbit.core.RabbitTemplate;

  4. import org.springframework.beans.factory.annotation.Autowired;

  5. import org.springframework.stereotype.Component;

  6. import zero.springboot.study.rabbitmq.model.User;

  7. import java.util.UUID;

  8. /**

  9. * @author lijianqing

  10. * @version 1.0

  11. * @ClassName HelloSender

  12. * @date 2019/4/23 11:22

  13. */

  14. @Component

  15. @Slf4j

  16. public class HelloSender {

  17. @Autowired

  18. private RabbitTemplate rabbitTemplate;

  19. public void send() {

  20. User user = new User();

  21. 1
    arduino复制代码    `user.setName("青");`
  22. 1
    arduino复制代码    `user.setPass("111111");`
  23. //发送消息到hello队列

  24. 1
    c复制代码    `log.info("发送消息:{}", user);`
  25. 1
    less复制代码    `rabbitTemplate.convertAndSend("hello", user,` `new` `CorrelationData(UUID.randomUUID().toString()));`
  26. String msg = "hello qing";

  27. 1
    c复制代码    `log.info("发送消息:{}", msg);`
  28. 1
    arduino复制代码    `rabbitTemplate.convertAndSend("simple", msg);`
  29. }

  30. }

  • 消息接收者
  1. import com.rabbitmq.client.Channel;

  2. import lombok.extern.slf4j.Slf4j;

  3. import org.springframework.amqp.rabbit.annotation.RabbitHandler;

  4. import org.springframework.amqp.rabbit.annotation.RabbitListener;

  5. import org.springframework.amqp.support.AmqpHeaders;

  6. import org.springframework.messaging.handler.annotation.Header;

  7. import org.springframework.stereotype.Component;

  8. import zero.springboot.study.rabbitmq.model.User;

  9. import java.io.IOException;

  10. /**

  11. * 监听hello队列

  12. *

  13. * @author lijianqing

  14. * @version 1.0

  15. * @ClassName HelloReceiver

  16. * @date 2019/4/23 11:42

  17. */

  18. @Component

  19. @Slf4j

  20. @RabbitListener(queues = "simple")

  21. public class HelloReceiver {

  22. @RabbitHandler

  23. public void processUser(User user, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {

  24. 1
    c复制代码    `log.info("收到消息:{}", user);`
  25. // 手动ACK

  26. try {

  27. // //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。

  28. 1
    go复制代码        `channel.basicAck(tag,` `false);`
  29. // 代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队

  30. // channel.basicNack(deliveryTag, false, false);

  31. // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队

  32. // channel.basicReject(deliveryTag,false);

  33. } catch (IOException e) {

  34. 1
    go复制代码        `e.printStackTrace();`
  35. }

  36. }

  37. @RabbitHandler

  38. public void processString(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {

  39. 1
    c复制代码    `log.info("收到消息:{}", message);`
  40. // 手动ACK

  41. try {

  42. // //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。

  43. 1
    go复制代码        `channel.basicAck(tag,` `false);`
  44. // 代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队

  45. // channel.basicNack(deliveryTag, false, false);

  46. // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队

  47. // channel.basicReject(deliveryTag,false);

  48. } catch (IOException e) {

  49. 1
    go复制代码        `e.printStackTrace();`
  50. }

  51. }

  52. }

这样就实现了简单的消息发送到指定队列的模式。我们写一个测试类

二、Direct Exchange模式

主要配置我们的Direct Exchange交换机,并且创建队列通过routing key 绑定到交换机上

  1. import org.springframework.amqp.core.Binding;
  2. import org.springframework.amqp.core.BindingBuilder;
  3. import org.springframework.amqp.core.DirectExchange;
  4. import org.springframework.amqp.core.Queue;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. /**
  8. *
  9. * @author lijianqing
  10. * @version 1.0
  11. * @ClassName DirectConfig
  12. * @date 2019/4/23 11:15
  13. */
  14. @Configuration
  15. public class DirectConfig {
  16. //队列名字
  17. public static final String QUEUE_NAME = "direct_name";
  18. //交换机名称
  19. public static final String EXCHANGE = "zero-exchange";
  20. //路由键名称
  21. public static final String ROUTING_KEY = "routingKey";
  22. @Bean
  23. public Queue blueQueue() {
  24. return new Queue(QUEUE_NAME, true);
  25. }
  26. @Bean
  27. public DirectExchange defaultExchange() {
  28. return new DirectExchange(EXCHANGE);
  29. }
  30. @Bean
  31. public Binding bindingBlue() {
  32. return BindingBuilder.bind(blueQueue()).to(defaultExchange()).with(ROUTING_KEY);
  33. }
  34. }

接下来我们创建生产者与消费者

  • 生产者
  1. import lombok.extern.slf4j.Slf4j;

  2. import org.springframework.amqp.rabbit.connection.CorrelationData;

  3. import org.springframework.amqp.rabbit.core.RabbitTemplate;

  4. import org.springframework.beans.factory.annotation.Autowired;

  5. import org.springframework.stereotype.Component;

  6. import zero.springboot.study.rabbitmq.config.DirectConfig;

  7. import zero.springboot.study.rabbitmq.model.User;

  8. import java.util.UUID;

  9. /**

  10. * @author lijianqing

  11. * @version 1.0

  12. * @ClassName HelloSender

  13. * @date 2019/4/23 11:22

  14. */

  15. @Component

  16. @Slf4j

  17. public class DirectSender {

  18. @Autowired

  19. private RabbitTemplate rabbitTemplate;

  20. public void send() {

  21. User user = new User();

  22. 1
    arduino复制代码    `user.setName("青");`
  23. 1
    arduino复制代码    `user.setPass("111111");`
  24. //发送消息到hello队列

  25. 1
    c复制代码    `log.info("DirectReceiver发送消息:{}", user);`
  26. 1
    less复制代码    `rabbitTemplate.convertAndSend(DirectConfig.EXCHANGE,` `DirectConfig.ROUTING_KEY, user,` `new` `CorrelationData(UUID.randomUUID().toString()));`
  27. String msg = "hello qing";

  28. 1
    c复制代码    `log.info("DirectReceiver发送消息:{}", msg);`
  29. 1
    go复制代码    `rabbitTemplate.convertAndSend(DirectConfig.EXCHANGE,` `DirectConfig.ROUTING_KEY, msg);`
  30. }

  31. }

  • 消费者
  1. /**

  2. *

  3. * @author lijianqing

  4. * @version 1.0

  5. * @ClassName HelloReceiver

  6. * @date 2019/4/23 11:42

  7. */

  8. @Component

  9. @Slf4j

  10. @RabbitListener(queues = "direct_name")

  11. public class DirectReceiver {

  12. @RabbitHandler

  13. public void processUser(User user, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {

  14. 1
    c复制代码    `log.info("DirectReceiver收到消息:{}", user);`
  15. // 手动ACK

  16. try {

  17. // //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。

  18. 1
    go复制代码        `channel.basicAck(tag,` `false);`
  19. // 代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队

  20. // channel.basicNack(deliveryTag, false, false);

  21. // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队

  22. // channel.basicReject(deliveryTag,false);

  23. } catch (IOException e) {

  24. 1
    go复制代码        `e.printStackTrace();`
  25. }

  26. }

  27. @RabbitHandler

  28. public void processString(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {

  29. 1
    c复制代码    `log.info("收到消息:{}", message);`
  30. // 手动ACK

  31. try {

  32. // //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。

  33. 1
    go复制代码        `channel.basicAck(tag,` `false);`
  34. // 代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队

  35. // channel.basicNack(deliveryTag, false, false);

  36. // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队

  37. // channel.basicReject(deliveryTag,false);

  38. } catch (IOException e) {

  39. 1
    go复制代码        `e.printStackTrace();`
  40. }

  41. }

  42. }

三、Topic Exchange模式

创建队列以及交换机。并通过路由匹配规则将队列与交换机绑定上

  1. import org.springframework.amqp.core.Binding;
  2. import org.springframework.amqp.core.BindingBuilder;
  3. import org.springframework.amqp.core.Queue;
  4. import org.springframework.amqp.core.TopicExchange;
  5. import org.springframework.beans.factory.annotation.Qualifier;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. /**
  9. * queueMessages 匹配topic.#,queueMessage 只匹配 "topic.message"
  10. *
  11. * @author lijianqing
  12. * @version 1.0
  13. * @ClassName TopicRabbitConfig
  14. * @date 2019/4/23 15:03
  15. */
  16. @Configuration
  17. public class TopicRabbitConfig {
  18. final static String message = "topic.message";
  19. final static String messages = "topic.messages";
  20. @Bean
  21. public Queue queueMessage() {
  22. return new Queue(TopicRabbitConfig.message);
  23. }
  24. @Bean
  25. public Queue queueMessages() {
  26. return new Queue(TopicRabbitConfig.messages);
  27. }
  28. @Bean
  29. TopicExchange exchange() {
  30. return new TopicExchange("topicExchange");
  31. }
  32. @Bean
  33. Binding bindingExchangeMessage(@Qualifier("queueMessage") Queue queueMessage, TopicExchange exchange) {
  34. return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message");
  35. }
  36. @Bean
  37. Binding bindingExchangeMessages(@Qualifier("queueMessages") Queue queueMessages, TopicExchange exchange) {
  38. return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#");
  39. }
  40. }
  • 创建生产者
  1. import lombok.extern.slf4j.Slf4j;

  2. import org.springframework.amqp.rabbit.core.RabbitTemplate;

  3. import org.springframework.beans.factory.annotation.Autowired;

  4. import org.springframework.stereotype.Component;

  5. /**

  6. * @author lijianqing

  7. * @version 1.0

  8. * @ClassName TopicSender

  9. * @date 2019/4/23 15:10

  10. */

  11. @Component

  12. @Slf4j

  13. public class TopicSender {

  14. @Autowired

  15. private RabbitTemplate rabbitTemplate;

  16. /**

  17. * 匹配topic.message,两个队列都会收到

  18. */

  19. public void send1() {

  20. String context = "hi, i am message 1";

  21. 1
    c复制代码    `log.info("主题发送 : {}"` `, context);`
  22. 1
    less复制代码    `rabbitTemplate.convertAndSend("topicExchange",` `"topic.message", context);`
  23. }

  24. /**

  25. * 匹配topic.messages

  26. */

  27. public void send2() {

  28. String context = "hi, i am messages 2";

  29. 1
    c复制代码    `log.info("主题发送 : {}"` `, context);`
  30. 1
    less复制代码    `rabbitTemplate.convertAndSend("topicExchange",` `"topic.messages", context);`
  31. }

  32. }

  • 创建消费者,这里我们分别创建两个队列的消费者
  1. @Component

  2. @RabbitListener(queues = "topic.message")

  3. @Slf4j

  4. public class TopicReceiver {

  5. @RabbitHandler

  6. public void process(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {

  7. 1
    c复制代码    `log.info("topic.message Receiver1  {}: ", message);`
  8. // 手动ACK

  9. try {

  10. // //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。

  11. 1
    go复制代码        `channel.basicAck(tag,` `false);`
  12. // 代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队

  13. // channel.basicNack(deliveryTag, false, false);

  14. // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队

  15. // channel.basicReject(deliveryTag,false);

  16. } catch (IOException e) {

  17. 1
    go复制代码        `e.printStackTrace();`
  18. }

  19. }

  20. }

第二个消费者

  1. @Component

  2. @RabbitListener(queues = "topic.messages")

  3. @Slf4j

  4. public class TopicReceiver2 {

  5. @RabbitHandler

  6. public void process(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {

  7. 1
    c复制代码    `log.info("topic.messages Receiver2  : {}", message);`
  8. // 手动ACK

  9. try {

  10. // //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。

  11. 1
    go复制代码        `channel.basicAck(tag,` `false);`
  12. // 代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队

  13. // channel.basicNack(deliveryTag, false, false);

  14. // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队

  15. // channel.basicReject(deliveryTag,false);

  16. } catch (IOException e) {

  17. 1
    go复制代码        `e.printStackTrace();`
  18. }

  19. }

  20. }

四、Fanout 模式

也就是发布、订阅。所有绑定在交换机上的队列都会收到消息,发送端指定的routing key的任何字符都会被忽略

配置交换机与队列

  1. @Configuration
  2. public class FanoutRabbitConfig {
  3. @Bean
  4. public Queue AMessage() {
  5. return new Queue("fanout.A");
  6. }
  7. @Bean
  8. public Queue BMessage() {
  9. return new Queue("fanout.B");
  10. }
  11. @Bean
  12. public Queue CMessage() {
  13. return new Queue("fanout.C");
  14. }
  15. @Bean
  16. FanoutExchange fanoutExchange() {
  17. return new FanoutExchange("fanoutExchange");
  18. }
  19. @Bean
  20. Binding bindingExchangeA(Queue AMessage, FanoutExchange fanoutExchange) {
  21. return BindingBuilder.bind(AMessage).to(fanoutExchange);
  22. }
  23. @Bean
  24. Binding bindingExchangeB(Queue BMessage, FanoutExchange fanoutExchange) {
  25. return BindingBuilder.bind(BMessage).to(fanoutExchange);
  26. }
  27. @Bean
  28. Binding bindingExchangeC(Queue CMessage, FanoutExchange fanoutExchange) {
  29. return BindingBuilder.bind(CMessage).to(fanoutExchange);
  30. }
  31. }
  • 创建发送者
  1. @Component

  2. @Slf4j

  3. public class FanoutSender {

  4. @Autowired

  5. private RabbitTemplate rabbitTemplate;

  6. public void send() {

  7. String context = "hi, fanout msg ";

  8. 1
    csharp复制代码    `rabbitTemplate.convertAndSend("fanoutExchange",` `null, context);`
  9. }

  10. }

  • 创建A、B、C队列消费者
  1. @Component

  2. @RabbitListener(queues = "fanout.A")

  3. @Slf4j

  4. public class FanoutReceiverA {

  5. @RabbitHandler

  6. public void process(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {

  7. 1
    c复制代码    `log.info("fanout Receiver A  : {}"` `, message);`
  8. // 手动ACK

  9. try {

  10. // //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。

  11. 1
    go复制代码        `channel.basicAck(tag,` `false);`
  12. // 代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队

  13. // channel.basicNack(deliveryTag, false, false);

  14. // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队

  15. // channel.basicReject(deliveryTag,false);

  16. } catch (IOException e) {

  17. 1
    go复制代码        `e.printStackTrace();`
  18. }

  19. }

  20. }

剩下的B、C就不重复贴了。

单元测试

  1. import org.junit.Test;

  2. import org.junit.runner.RunWith;

  3. import org.springframework.beans.factory.annotation.Autowired;

  4. import org.springframework.boot.test.context.SpringBootTest;

  5. import org.springframework.test.context.junit4.SpringRunner;

  6. import zero.springboot.study.rabbitmq.direct.DirectSender;

  7. import zero.springboot.study.rabbitmq.fanout.FanoutSender;

  8. import zero.springboot.study.rabbitmq.simple.HelloSender;

  9. import zero.springboot.study.rabbitmq.topic.TopicSender;

  10. @RunWith(SpringRunner.class)

  11. @SpringBootTest(classes = RabbitmqApplication.class)

  12. public class RabbitmqApplicationTests {

  13. @Autowired

  14. private DirectSender directSender;

  15. @Autowired

  16. private TopicSender topicSender;

  17. @Autowired

  18. private FanoutSender fanoutSender;

  19. @Autowired

  20. private HelloSender helloSender;

  21. @Test

  22. public void testDirect() {

  23. 1
    go复制代码    `directSender.send();`
  24. }

  25. @Test

  26. public void topic1() {

  27. 1
    go复制代码    `topicSender.send1();`
  28. }

  29. @Test

  30. public void topic2() {

  31. 1
    go复制代码    `topicSender.send2();`
  32. }

  33. @Test

  34. public void testFanout() {

  35. 1
    go复制代码    `fanoutSender.send();`
  36. }

  37. @Test

  38. public void testSimple() {

  39. 1
    go复制代码    `helloSender.send();`
  40. }

  41. }

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java学习之路:32Swing常用组件 一前言 二S

发表于 2021-08-28

“这是我参与8月更文挑战的第26天,活动详情查看: 8月更文挑战” juejin.cn/post/698796…

@[TOC]


一.前言

首先应该明白,用java创建一个窗体程序包括下面几步:

1.实例化JFrame对象,也就是创建一个窗体。
2.获取一个容器。
3.创建组件。
4.向容器添加组件。
5.使窗体可视。
6.设置窗体大小。
7.调用上面方法。

应该有这样的概念,Swing组件的窗体通常与组件和容器相关,所以在JFrame对象创建完成后,需要调用方法将窗体转换为容器,然后在容器中添加组件或设置布局管理器,通常,这个容器用来包含和显示组件。

类的继承图
可以看到JFrame继承于Container类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码import java.awt.*;
import javax.swing.*;
public class zujian extends JFrame{
public void windows() {
//实例化JFrame对象,就是创建一个窗体
JFrame jf =new JFrame("花狗");//设置窗体标题
//获取容器
Container c =jf.getContentPane();
////////////////////////////////

//下面所有组件的创建都将在这里编写,所有下面只列出组件创建的代码。
//如果有其他改动,我会列出。

////////////////////////////////
jf.setVisible(true);//设置窗体可视
jf.setSize(300,300);//设置窗体大小
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);//设置窗体关闭方式
}
public static void main(String[] args) {
new zujian().windows();
}
}

在这里插入图片描述

这是基础代码,创建一个空白窗体,为了文章不臃肿,我就只列出一次。关于java文档,可自行下载:
java文档


二.Swing常用组件

组件名称 定义
JButton 代表Swing按钮,按钮可以带一些图片或文字
JCheckBox 代表Swing中的复选框组件
JComboBox 代表Swing下拉列表框,可以在下拉显示区域显示多个选项
JFrame 代表Swing的框架类
JDialog 代表Swing版本的对话框
JLabel 代表Swing中的标签组件
JRadioButton 代表Swing单选按钮
JList 代表能够在用户界面中显示一系列条目的组件
JTextField 代表文本框
JPasswordField 代表密码框
JTextArea 代表Swing中的文本区域
JOptionPane 代表Swing中的一些对话框

每个组件都有很多种用法,建议查看java文档。


三.JButton | 代表Swing按钮

JButton类拥有5种构造方法:

构造方法 解释
JButton() 创建没有设置文本或图标的按钮
JButton(String text) 创建带有文本的按钮
JButton(Icon icon) 创建带有图标的按钮
JButton(String text, Icon icon) 创建带有初始文本和图标的按钮
JButton(Action a) 创建一个按钮,其属性从所提供的Action中获取

1.创建没有设置文本或图标的按钮

1
2
java复制代码 JButton jb=new JButton();
c.add(jb);

在这里插入图片描述 =200x)可以看到创建的按钮涉及整个窗体,这是为什么呢,窗体默认使用边界布局器,创建的组件会覆盖整个窗体,我们只需要取消边界布局,改用绝对布局:

1
2
3
4
java复制代码 jf.setLayout(null);  //告诉窗体取消边界布局
JButton jb=new JButton();
jb.setBounds(10,30,80,30);//使用绝对布局,自定义按钮大小
c.add(jb);

在这里插入图片描述 =200x)
如果这里有疑问可查看:
布局管理器

2.创建带有文本的按钮

1
2
3
4
5
java复制代码 JButton jb=new JButton("我是按钮");
jb.setBounds(50,50,100,100);//使用绝对布局,自定义按钮大小
c.add(jb);
jf.setVisible(true);
jf.setSize(300,300);

在这里插入图片描述 =200x)

3.创建带有图标的按钮

想使用带有图标的按钮,需要使用Icon接口:Icon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码import java.awt.*;
import javax.swing.*;
public class DrawIcon implements Icon {
private int width; //设置图标的宽
private int height; //设置图标的长
public int getIconHeight() {
return this.height;
}
public int getIconWidth() {
return this.width;
}
public DrawIcon(int width,int height) {
this.width=width;
this.height=height;
}
public void paintIcon(Component arg0,Graphics arg1,int x,int y) {
arg1.fillOval(x, y, width, height);//绘制一个圆形
}
public static void main(String[] args) {
DrawIcon icon =new DrawIcon(15,15);
JButton jl=new JButton(icon);
JFrame jf =new JFrame("花狗");
jf.setLayout(null); //告诉窗体取消边界布局
jl.setBounds(50,50,100,100);//使用绝对布局,自定义按钮大小
Container c=jf.getContentPane();
c.add(jl);
jf.setVisible(true);
jf.setSize(300,300);
}
}

在这里插入图片描述 =200x)

也可以使用图片作为图标来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码import java.awt.*;
import javax.swing.*;
import java.net.URL;
public class myicon extends JFrame{
public myicon() {
Container container =getContentPane();
//创建一个标签
JButton jb =new JButton();
setLayout(null); //告诉窗体取消边界布局
jb.setBounds(50,50,100,100);//使用绝对布局,自定义按钮大小
//获取图片所在的URL
URL url = myicon.class.getResource("/wo.PNG");
Icon icon =new ImageIcon(url);
jb.setIcon(icon);
//有两种添加方法,一种就是在创建的时候直接添加图标,第二种就是在后续使用方法添加
jb.setHorizontalAlignment(SwingConstants.CENTER);
jb.setOpaque(true);
container.add(jb);
setSize(400,300);
setVisible(true);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
new myicon();
}
}

在这里插入图片描述 =200x)
创建文本加图标的按钮,这里就不在赘述,至于最后一个构造方法使用Action接口的使用方法,这里不在讲解。


四.JCheckBox| 代表Swing中的复选框组件

JCheckBox类有8中构造方法:

构造方法 解释
JCheckBox() 创建一个最初未选中的复选框按钮,该按钮没有文本,也没有图标。
JCheckBox(Icon icon) 使用图标创建一个最初未选中的复选框。
JCheckBox(Icon icon, boolean selected) 创建带有图标的复选框,并指定是否最初选中它。
JCheckBox(String text) 用文本创建一个最初未选中的复选框。
JCheckBox(String text, boolean selected) 创建带有文本的复选框,并指定是否最初选中它。
JCheckBox(String text, Icon icon) 用指定的文本和图标创建一个最初未选中的复选框。
JCheckBox(String text, Icon icon, boolean selected) 创建带有文本和图标的复选框,并指定是否最初选中它。
JCheckBox(Action a) 创建一个复选框,其中的属性取自提供的操作。

1.创建一个最初未选中的复选框按钮,该按钮没有文本,也没有图标。

1
2
java复制代码JCheckBox jc =new JCheckBox();
jc.setBounds(50,50,100,100);//使用绝对布局,自定义大小

在这里插入图片描述 =200x)

2.用文本创建一个最初未选中的复选框。

1
java复制代码 JCheckBox  jl=new JCheckBox ("橘子");

在这里插入图片描述 =200x)

不常用的这里不再列出。


五.JComboBox | 代表Swing下拉列表框

JComboBox有4种构造方法:

构造方法 解释
JComboBox() 使用默认数据模型创建JComboBox。
JComboBox(ComboBoxModel aModel) 创建一个JComboBox,它从现有的ComboBoxModel中获取项目。
JComboBox(Object[] items) 创建一个JComboBox,其中包含指定数组中的元素。
JComboBox(Vector<?> items) 创建一个JComboBox,其中包含指定向量中的元素。

1.使用默认数据模型创建JComboBox。

1
2
java复制代码 JComboBox jc =new JComboBox();
jc.setBounds(50,50,80,30);//使用绝对布局,自定义大小

在这里插入图片描述 =200x)
可以看到创建了一个空白的下拉列表框,显然在日常应用中是没有用的,下面来添加选项:

2.创建一个JComboBox,它从现有的ComboBoxModel中获取项目。

1
2
3
4
5
6
java复制代码 JComboBox jc =new JComboBox();
jc.setBounds(50,50,100,30);//使用绝对布局,自定义大小
jc.addItem("--请选择--");
jc.addItem("身份证");
jc.addItem("驾驶证");
jc.addItem("学生证");

在这里插入图片描述 =200x)


六.JFrame |代表Swing的框架类

JFrame类有4种构造方法:

构造方法 解释
JFrame() 构造一个最初不可见的新框架。
JFrame(GraphicsConfiguration gc) 在屏幕设备的指定图形配置中创建一个框架和一个空白标题。
JFrame(String title) 使用指定的标题创建一个最初不可见的新框架。
JFrame(String title, GraphicsConfiguration gc) 创建具有指定标题和指定屏幕设备图形配置的JFrame。

1.使用指定的标题创建一个最初不可见的新框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码import java.awt.*;
import java.util.Vector;
import javax.swing.*;
public class zujian extends JFrame{
public void windows() {
//实例化JFrame对象,就是创建一个窗体
JFrame jf =new JFrame("花狗");//设置窗体标题
//获取容器
Container c =jf.getContentPane();
jf.setLayout(null); //告诉窗体取消边界布局
jf.setVisible(true);
jf.setSize(300,300);
}
public static void main(String[] args) {
new zujian().windows();
}
}

在这里插入图片描述 =200x)


七.JDialog | 代表Swing版本的对话框

JDialog有N种,确实有点多,只列出常见的:

构造方法 解释
JDialog() 创建没有标题和指定框架所有者的非模态对话框。
JDialog(Frame owner) 创建一个非模态对话框,指定框架为其所有者,标题为空。
JDialog(Frame owner, boolean modal) 以空标题和指定的模式和框架作为其所有者创建对话框。
JDialog(Frame owner, String title) 创建具有指定标题和指定所有者框架的非模态对话框。
JDialog(Frame owner, String title, boolean modal) 创建具有指定标题、所有者框架和模式的对话框。

1.创建具有指定标题、所有者框架和模式的对话框。

1
2
3
4
5
6
7
8
9
10
11
java复制代码import java.awt.*;
import javax.swing.*;
public class windowpr extends JDialog{
public windowpr(MyFrame frame) {
//实例化一个JDialog类对象,指定对话框的父窗体,窗体标题和类型
super(frame,"第一个JDialog窗体",true);
Container container =getContentPane();//创建一个容器
container.add(new JLabel("这是一个对话框"));//在容器中添加标签
setBounds(120,120,100,100);//设置对话框窗体大小
}
}

在这里插入图片描述 =200x)


八.JLabel| 代表Swing中的标签组件

JLabel有6种构造方法:

构造方法 解释
JLabel() 创建一个没有图像和标题为空字符串的JLabel实例。
JLabel(Icon image) 使用指定的图标创建JLabel实例。
JLabel(Icon image, int horizontalAlignment) 使用指定的图像和水平对齐方式创建JLabel实例。
JLabel(String text) 使用指定的文本创建JLabel实例。
JLabel(String text, Icon icon, int horizontalAlignment) 使用指定的文本、图像和水平对齐方式创建JLabel实例。
JLabel(String text, int horizontalAlignment) 使用指定的文本和水平对齐方式创建JLabel实例。

1.创建一个没有图像和标题为空字符串的JLabel实例。

1
java复制代码   JLabel jl=new JLabel();//创建一个标签

在这里插入图片描述 =200x)
这类标签没有上面实际作用,我们继续往下看:

2.使用指定的文本创建JLabel实例。

1
java复制代码JLabel jl=new JLabel("标签");//创建一个标签

在这里插入图片描述 =200x)

3.使用指定的图标创建JLabel实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码import java.awt.*;
import javax.swing.*;
public class DrawIcon implements Icon {
private int width; //设置图标的宽
private int height; //设置图标的长
public int getIconHeight() {
return this.height;
}
public int getIconWidth() {
return this.width;
}
public DrawIcon(int width,int height) {
this.width=width;
this.height=height;
}
public void paintIcon(Component arg0,Graphics arg1,int x,int y) {
arg1.fillOval(x, y, width, height);
}
public static void main(String[] args) {
DrawIcon icon =new DrawIcon(15,15);
JLabel jl=new JLabel ("标签",icon,SwingConstants.CENTER);
JFrame jf =new JFrame("花狗");
jf.setLayout(null); //告诉窗体取消边界布局
jl.setBounds(50,50,100,100);//使用绝对布局,自定义按钮大小
Container c=jf.getContentPane();
c.add(jl);
jf.setVisible(true);
jf.setSize(300,300);
}
}

在这里插入图片描述 =200x)


8.JRadioButton | 代表Swing单选按钮

构造方法 解释
JRadioButton() 创建一个没有设置文本的初始未选单选按钮。
JRadioButton(Action a) 创建一个radiobutton,其中的属性取自提供的操作。
JRadioButton(Icon icon) 创建一个最初未选择的单选按钮,该按钮具有指定的图像,但没有文本。
JRadioButton(Icon icon, boolean selected) 创建具有指定图像和选择状态但没有文本的单选按钮。
JRadioButton(String text) 用指定的文本创建未选中的单选按钮。
JRadioButton(String text, boolean selected) 创建具有指定文本和选择状态的单选按钮。
JRadioButton(String text, Icon icon) 创建一个单选按钮,该按钮具有指定的文本和图像,并且最初未被选中。
JRadioButton(String text, Icon icon, boolean selected) 创建具有指定文本、图像和选择状态的单选按钮。
  1. 创建一个没有设置文本的初始未选单选按钮。

1
java复制代码JRadioButton jr =new JRadioButton();

在这里插入图片描述 =200x)

2.用指定的文本创建未选中的单选按钮。

1
java复制代码JRadioButton jr =new JRadioButton("香蕉");

在这里插入图片描述 =200x)


9.JList| 代表能够在用户界面中显示一系列条目的组件

JList类有4个构造方法:

构造方法 解释
JList() 使用空的只读模型构造JList。
JList(E[] listData) 构造一个JList,用于显示指定数组中的元素。
JList(ListModel dataModel) 构造一个JList,用于显示来自指定的非空模型的元素。
JList(Vector<? extends E> listData) 构造一个JList,用于显示指定向量中的元素。

1.使用空的只读模型构造JList。

1
java复制代码JList jr =new JList();

在这里插入图片描述 =200x)和下拉列表框一样空白,没有什么实际价值。

2.构造一个JList,用于显示指定向量中的元素。

1
2
3
4
5
6
java复制代码 Vector vt =new Vector();
vt.add("红色");
vt.add("绿色");
vt.add("蓝色");
JList jr =new JList();
jr.setListData(vt);

在这里插入图片描述 =200x)
和下拉列表框不同的是该组件直接显示选项。


10.JTextField | 代表文本框

JTextField类有5种构造方法:

构造方法 解释
JTextField() 构造一个新TextField。
JTextField(Document doc, String text, int columns) 构造一个新的JTextField,该字段使用给定的文本存储模型和给定的列数。
JTextField(int columns) 构造具有指定列数的新空TextField。
JTextField(String text) 构造一个用指定文本初始化的新TextField。
JTextField(String text, int columns) 构造一个用指定的文本和列初始化的新TextField。

1.构造一个用指定文本初始化的新TextField。

1
2
java复制代码 String str="大家好,我是花狗。";
JTextField jr =new JTextField(str);

在这里插入图片描述 =200x)


11.JPasswordField | 代表密码框

JPasswordField类有5种构造方法:

构造方法 解释
JPasswordField() 构造一个新的JPasswordField,其中包含一个默认文档、null起始文本字符串和0列宽度。
JPasswordField(Document doc, String txt, int columns) 构造一个新的JPasswordField,该字段使用给定的文本存储模型和给定的列数。
JPasswordField(int columns) 使用指定的列数构造新的空JPasswordField。
JPasswordField(String text) 构造一个用指定文本初始化的新JPasswordField。
JPasswordField(String text, int columns) 构造一个用指定文本和列初始化的新JPasswordField。

1.构造一个新的JPasswordField

1
java复制代码 JPasswordField   jr =new JPasswordField  ();

在这里插入图片描述 =200x)
可以通过该方法修改回显字符:

1
java复制代码jr.setEchoChar('#');

在这里插入图片描述 =200x)


12.JTextArea | 代表Swing中的文本区域

JTextArea类有有6种构造方法:

构造方法 解释
JTextArea() 构造一个新的文本区域。
JTextArea(Document doc) 使用给定的文档模型构造一个新的JTextArea,并默认为所有其他参数(null, 0,0)。
JTextArea(Document doc, String text, int rows, int columns) 用指定的行数和列数以及给定的模型构造一个新的JTextArea。
JTextArea(int rows, int columns) 用指定的行数和列数构造新的空TextArea。
JTextArea(String text) 构造一个显示指定文本的新文本区域。
JTextArea(String text, int rows, int columns) 用指定的文本和行与列的数目构造一个新TextArea。

1.构造一个新的文本区域。

1
java复制代码JTextArea   jr =new JTextArea  ();

与文本框不同的是这个可以输入内容:
在这里插入图片描述 =200x)
也可以提前显示一些内容,就好比一些软件搜索框默认热门内容:

2.构造一个显示指定文本的新文本区域。

在这里插入图片描述 =200x)


13.JOptionPane | 代表Swing中的一些对话框

JOptionPane类有7种构造方法:

构造方法 解释
JOptionPane() 使用测试消息创建JOptionPane。
JOptionPane(Object message) 创建JOptionPane实例,以显示使用纯消息消息类型和UI提供的默认选项的消息。
JOptionPane(Object message, int messageType) 创建JOptionPane实例,以显示具有指定消息类型和默认选项的消息。
JOptionPane(Object message, int messageType, int optionType) 创建JOptionPane实例,以显示具有指定消息类型和选项的消息。
JOptionPane(Object message, int messageType, int optionType, Icon icon) 创建JOptionPane实例,以显示具有指定消息类型、选项和图标的消息。
JOptionPane(Object message, int messageType, int optionType, Icon icon, Object[] options) 创建JOptionPane实例,以显示具有指定消息类型、图标和选项的消息。
JOptionPane(Object message, int messageType, int optionType, Icon icon, Object[] options, Object initialValue) 创建JOptionPane实例,以显示具有指定消息类型、图标和选项的消息,并指定初始选择的选项。
1
2
java复制代码 JOptionPane jo =new JOptionPane();
jo.showMessageDialog(null, "普通提示框");

在这里插入图片描述


若有错误,欢迎指正,欢迎评论。
==每文一句:奋斗不是简单的吃苦,奋斗是一种心态。一种做任何事情,只要有一丝可能,就愿意全心全意去投入和尝试的心态。奋斗的心态会调用尽你前半生所有的智慧、资源、技巧和情商去面对问题,还会把你以前积累上的不足,以最直接的方式暴露在你前面。让你知道自己有多强,也让你知道自己有多弱。==

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

GitHub 标星 143K+!一款开源替代 ls 的工具

发表于 2021-08-28

这是我参与 8 月更文挑战的第 28 天,活动详情查看: 8月更文挑战

一名致力于在技术道路上的终身学习者、实践者、分享者,一位忙起来又偶尔偷懒的原创博主,一个偶尔无聊又偶尔幽默的少年。

欢迎各位掘友们微信搜索「杰哥的IT之旅」关注!

原文链接:GitHub 标星 14.3K+!一款开源替代 ls 的工具你值得拥有!

前言

提到ls命令,大家都不陌生,在Linux环境下,其主要作用:列出当前目录下所包含的文件及子目录,如果当前目录下文件过多,则使用命令ls不是很好,因为这输出出来的结果跟你所要查找的文件未能达成一致,第一: 需要进行二次过滤查找;第二: 文件过多时,终端输出结果较慢;

那么,今天杰哥给大家介绍一款 GitHub 开源的替代 ls 命令的工具。

一、EXA 是什么?

EXA 是 Unix 和 Linux 操作系统附带的命令行程序的 ls 现代替代品。

二、EXA 与 LS 有什么不同之处?

  • 终端屏幕不再是瓶颈:exa 功能强大,比 ls 要友好,输出结果会更快;
  • 颜色:exa 大量使用颜色;
  • 不与 ls 命令冲突

三、EXA 的特征

  • 色彩:文件类型列表以及如何区分文件;
  • 网格视图:在与终端窗口大小相同的网格中显示文件;
  • 长远:在表中显示文件及其元数据;
  • 树视图:在树中显示文件及其子代;
  • Git 集成:在存储库中时,查看文件的 Git 状态;
  • 筛选:隐藏列表中的文件,显示不可见的文件,并对输出进行排序;
  • Xattrs:列出文件的扩展属性;

GitHub 地址:github.com/ogham/exa

image.png

四、安装 Rust

exa 是基于 Rust 语言实现的,安装前需检查您的操作系统是否支持Rustc 1.17.0或更高版本,推荐安装 Rust 的方法是从官方下载页面,网址:https://www.rust-lang.org/tools/install设置好之后,通过make install将编译exa并将其安装到/usr/local/bin中。

开始使用Rust的方法是必须要安装 Rustup,它可以下载并进行安装Rust,并管理这两个组件:Cargo package manager(Cargo包管理器)和 build tool(构建工具)的更新。

五、安装 EXA

exa是一个独立的二进制文件,不需要任何依赖关系或特殊权限,如果操作系统有可用的软件包,那么可使用软件包管理器自动处理下载和验证以及安装手册页和Shell补全文件,如果没有,可以手动安装exa也可编译一个副本。

  • MacOS

在MacOS上安装exa是使用Homebrew,Homebrew软件包管理器提供了exa二进制文件,也可以手动安装。

传送门:brew.sh/

1
php复制代码$ brew install exa

如果您的Linux发行版包含exa软件包,则可以从命令行安装它:

  • Arch
1
php复制代码$ sudo pacman -S exa
  • Fedora
1
php复制代码$ sudo dnf install exa
  • openSUSE
1
php复制代码$ sudo zypper install exa
  • 从源代码构建 EXA

EXA 是开源的,可根据自己的需求构建版本,通过源代码构建,同样也是先按照上面的步骤,安装 Rust,EXA 是基于 Rust 编写的,在执行其他任何操作之前,必须先安装 Rust 编译器。

安装其他依赖项

EXA 需要完成两项依赖项:libgit2(Git 库)和 CMake(在 Rust 中使用 libgit2 的构建系统)。

exa的某些功能取决于libgit2,若无法编译libgit2,则可以通过运行退出Git支持

1
arduino复制代码cargo build --release --no-default-features

如编译musl,并且要使Git功能正常工作,则需要使用标记vendored-openssl:

1
css复制代码cargo build --release --target=x86_64-unknown-linux-musl --features vendored-openssl,git

获取 EXA 源

1
shell复制代码$ git clone https://github.com/ogham/exa.git

获取代码并设置环境变量后,即可使用 Cargo 构建 exa

1
arduino复制代码$ cargo build --release

一个可运行的二进制文件将出现在target/release目录中,可通过该二进制文件复制到您的目录中来永久安装 exa $PATH。

采用 Cargo 安装

如果安装了最新版本的Cargo(至少是Cargo v0.5.0),则可以使用它来安装exa

1
php复制代码$ cargo install exa

Cargo构建exa二进制文件将放置在$HOME/.cargo目录中,您必须将其放置在目录中$PATH,也可以通过将--root选项参数来给Cargo来覆盖此位置。

如使用的 Linux 发行版不支持通过以上方式安装,可通过官方 GitHub 仓库Releases页面下载编译好的二进制版本进行使用。

github.com/ogham/exa/r…

下载对应的二进制版本并进行解压安装到指定目录下

1
2
3
shell复制代码# wget https://github.com/ogham/exa/releases/download/v0.9.0/exa-linux-x86_64-0.9.0.zip
# unzip exa-linux-x86_64-0.9.0.zip
# mv exa-linux-x86_64 /usr/local/bin

下载并安装对应的 MAN 手册

1
2
shell复制代码# wget https://raw.githubusercontent.com/ogham/exa/master/contrib/man/exa.1
# mv exa.1 /usr/share/man/man1

六、使用 EXA

EXA 的使用方法基本和 ls 一样,也是一个命令行程序,打开终端,使用 EXA 的选项参数即可运行命令,查询出来的结果并进行打印。

基本语法格式:

1
css复制代码$ exa [OPTIONS] [FILES]

清单文件

EXA 的主要内容是:清单文件,将传递着所需要的文件作为参数以及一些文件的选项。

使用-l选项可通过长视图显示文件及其元数据。

1
php复制代码$ exa -l

递归

EXA 可以递归到目录中并列出目录下每个子目录的内容,可使用-R或--recurse命,递归时,EXA 将为原始目录下发现的任何子目录提供单独的列表。

使用-R选项将列出目录及其包含的子目录。

1
php复制代码$ exa -R

除上述举例说的两个参数选项以外,就不一一描述了,大家可以看如下图片中的参数选项或者在官网上探索更多参数的用法。

image.png

image.png

image.png

传送门网址

1、命令行参数选项
the.exa.website/docs/comman…

2、GitHub 地址
github.com/ogham/exa

3、releases 页面地址
github.com/ogham/exa/r…

4、环境变量地址
the.exa.website/docs/enviro…

5、颜色主题地址
the.exa.website/docs/colour…

本文完。


原创不易,如果你觉得这篇文章对你有点用的话,麻烦你为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

对了,掘友们记得给我点个免费的关注哟!防止你迷路下次就找不到我了。

我们下期再见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RabbitMQ 3811 集群搭建(docker-co

发表于 2021-08-28

RabbitMQ 3.8.11 集群搭建 虚拟机方式,很繁琐。
可以将其进行docker化。

  1. 创建docker-compose.yml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
yml复制代码version: '3'
services:
rabbitmq1:
image: rabbitmq:3.8.11-management
container_name: rabbitmq1
restart: always
hostname: rabbitmq1
ports:
- 15683:15672
- 5683:5672
volumes:
- ./data1:/var/lib/rabbitmq
- ./rabbitmq.sh:/home/rabbitmq.sh
- ./etc/hosts:/etc/hosts
- ./etc/rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins
environment:
- RABBITMQ_DEFAULT_USER=root
- RABBITMQ_DEFAULT_PASS=root
- STOMP_DEFAULT_USER=root
- STOMP_DEFAULT_PASS=root
- RABBITMQ_ERLANG_COOKIE=CURIOAPPLICATION
networks:
rabbit:
ipv4_address: 192.168.1.127

rabbitmq2:
image: rabbitmq:3.8.11-management
container_name: rabbitmq2
restart: always
hostname: rabbitmq2
ports:
- 15684:15672
- 5684:5672
volumes:
- ./data2:/var/lib/rabbitmq
- ./rabbitmq.sh:/home/rabbitmq.sh
- ./etc/hosts:/etc/hosts
- ./etc/rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins
environment:
- RABBITMQ_DEFAULT_USER=root
- RABBITMQ_DEFAULT_PASS=root
- STOMP_DEFAULT_USER=root
- STOMP_DEFAULT_PASS=root
- RABBITMQ_ERLANG_COOKIE=CURIOAPPLICATION
networks:
rabbit:
ipv4_address: 192.168.1.128

rabbitmq3:
image: rabbitmq:3.8.11-management
container_name: rabbitmq3
restart: always
hostname: rabbitmq3
ports:
- 15692:15672
- 5692:5672
volumes:
- ./data3:/var/lib/rabbitmq
- ./rabbitmq.sh:/home/rabbitmq.sh
- ./etc/hosts:/etc/hosts
- ./etc/rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins
environment:
- RABBITMQ_DEFAULT_USER=root
- RABBITMQ_DEFAULT_PASS=root
- STOMP_DEFAULT_USER=root
- STOMP_DEFAULT_PASS=root
- RABBITMQ_ERLANG_COOKIE=CURIOAPPLICATION
networks:
rabbit:
ipv4_address: 192.168.1.129

networks:
rabbit:
external:
name: rabbitmqnet
  1. 启动使用插件(rabbitmq_stomp)

./etc/rabbitmq/enabled_plugins

1
csharp复制代码[rabbitmq_management,rabbitmq_prometheus,rabbitmq_stomp].
  1. 创建rabbitmq.sh

1
2
3
4
sh复制代码rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbitmq@rabbitmq1
rabbitmqctl start_app
  1. 创建主机列表

./etc/hosts

1
2
3
复制代码192.168.1.127 rabbitmq1
192.168.1.128 rabbitmq2
192.168.1.129 rabbitmq3
  1. 创建依赖网络

1
cmd复制代码# docker network create --subnet 192.168.1.1/24 rabbitmqnet
  1. 开启rabbitMQ集群

1
cmd复制代码# docker-compose up -d
  1. 打开监控平台

image.png

QA 问题总结

Q1. 解决 RabbitMQ 集群 Channel shutdown: connection error 错误(HAProxy 负载均衡)

A1. HAProxy 配置的timeout client超时时间,OS的tcp_keepalive_time(系统tcp_keepalive_time发送TCP keepalive数据包间隔时间是 2 个小时)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot多租户业务的多数据源动态切换解决方案

发表于 2021-08-28

数据源切换方法

Springboot提供了AbstractRoutingDataSource抽象类,类名意思是数据源路由,让用户可以选择根据需要切换当前数据源

该类提供了一个抽象方法determineCurrentLookupKey(), 切换数据源时springboot会调用这个方法,所以只需要实现该方法,在该方法中返回需要切换的数据源名称即可

源码解读

1.从类关系图中可以看出AbstractRoutingDataSource类实现的是DataSource接口(非最底层),
其要求实现一个方法getConnection(),即获取DB连接

image.png

2.AbstractRoutingDataSource实现了这两个方法

image.png

其中determineTargetDataSource()调用determineCurrentLookupKey()方法,取到当前设定的查找键,通过查找键在上下文this.resolvedDataSources属性中尝试获取DataSource对象,这个对象即当前连接的数据源

image.png

3.那么this.resolvedDataSources在哪里维护?
AbstractRoutingDataSource类实现了InitializingBean类的afterPropertiesSet()方法,
在bean的所有属性设置完成后变会调用此方法,可以看到this.resolvedDataSources从this.targetDataSources取的信息;

image.png

所以只需要改变this.targetDataSources,并且触发afterPropertiesSet(),即可改变this.resolvedDataSources;后续改变determineCurrentLookupKey()的返回值(key),在调用getConnection()时即可获取到指定的数据源

多租户业务背景

多租户业务场景下,往往每个租户都独立一个数据库(是否独立数据源实例根据实际需要处理),每个租户的数据在数据库层面先做了隔离,在开展详细业务编写时就可以不用考虑不同租户的数据会混淆。但是随之而来的就是数据源灵活切换的需求,需要封装一套方法,在业务编写时可以根据提供的租户代码便捷的切换到对应的数据源

提供的切换方式

1.注解方式切换

提供一个注解,可以根据租户代码切换,也可以根据配置文件中写定的数据源名称切换

2.直接调用方法方式切换

提供一个租户rds切换类,在编写业务代码时调用方法切换,该方式可以让租户代码以变量形式传递,无需提前知道

实现步骤概要

1.添加pom依赖、配置数据源信息

2.编写数据源配置类,将数据源配置信息注入到容器

3.编写DynamicDataSource类继承AbstractRoutingDataSource抽象类,维护当前数据源信息,提供切换方法

4.编写租户rds切换类,业务切换数据源时统一调用此类

5.编写自定义注解

6.编写切面类,将连接点直接设定在编写的自定义注解上,根据参数等调用rds切换类切换数据源

7.异常类、异常枚举类,规范异常抛出

详细步骤

1.pom依赖添加、配置数据源信息

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<dependencies>
<!-- mysql ps:由于连接的数据库是5.6所以用较老的包,读者可以根据数据库版本选择 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
<scope>runtime</scope>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
yaml复制代码# 主配置
spring:
# 数据源配置
datasource:
# 修改数据源为druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver #这个要根据mysql-connector-java版本
# druid配置
druid:
# 主数据源
master:
driver-class-name: com.mysql.jdbc.Driver
# 默认数据库连接(配置库)
url: jdbc:mysql://xxx:xxx/config?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: xxx
password: xxx
# 递增db配置
db1:
driver-class-name: com.mysql.jdbc.Driver #这个要根据mysql-connector-java版本
url: jdbc:mysql://xxx:xxx/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: xxx
password: xxx
initial-size: 5 # 初始化时建立物理连接的个数
max-active: 30 # 最大连接池数量
min-idle: 5 # 最小连接池数量
max-wait: 60000 # 获取连接时最大等待时间,单位毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 # 连接保持空闲而不被驱逐的最小时间
validation-query: SELECT 1 FROM DUAL # 用来检测连接是否有效的sql,要求是一个查询语句
test-while-idle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-on-borrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-return: false # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
pool-prepared-statements: true # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
max-pool-prepared-statement-per-connection-size: 50 # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
filters: stat,wall,log4j2 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计;配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
use-global-data-source-stat: true # 合并多个DruidDataSource的监控数据
stat-view-servlet:
allow: '' # IP白名单(没有配置或者为空,则允许所有访问) allow: 127.0.0.1,192.168.163.1
deny: '' # IP黑名单 (存在共同时,deny优先于allow)
login-password: xxxxxx # 登录密码
login-username: admin # 登录名
reset-enable: false # 禁用HTML页面上的“Reset All”功能
url-pattern: /druid/* # 配置DruidStatViewServlet
web-stat-filter: # 配置DruidStatFilter
enabled: true
exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*'
url-pattern: /*

1.配置里面包含了一些druid的配置,可以根据业务需要自行配置

2.其中,spring.datasource.druid.master为主数据源,也是配置库数据源,租户库数据源连接信息会在配置库中获取,spring.datasource.druid.db1为递增数据源,db1可以命名为具体的业务库名称,这里仅仅方便理解取名为db1


2.编写数据源配置类,将数据源配置信息注入到容器

数据源配置类DataSourceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) // 排除 DataSourceAutoConfiguration 的自动配置,避免环形调用
public class DataSourceConfig {
/**
* 默认数据源
*
* @return
*/
@Bean(DataSourceConstant.DATA_SOURCE_MASTER)
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource dataSourceMaster() {
return DruidDataSourceBuilder.create().build();
}

/**
* 递增数据源
*
* @return
*/
@Bean(DataSourceConstant.DATA_SOURCE_DB_1)
@ConfigurationProperties("spring.datasource.druid.db1")
public DataSource dataSourceDb1() {
return DruidDataSourceBuilder.create().build();
}


/**
* 设置动态数据源为主数据源
*
* @return
*/
@Bean
@Primary
public DynamicDataSource dataSource() {
// 将数据源设置进map
DynamicDataSource.setDataSourceMap(DataSourceConstant.DATA_SOURCE_MASTER, dataSourceMaster());
DynamicDataSource.setDataSourceMap(DataSourceConstant.DATA_SOURCE_DB_1, dataSourceDb1());
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 使用 Map 保存多个数据源,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上
dynamicDataSource.setTargetDataSources(DynamicDataSource.dataSourceMap);
return dynamicDataSource;
}
}

数据源常量类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class DataSourceConstant {
private DataSourceConstant() {
}

/**
* 这里的命名统一在配置文件命名的基础上加dataSource前缀且改小驼峰
* 默认数据源名称
*/
public static final String DATA_SOURCE_MASTER = "dataSourceMaster";

/**
* 递增可配数据源名称
* 这里的命名统一在配置文件命名的基础上加dataSource前缀且改小驼峰
* 后面可接着 db2... dbn 也可以根据
*/
public static final String DATA_SOURCE_DB_1 = "dataSourceDb1";
}

此处先往DynamicDataSource.dataSourceMap将两个配置好的数据源连接信息写入,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上


3.编写DynamicDataSource类继承AbstractRoutingDataSource抽象类,维护当前数据源信息,提供切换方法

动态数据源类DynamicDataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
java复制代码public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 存储当前线程的数据源key
*/
private static final ThreadLocal<String> DATA_SOURCE_KEY = ThreadLocal.withInitial(() -> DataSourceConstant.DATA_SOURCE_MASTER);

/**
* 数据源map
*/
public static Map<Object, Object> dataSourceMap = new ConcurrentHashMap<>(1000);

/**
* 获取数据源key
*
* @return
*/
public static String getDataSourceKey() {
return DynamicDataSource.DATA_SOURCE_KEY.get();
}

/**
* 设置数据源key
*
* @param key
*/
public static void setDataSourceKey(String key) {
DynamicDataSource.DATA_SOURCE_KEY.set(key);
}

/**
* 移除默认数据源key
*/
public static void remove() {
DynamicDataSource.DATA_SOURCE_KEY.remove();
}

/**
* 切换成默认的数据源
*/
public static void setDataSourceDefault() {
setDataSource(DataSourceConstant.DATA_SOURCE_MASTER);
}

/**
* 切换成指定数据源 前提是dataSourceMap中有该key
* 外层调用时需要判断下map是否有,可靠性交给外层维护
*
* @param dataSource
*/
public static void setDataSource(String dataSource) {
setDataSourceKey(dataSource);
// InitializingBean.afterPropertiesSet()是,实例化后,bean的所有属性初始化后调用;但是如果该bean是直接从容器中拿的,并不需要实例化动作
// 这里直接拿到dataSource,手动触发一下,让AbstractRoutingDataSource.resolvedDataSources重新赋值,取到本类维护的map的值
DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtil.getBean("dataSource");
dynamicDataSource.afterPropertiesSet();
}

/**
* 获取租户数据源配置
*
* @param tenantCode
* @return
*/
public static Object getDataSourceMap(String tenantCode) {
return DynamicDataSource.dataSourceMap.get(tenantCode);
}

/**
* 设置map
*
* @param dataSourceName
* @return void
* @author Linzs
* @date 2021/8/28 11:53
**/
public static void setDataSourceMap(String dataSourceName, Object dataSource) {
dataSourceMap.put(dataSourceName, dataSource);
}

/**
* 设置map
*
* @param dataSourceName
* @return void
* @author Linzs
* @date 2021/8/28 11:53
**/
public static void setDataSourceMap(String dataSourceName) {
dataSourceMap.put(dataSourceName, SpringContextUtil.getBean(dataSourceName));
}

/**
* 设置租户数据源配置
*
* @param rdsConfig
* @return
*/
public static void setDataSourceMap(RdsConfig rdsConfig) {
DynamicDataSource.dataSourceMap.put(rdsConfig.getTenantCode(), getDruidDataSource(rdsConfig));
}

/**
* 获取DruidDataSource
*
* @param rdsConfig
* @return
*/
private static DruidDataSource getDruidDataSource(RdsConfig rdsConfig) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://" + rdsConfig.getDbUrl() + ":" + rdsConfig.getDbPort() + "/" + rdsConfig.getDbName() + "?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=true&autoReconnect=true&serverTimezone=Asia/Shanghai");
druidDataSource.setUsername(rdsConfig.getDbAccount());
druidDataSource.setPassword(rdsConfig.getDbPassword());
return druidDataSource;
}

/**
* 重写determineCurrentLookupKey方法
*
* @return java.lang.Object
* @date 2021/8/28 12:14
**/
@Override
protected Object determineCurrentLookupKey() {
return getDataSourceKey();
}
}

  1. 维护了一个key,用于表示当前用的是哪个数据源
  2. 维护了一个map,用于springboot获取数据源信息
  3. 重写determineCurrentLookupKey方法,可参照上面源码解读理解

4.编写租户rds切换类,业务切换数据源时统一调用此类

RdsConfig类,该javabean描述rds的连接信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码@Data
public class RdsConfig implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 租户编码
*/
private String tenantCode;

/**
* 数据库URL
*/
private String dbUrl;

/**
* 数据库端口
*/
private String dbPort;

/**
* 数据库名称
*/
private String dbName;

/**
* 数据库账号
*/
private String dbAccount;

/**
* 数据库密码
*/
private String dbPassword;
}

具体rds切换服务类:TenantRdsServiceImpl类,实现TenantRdsService接口,这里不贴出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
java复制代码@Service
@Slf4j
public class TenantRdsServiceImpl implements TenantRdsService {
@Autowired
private TenantMapper tenantMapper;

@Autowired
private RdsMapper rdsMapper;

/**
* 获取rds配置
*
* @param tenantCode
* @date 2021/8/28 13:53
**/
@Override
public RdsConfig getRdsConfig(String tenantCode) {
// 根据租户代码取租户表
Tenant tenant = tenantMapper.selectByTenantCode(tenantCode);
if (null == tenant) {
return null;
}
// 取rds表
Rds rds = rdsMapper.selectByPrimaryKey(tenant.getRdsId());
if (null == rds) {
return null;
}
// 转换为rds配置
RdsConfig rdsConfig = new RdsConfig();
rdsConfig.setDbUrl(rds.getHost());
rdsConfig.setTenantCode(tenantCode);
rdsConfig.setDbName(tenant.getDbName());
rdsConfig.setDbAccount(rds.getAccount());
rdsConfig.setDbPassword(rds.getPwd());
rdsConfig.setDbPort(String.valueOf(rds.getPort()));
return rdsConfig;
}

/**
* 根据租户代码切换rds连接,同一个线程内rds配置只会查一次
*
* @param tenantCode
* @date 2021/8/28 13:16
**/
@Override
public void switchRds(String tenantCode) {
if (StringUtils.isBlank(tenantCode)) {
throw new TenantCodeIsBlankException();
}
// 如果当前已是这个租户rds则直接返回
if (tenantCode.equals(DynamicDataSource.getDataSourceKey())) {
return;
}
// 如果本地已有则不查了 改rds需要重启服务
if (null == DynamicDataSource.getDataSourceMap(tenantCode)) {
// 如果当前不是配置库则先切回配置库
if (!DataSourceConstant.DATA_SOURCE_MASTER.equals(DynamicDataSource.getDataSourceKey())) {
DynamicDataSource.setDataSourceDefault();
}
// 获取rds配置
RdsConfig rdsConfig = getRdsConfig(tenantCode);
if (null == rdsConfig) {
throw new RdsNotFoundException();
}
DynamicDataSource.setDataSourceMap(rdsConfig);
}
// 切换到业务库
DynamicDataSource.setDataSource(tenantCode);
}

/**
* 根据数据源名称切换rds连接,同一个线程内rds配置只会查一次
*
* @param dataSourceName
* @date 2021/8/28 13:16
**/
@Override
public void switchRdsByDataSourceName(String dataSourceName) {
if (StringUtils.isBlank(dataSourceName)) {
throw new DataSourceNameIsEmptyException();
}
// 如果当前已是这个数据源直接返回
if (dataSourceName.equals(DynamicDataSource.getDataSourceKey())) {
return;
}
// 如果本地已有则不查了 改rds需要重启服务
if (null == DynamicDataSource.getDataSourceMap(dataSourceName)) {
throw new DataSourceNotExistException();
}
// 切换
DynamicDataSource.setDataSource(dataSourceName);
}
}

1.这里用到了两张表,一张是租户表(tenant)用于存储租户代码与rds的对应关系,另一张是DB连接信息(rds)表,用于存储数据源连接信息,具体的mapper和javabean的代码这里就不贴出来了,根据需求建表具体实现即可

2.提供了三个方法分别是根据租户代码获取rds连接信息,根据租户代码切换rds,根据数据源名称切换rds,切换方法中对当前连接信息做了判断,不会重复切换,也不会重复查配置库获取rds信息


5.编写自定义注解

自定义注解 @SwitchMasterRds

1
2
3
4
5
6
7
8
java复制代码/**
* 切换至主数据源-自定义注解
* 这个仅为了方便使用,用SwitchRds注解指定为默认数据源也可以实现
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchMasterRds {
}

自定义注解 @SwitchRds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* 切换数据源-自定义注解
*/
// 注解作用目标;ElementType.METHOD表示该注解会用在方法上;ElementType.TYPE表示该注解会用在类,接口,枚举;
@Target({ElementType.METHOD, ElementType.TYPE})
// 注解策略属性;RetentionPolicy.RUNTIME表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchRds {
/**
* 根据数据源bean切换数据源
* 此处可以切换的数据源在 DataSourceConfig 配置类中
* 同时指定了tenantCode则这个优先
*/
String dataSource() default "";

/**
* 动态切换-根据租户代码切换数据源
*/
String tenantCode() default "";
}

1.SwitchRds注解既可以用租户代码切换rds,又可以使用数据源名称切换

2.SwitchMasterRds注解是为了方便切换成主数据源而添加的


6.编写切面类

SwitchMasterRds注解的切面类SwitchMasterRdsAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码@Aspect
@Component
@Slf4j
public class SwitchMasterRdsAspect {
/**
* 租户rds服务类
*/
@Autowired
private TenantRdsService tenantRdsServiceImpl;

/**
* 切点
* 连接点:直接指定为注解
* 注意:com.xxx.SwitchMasterRds这里包名自行修改
* @date 2021/8/27 14:26
**/
@Pointcut("@annotation(com.xxx.SwitchMasterRds)")
public void myPointcut() {
}

/**
* 环绕通知
*
* @return java.lang.Object
* @date 2021/8/27 14:26
**/
@Around(value = "myPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object proceed;
try {
tenantRdsServiceImpl.switchRdsByDataSourceName(DataSourceConstant.DATA_SOURCE_MASTER);
// 执行
proceed = pjp.proceed();
} finally {
// todo 这里需要做移除切换的数据源也可以,但是如果没移除再下次切换的时候会先切换到配置库
}
return proceed;
}
}

SwitchRds注解的切面类SwitchRdsAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码@Aspect
@Component
@Slf4j
public class SwitchRdsAspect {
/**
* 租户rds服务类
*/
@Autowired
private TenantRdsService tenantRdsServiceImpl;

/**
* 切点
* 连接点:直接指定为注解
* 注意:com.xxx.SwitchRds这里包名自行修改
* @date 2021/8/27 14:26
**/
@Pointcut("@annotation(com.xxx.SwitchRds)")
public void myPointcut() {
}

/**
* 环绕通知
*
* @return java.lang.Object
* @date 2021/8/27 14:26
**/
@Around(value = "myPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
SwitchRds annotation = getAnnotation(pjp);
// 获取注解上的租户代码
String tenantCode = annotation.tenantCode();
String dataSource = annotation.dataSource();
Object proceed;
try {
if (StringUtils.isNotBlank(dataSource)) {
tenantRdsServiceImpl.switchRdsByDataSourceName(dataSource);
} else if (StringUtils.isNotBlank(tenantCode)) {
tenantRdsServiceImpl.switchRds(tenantCode);
} else {
throw new DataSourceSwitchFailException();
}
// 执行
proceed = pjp.proceed();
} finally {
// todo 这里需要做移除切换的数据源也可以,但是如果没移除再下次切换的时候会先切换到配置库
}
return proceed;
}

/**
* 获取注解
*
* @param pjp
* @date 2021/8/27 17:58
**/
private SwitchRds getAnnotation(ProceedingJoinPoint pjp) {
// 尝试获取类上的注解
SwitchRds annotation = pjp.getTarget().getClass().getAnnotation(SwitchRds.class);
// 如果类上没有注解则获取方法上面的
if (null == annotation) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
annotation = methodSignature.getMethod().getAnnotation(SwitchRds.class);
}
return annotation;
}

}

这里将连接点直接设定在编写的自定义注解上,根据参数等调用rds切换类切换数据源


7.异常类、异常枚举类

ErrorInfo接口,规范异常枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface ErrorInfo {
/**
* 异常码
* @return int
*/
int code();

/**
* 异常描述
* @return String
*/
String message();
}

处理异常枚举类,将所有错误类型以及错误代码枚举出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
java复制代码/**
* 处理异常枚举类
*/
public enum HandleExceptionEnum implements ErrorInfo {
/**
* 待处理
*/
WAIT(0, "待处理"),

/**
* 成功
*/
SUCCESS(10, "SUCCESS"),

/**
* 程序错误
*/
ERROR(100, "程序错误"),


/**
* 公共 - rds配置未取到
*/
C_GENERATE_RDS_NOT_FOUND(1001, "rds配置未取到"),

/**
* 公共 - 租户代码为空
*/
C_GENERATE_TENANT_CODE_IS_BLANK(1002, "租户代码为空"),

/**
* 公共 - 数据源配置不存在
*/
C_GENERATE_DATA_SOURCE_NOT_EXIST(1003, "数据源配置不存在"),

/**
* 公共 - 数据源名称为空
*/
C_GENERATE_DATA_SOURCE_NAME_IS_EMPTY(1004, "数据源名称为空"),

/**
* 公共 - 数据源名称为空
*/
C_GENERATE_DATA_SOURCE_SWITCH_FAIL(1005, "数据源切换失败"),


// ------------------------------------------------------------------

;

/**
* 编码
*/
private final int code;

/**
* 信息
*/
private final String message;

HandleExceptionEnum(int code, String message) {
this.code = code;
this.message = message;
}

@Override
public int code() {
return code;
}

@Override
public String message() {
return message;
}

/**
* code转换成enum
*
* @param code 错误码
* @return HandleExceptionEnum
*/
public static HandleExceptionEnum codeOf(int code) {
for (HandleExceptionEnum item : HandleExceptionEnum.values()) {
if (item.code() == code) {
return item;
}
}
return null;
}

/**
* 指定code是否在枚举之内
*
* @param code 错误码
* @return boolean
*/
public static boolean contain(int code) {
for (HandleExceptionEnum item : HandleExceptionEnum.values()) {
if (item.code() == code) {
return true;
}
}
return false;
}
}

处理异常基类,所有的处理异常全部继承该类,其保存ErrorInfo信息,可以方便获取错误代码等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码/**
* HandlerException
*/
public class HandlerException extends RuntimeException {
/**
* 异常信息
*/
private final ErrorInfo errorInfo;

/**
* 无参构造方法默认为程序错误
*/
public HandlerException() {
super(HandleExceptionEnum.ERROR.message());
this.errorInfo = HandleExceptionEnum.ERROR;
}

public HandlerException(HandleExceptionEnum handleExceptionEnum) {
super(handleExceptionEnum.message());
this.errorInfo = handleExceptionEnum;
}

public HandlerException(HandleExceptionEnum handleExceptionEnum, String message) {
super(message);
this.errorInfo = handleExceptionEnum;
}

/**
* 根据异常类型获取code
*
* @param e
* @return int
*/
public static int getCode(Exception e){
return e instanceof HandlerException ? ((HandlerException) e).getErrorInfo().code() : HandleExceptionEnum.ERROR.code();
}

/**
* 获取异常信息
*
* @return ErrorInfo
*/
public ErrorInfo getErrorInfo() {
return errorInfo;
}
}

具体的异常类,直接继承处理异常基类,文中主动抛出的异常全是这样方式编写,这里就不一一列举了

1
2
3
4
5
6
7
8
java复制代码/**
* rds配置未取到
*/
public class RdsNotFoundException extends HandlerException {
public RdsNotFoundException() {
super(HandleExceptionEnum.C_GENERATE_RDS_NOT_FOUND);
}
}

使用

1.注解方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码@RestController
public class HelloController {
/**
* 切换到主数据源方式1
*/
@GetMapping("/masterFirst")
@SwitchRds(dataSource = DataSourceConstant.DATA_SOURCE_MASTER)
public Object masterFirst() {
// todo
}

/**
* 切换到主数据源方式2
*/
@GetMapping("/masterSecond")
@SwitchMasterRds
public Object masterSecond() {
// todo
}

/**
* 切换到其他已配置的数据源
*/
@GetMapping("/other")
@SwitchRds(dataSource = DataSourceConstant.DATA_SOURCE_DB_1)
public Object other() {
// todo
}

/**
* 根据租户代码切换
*/
@GetMapping("/tenant")
@SwitchRds(tenantCode = "tenantxxx")
public Object tenant() {
// todo
}
}

2.业务代码使用方式

1
2
3
4
5
java复制代码try {
tenantRdsServiceImpl.switchRds("any tenant code");
} catch (Exception e) {
log.error("切换租户rds时出错:{},context:{}", e.getMessage(), context, e);
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux云服务部署Spring boot项目 Linux云

发表于 2021-08-28

Linux云服务部署Spring boot项目

这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战

背景:

之前经过两个周的时间,做了一个简单的博客网站,网址:点击进入,在本地可以正常使用以后,想着部署到服务器上,给大家伙看个乐呵,于是有了这篇部署文章。

简单介绍一下博客内容:

  1. 仿照小而美搭建的,前端模板也采用小而美实现。
  2. 技术点:springBoot+thymeleaf+mysql+Mybatis-plus(混合xml实现的)

该博客不会维护,作用是梳理前面的学习过的知识,增加记忆点。

话不多说,开始吧!

环境搭建:

为了不让项目产生版本兼容问题,统一使用的项目的版本安装。

包名 版本号
maven 3.6.3
jdk 9
mysql 最新版本(有坑)

所需要的工具:

  1. Xshell(Linux连接)
  2. Xftp(文件传输)
  3. navicat(数据库连接)
  4. Linux(云服务器) 新用户9块一个月。

这里就没有用云mysql.

部署过程:

项目打包:

  1. clear
  2. package

image-20210828162448130

打包成功的样例:

image-20210828162911980

上传到云服务器:

可以界面化上传的软件很多,自行选择。(个人:Xftp软件)

image-20210828163210619

image-20210828163358866

看图说话,如果之前有朋友部署过,应该很清楚,接下来解释下:

我分了两个包:

  1. javapro 放置jdk和系统的文件
  2. mvn 放置maven安装包

可以随时修改配置文件,使得项目生效。

解压命令:

1
shell复制代码tar -zxvf 包名

配置jdk以及maven环境:

进入jdk文件夹:

1
2
3
shell复制代码cd jdk-9.0.4/
root@iZuf67gh3nsadi0q4sg7ulZ:~/javapro/jdk-9.0.4# pwd
/root/javapro/jdk-9.0.4

同操作:

1
2
3
shell复制代码cd apache-maven-3.6.3/
root@iZuf67gh3nsadi0q4sg7ulZ:~/mvn/apache-maven-3.6.3# pwd
/root/mvn/apache-maven-3.6.3

记住当前路径,输入:

1
shell复制代码vi /etc/profile

如下:

1
2
3
4
shell复制代码export JAVA_HOME=/root/javapro/jdk-9.0.4 ## 后面是目录的路径和软件链接 ,根据实际情况来写
export PATH=.:$JAVA_HOME/bin:$PATH
#mvn
export PATH=$PATH:/root/mvn/apache-maven-3.6.3/bin

仿照上面格式,填入自己的路径。

退出后,加载一下:

1
shell复制代码source /etc/profile

最后验证:

1
2
shell复制代码java -version
mvn -version

如下:

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码mvn -version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /root/mvn/apache-maven-3.6.3
Java version: 9.0.4, vendor: Oracle Corporation, runtime: /root/javapro/jdk-9.0.4
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-47-generic", arch: "amd64", family: "unix"
--------------------------------------------------------------------------------
java -version
java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)
root@iZuf67gh3nsadi0q4sg7ulZ:~/mvn/apache-maven-3.6.3#

安装MySQL实现远程访问:

之前MySQL的安装其实也写过,但是没想到折磨也在这里折磨的。

首先在线安装mysql,自己使用官网的安装包,下载到linux中解压报错,原来以为是包下错了,但是在网上找到的教程,还是没有解决,所以实现在线安装的。

如果有知道我这种错误的朋友,可以评论交流下。

这是我之前的写的在线安装的文章:链接

这里针对文章中的做一个补充和修改,该文章在MySQL安装上没什么问题,只是在权限认证上需要补充一下:

  1. 之前一直用mysql5版本进行远程授权登录,但是换mysql8.0原来的授权方式报错
  2. 版本5可以使用原来的文章进行配置,但是MySQL8不行,不能授权给自己了。

MySQL8权限验证解决方式:新增一个用户,远程连接到该用户上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql复制代码<br>mysql> create user 'xbhog'@'%' identified by 'xbhog';
Query OK, 0 rows affected (0.02 sec)

mysql>
mysql>
mysql> GRANT ALL PRIVILEGES ON *.* TO 'xbhog'@'%';
Query OK, 0 rows affected (0.04 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

mysql> ALTER USER 'xbhog'@'%' IDENTIFIED WITH mysql_native_password BY '对应自己的密码';
Query OK, 0 rows affected (0.02 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.01 sec)

使用Navicat连接需要更改下加密方式:

1
2
3
4
5
6
7
8
9
mysql复制代码如果用Navicat连接的回报不支持caching_sha2_password验证方式的错误。

将远程访问用户的验证方式改为: msyql_native_password

执行:

ALTER USER 'username'@'%' IDENTIFIED WITH mysql_native_password BY 'password';

FLUSH PRIVILEGES;

image-20210828171045262

启动项目:

1
shell复制代码java -jar jar包名(Blog-0.0.1-SNAPSHOT.jar)

image-20210828171314016

后台运行:

1
shell复制代码nohup java -jar jar包名(Blog-0.0.1-SNAPSHOT.jar)

参考:

MySQL8问题

结束:

如果你看到这里或者正好对你有所帮助,希望能点个👍或者⭐感谢;

有错误的地方,欢迎在评论指出,作者看到会进行修改。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【RabbitMQ】2 五种消息模型

发表于 2021-08-28

消息队列的特性

  • durable:队列持久化。如果设置持久化,那么无论RabbitMQ在关闭时,就会将队列存储到本地磁盘,无论宕机还是重启,队列也不会删除;如果设置不持久化,那么在RabbitMQ关闭时,就会将队列删除。
  • exclusive:独占队列。如果设置独占,那么当前队列只允许预先设置的Connection访问;如果设置不独占,则所有Connection都可以访问。
  • autoDelete:自动删除队列。如果设置自动删除,那么当消费者消费完该队列的消息时,该队列立刻被删除;如果设置不自动删除,那么当消费者消费完该队列的消息时,该队列还会继续被保留。

消息自动确认机制

20210826163116.png

如果开启消息自动确认,那么一旦MQ把消息发送给消费者,那么该消息就会立即标记为删除。又因为在默认情况下,在MQ把消息发送给对应Consumer时,是一次性把属于该Consumer的所有消息发送到对应的通道内。所有如果其中一个消费者正在消费一个长期的任务却只完成的一部分就死了,那么我们就会丢失正在消费的消息和发送给该消费者中通道内尚未消费的所有消息。

如上模型,如果是循环的分发方式,Consumer-1一次性被分到的消息为1,3,5号消息,Consumer-2一次性被分到的消息为2,4,6号消息。如果Consumer-1在消费3号消息时出错宕机了,那么正在消费的3号消息和通道中还未消费的5号消息就会全部丢失。如果Consumer-2在消费2号消息时宕机了,那么正在消费的2号消息和通道中还未消费的4号、6号消息就会全部丢失。

为了保证业务数据的完整性,我们需要修改两个点:

  1. 在MQ把消息发送给消费者后,该消息不会标记为删除。
  2. 当某一消费者在宕机时,能够将正在消费的消息和还未消费的消息交给其他消费者继续消费。

第一点如果要实现,就要关闭消息自动确认机制。目的是为了保证在消费者消费消息宕机时,当前消息不会丢失。在关闭消息自动确认后,我们需要在每一个消息被正常消费后,手动确认该消息,这样该消息才会被标记为删除,MQ才会发送下一个消息给该Consumer。

第二点如果要实现,那么就不能按照默认的那样,MQ一次性将消息发送给Consumer,需要设置MQ每一次只能发给Consumer一个消息。目的是为了保证在消费者消费消息时,对应通道内没有其他消息,以至于如果该消费者宕机了,本应分给该消费者的消息能够被MQ分配给其他消费者消费。

消息模型

RabbitMQ在3.5版本之前只支持五种消息发布模型,分别是:”Hello World!”,Work queues,Publish/Subscribe,Routing,Topics。在3.7版本支持六种消息发布模型(新增RPC),在3.9版本支持七种消息发布模型(新增Publisher Confirms)。

RPC模型和Publisher Confirms模型本文中不会描述,本文只描述常见的五种消息发布模型,足够应对多种业务场景。

生产者和对应的一个或多个消费者必须在同一个用户下,连接同一个Virtual host(虚拟主机)、Channel(通道)、Exchange(交换机)和MQ(消息队列),才能相互进行通信。

1. “Hello World!”模型

20210826022959.png

点对点模型,一个生产者、一个消息队列和一个消费者。

生产者直接将消息发送进消息队列,消费者监听消息队列,不断地从中获取消息并消费。

点对点模型只能处理一些简单的业务,如果消息比较多,消费者处理消息比较耗时时,那么生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积的越来越多。

2. Work queues模型

20210826190813.png

任务模型,一个生产者、一个消息队列和多个消费者。

生产者直接将消息发送进消息队列,多个消费者监听同一个消息队列,共同消费消息队列中的消息。

队列中的消息一旦被某一个消费者消费,就会消失,因此消息是不会被重复消费的。

在Work queues模型中,MQ绑定了多个消费者,RabbitMQ默认采用了循环的方式分发消息。

循环:RabbitMQ将按顺序将每个消息发送到下一个使用者,平均每个消费者都会收到相同数量的消息。例如,有编号1-10的消息在MQ中,同时存在两个消费者,则消费者A分到的消息为1,3,5,7,9,消费者B分到的消息为2,4,6,8,10。

因为RabbitMQ默认是以循环的方式分发消息,所以如果消费者中有消费能力较弱的消费者,那么也会造成消息的堆积越来越多,拖慢系统的运行速度。RabbitMQ还提供了能者多劳的分发消息的方式,消费能力强的消费者就会被分到更多的消息,消费能力弱的消费者就会被分到更少的消息,该方式需要作额外设置。

3. Publish/Subscribe模型

20210826193315.png

广播模型,一个生产者、一个fanout交换机、多个消息队列和多个消费者,一个交换机绑定多个队列,一个消息队列绑定一个消费者。

生产者首先将消息先发给交换机,交换机将消息发送给所有队列,队列接收到消息后再发送给对应绑定的消费者,实现一条消息被所有消费者消费。

根据AMQP协议,前两种模型也需要连接交换机,之所以没有在模型图中显示,是因为前两种模型使用的是RabbitMQ中的default交换机。

4. Routing模型

20210827003701.png

路由模型,一个生产者、一个direct交换机、多个消息队列和多个消费者,一个交换机绑定多个队列,一个消息队列绑定一个消费者。

与广播模型的不同的是,广播模型的交换机与队列之间是任意绑定,也就是交换机绑定了所有队列。在路由模型中,交换机需要绑定一个RoutingKey(路由key)。队列也需要指定一个RoutingKey,交换机不再把消息发送给所有队列,而是根据队列的RoutingKey进行判断,只有队列的RoutingKey和交换机的RoutingKey完全一致,交换机才把消息发送给该队列进行消费。

例如上述模型,所有MQ绑定了 “info” 的RoutingKey,只有第一个MQ还另外绑定了 “error” 的RoutingKey。那么当生产者发送RoutingKey为 “info” 的消息时,交换机会将该消息发送给所有MQ;当生产者发送RoutingKey为 “error” 的消息时,交换机只会将该消息发送给第一个MQ。

注意:RoutingKey一般由一个或者多个单词组成,如果是以多个单词组成,单词之间以 “.” 分割。

5. Topics模型

20210827020206.png

动态路由模型,一个生产者、一个topic交换机、多个消息队列和多个消费者,一个交换机绑定多个队列,一个消息队列绑定一个消费者。

在路由模型中,如果队列需要绑定一个RoutingKey,那么就要单独设置一次RoutingKey,如果需要绑定多个RoutingKey,那么就要单独设置多次不同的RoutingKey,这样以来,会造成代码冗余,及其不方便。Topics模型提供了动态路由匹配规则,也就是在路由模型的基础上允许在RoutingKey中使用通配符,这样设置一个使用了统配符的RoutingKey就可以匹配多个符合规则的具体的RoutingKey了,从而不需要设置多次,大大简化了代码。

RabbitMQ中提供了两种通配符:

  • *:替换 1 个单词。
  • #:替换 0 个或者多个单词。

例如上述模型,第一个MQ绑定了 “*.rabbit.*“ 的RoutingKey,最后一个MQ绑定了 “*.rabbit.#” 的RoutingKey。那么当生产者发送RoutingKey为 “user.rabbit.insert” 的消息时,交换机会将该消息发送给第一个MQ;当生产者发送RoutingKey为 “user.rabbit.insert.all” 的消息时,交换机会将该消息发送给最后一个MQ;当生产者发送RoutingKey为 “user.rabbit” 的消息时,交换机也会将该消息发送给最后一个MQ。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Liteflow 一个解耦/组件化复杂业务流程的工具 官网介

发表于 2021-08-28

以下介绍直接引用官网,写得非常易懂,地址:yomahub.com/liteflow/do…

官网介绍

前言

在每个公司的系统中,总有一些拥有复杂业务逻辑的系统,这些系统承载着核心业务逻辑,几乎每个需求都和这些核心业务有关,这些核心业务业务逻辑冗长,涉及内部逻辑运算,缓存操作,持久化操作,外部资源调取,内部其他系统RPC调用等等。时间一长,项目几经易手,维护的成本得就会越来越高。各种硬代码判断,分支条件越来越多。代码的抽象,复用率也越来越低,各个模块之间的耦合度很高。一小段逻辑的变动,会影响到其他模块,需要进行完整回归测试来验证。如要灵活改变业务流程的顺序,则要进行代码大改动进行抽象,重新写方法。实时热变更业务流程,几乎很难实现。

LiteFlow框架的作用

LiteFlow就是为解耦复杂逻辑而生,如果你要对复杂业务逻辑进行新写或者重构,用LiteFlow最合适不过。它是一个轻量,快速的组件式流程引擎框架,组件编排,帮助解耦业务代码,让每一个业务片段都是一个组件,并支持热加载规则配置,实现即时修改。

使用LiteFlow,你需要去把复杂的业务逻辑按代码片段拆分成一个个小组件,并定义一个规则流程配置。这样,所有的组件,就能按照你的规则配置去进行复杂的流转。

LiteFlow的设计原则

LiteFlow是基于工作台模式进行设计的,何谓工作台模式?

n个工人按照一定顺序围着一张工作台,按顺序各自生产零件,生产的零件最终能组装成一个机器,每个工人只需要完成自己手中零件的生产,而无需知道其他工人生产的内容。每一个工人生产所需要的资源都从工作台上拿取,如果工作台上有生产所必须的资源,则就进行生产,若是没有,就等到有这个资源。每个工人所做好的零件,也都放在工作台上。

这个模式有几个好处:

  • 每个工人无需和其他工人进行沟通。工人只需要关心自己的工作内容和工作台上的资源。这样就做到了每个工人之间的解耦和无差异性。
  • 即便是工人之间调换位置,工人的工作内容和关心的资源没有任何变化。这样就保证了每个工人的稳定性。
  • 如果是指派某个工人去其他的工作台,工人的工作内容和需要的资源依旧没有任何变化,这样就做到了工人的可复用性。
  • 因为每个工人不需要和其他工人沟通,所以可以在生产任务进行时进行实时工位更改:替换,插入,撤掉一些工人,这样生产任务也能实时的被更改。这样就保证了整个生产任务的灵活性。

这个模式映射到LiteFlow框架里,工人就是组件,工人坐的顺序就是流程配置,工作台就是上下文,资源就是参数,最终组装的这个机器就是这个业务。正因为有这些特性,所以LiteFlow能做到统一解耦的组件和灵活的装配。

实际场景

场景1

我们在做在线教育系统的时候的下单流程,有两种下单方式,一是直接对老师下单买这个老师的课时,一个是先买课时,再分配老师。其实最终下的订单都是一样的,只是有没有分配老师,然而又穿插的不一样的业务流程,具体:

1
2
3
4
5
6
7
8
9
10
11
markdown复制代码下单流程1:对老师下单

1. 获取教师详情
2. 获取课时详情
3. 检测教师是否有空余排课时间
4. 检测课时库存
5. 创建订单流程-将老师信息填入订单表
6. 创建订单流程-将课时信息填入订单表
7. 支付
8. 更新老师排课时间
9. 发送消息通知老师
1
2
3
4
5
6
markdown复制代码下单流程2:对课时下单
1. 获取课时详情
2. 检测课时库存
3. 创建订单流程-将课时信息填入订单表
4. 支付
5. 发送消息通知教管安排老师

大家可以看到,虽然是同种下单,但是流程是不一样的,并且有些步骤的可以独立出来解耦的,我们平时开发的时候遇到这样的需求一般都是写2个又长又臭的代码方法,而这些代码又是耦合在一起,维护性太差,目前为止可能还看不到Liteflow的强大之处,如果接下来又有一个需求:如果是管理员在后台系统下单无需检测库存;难道我们又要写第三个又长又臭的代码出来吗?

使用Liteflow就可以把各步骤组件化进行装配的优势就很明显了:

1
2
3
4
5
markdown复制代码下单流程3:对课时下单 && 不检测库存
1. 获取课时详情
2. 创建订单流程-将课时信息填入订单表
3. 支付
4. 发送消息通知教管安排老师

我只需把检测库存的组件编排走就可以了,代码完全可以复用。想必大家已经明白Liteflow的用途了

简单实例

PS:此处例子只是用来简单演示,Liteflow功能远不止如此,具体去官网的文档和示例工程更全面

  1. 我们先定义一个数据插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码/**
* 定义一个插槽 (用简单的String演示)
* @author yejunxi
* @date 2021/8/28
*/
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@Data
public class OrderSlot extends AbsSlot {

/**
* 课时详情
*/
private String courseInfo;

/**
* 教师详情
*/
private String teacherInfo;

/**
* 库存
*/
private Integer stock;

}
  1. 定义4个组件:获取老师请求、获取课程详情、检测库存、下单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* 下单组件-补充订单教师详情
*
* @author yejunxi
* @date 2021/8/28
*/
@LiteflowComponent("OrderTeacherCmp")
public class OrderTeacherCmp extends NodeComponent {
@Override
public void process() throws Exception {
OrderSlot orderSlot = this.getSlot();
orderSlot.setTeacherInfo("教师详情");

System.out.println("获取教师信息");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
/**
* 下单组件-补充订单课时详情
*
* @author yejunxi
* @date 2021/8/28
*/
@LiteflowComponent("OrderCourseCmp")
public class OrderCourseCmp extends NodeComponent {
@Override
public void process() throws Exception {
OrderSlot orderSlot = this.getSlot();
orderSlot.setCourseInfo("课时详情");
orderSlot.setStock(1);

System.out.println("获取课时信息");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码
/**
* 下单组件-检测课时库存
*
* @author yejunxi
* @date 2021/8/28
*/
@LiteflowComponent("CheckCourseStockCmp")
public class CheckCourseStockCmp extends NodeComponent {
@Override
public void process() throws Exception {
OrderSlot orderSlot = this.getSlot();
String courseInfo = orderSlot.getCourseInfo();
}


/**
* 表示出错是否继续往下执行下一个组件,默认为false
*
* @return
*/
@Override
public boolean isEnd() {
System.out.println("检测库存");
//判断库存是否充足
OrderSlot orderSlot = this.getSlot();
return orderSlot.getStock() <= 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* 下单组件-创建订单
*
* @author yejunxi
* @date 2021/8/28
*/
@LiteflowComponent("OrderCreateCmp")
public class OrderCreateCmp extends NodeComponent {
@Override
public void process() throws Exception {
OrderSlot orderSlot = this.getSlot();
System.out.println("此处已经拿到完整的slot进行下单:" + orderSlot.toString());
}
}
  1. 定义组件装配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<flow>
<!-- 下单检测库存-->
<chain name="createOrder">
<!--读取老师、课程信息-->
<then value="OrderTeacherCmp,OrderCourseCmp"/>
<!--检测库存-->
<then value="CheckCourseStockCmp"/>
<!--下单-->
<then value="OrderCreateCmp"/>
</chain>


<!-- 下单-不检测库存-->
<chain name="createOrderNotCheckStock">
<!--读取老师、课程信息-->
<then value="OrderTeacherCmp,OrderCourseCmp"/>
<!--下单-->
<then value="OrderCreateCmp"/>
</chain>

</flow>
  1. 调用
1
2
3
4
5
java复制代码String courseId = "课程ID";
//检测库存下单
LiteflowResponse<OrderSlot> response1 = flowExecutor.execute2Resp("createOrder", courseId, OrderSlot.class);
//不检测库存下单
LiteflowResponse<OrderSlot> response2= flowExecutor.execute2Resp("createOrderNotCheckStock", courseId, OrderSlot.class);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在PyQt中构建 Python 菜单栏、菜单和工具栏

发表于 2021-08-28

摘要:菜单、工具栏和状态栏是大多数GUI 应用程序的常见且重要的图形组件。您可以使用它们为您的用户提供一种快速访问应用程序选项和功能的方法。

本文分享自华为云社区《Python 和 PyQt:创建菜单、工具栏和状态栏》,作者:Yuchuan。

在使用 Python 和PyQt开发图形用户界面 (GUI)应用程序时,您将使用的一些最有用和最通用的图形元素是菜单、工具栏和状态栏。

菜单和工具栏可以使您的应用程序看起来精美和专业,为用户提供一组可访问的选项,而状态栏允许您显示有关应用程序状态的相关信息。

在本教程中,您将学习:

  • 什么菜单,工具栏和状态栏是
  • 如何以编程方式创建菜单、工具栏和状态栏
  • 如何使用PyQt 操作填充 Python 菜单和工具栏
  • 如何使用状态栏显示状态信息

此外,您将学习一些编程最佳实践,您可以在使用 Python 和 PyQt 创建菜单、工具栏和状态栏时应用这些实践。

在 PyQt 中构建 Python 菜单栏、菜单和工具栏

一个菜单栏是一个GUI应用程序的区域主窗口中保存菜单。菜单是选项的下拉列表,可以方便地访问应用程序的选项。例如,如果您正在创建一个文本编辑器,那么您的菜单栏中可能会有以下一些菜单:

  • 一个_文件_菜单,提供以下的一些菜单选项:
  • _新建_用于创建新文档
  • _打开_以打开现有文档
  • _打开最近_打开最近的文档
  • _Save_用于保存文档
  • _Exit_退出应用程序
  • 提供以下一些菜单选项的“_编辑”_菜单:
  • _Copy_用于复制一些文本
  • _Paste_用于粘贴一些文本
  • _Cut_用于剪切一些文本
  • 一个_帮助_菜单,提供以下一些菜单选项:
  • _帮助内容_用于启动用户手册和帮助内容
  • _关于_启动关于对话框

您还可以将其中一些选项添加到工具栏。工具栏是带有有意义图标的按钮面板,可提供对应用程序中最常用选项的快速访问。在您的文本编辑器示例中,您可以向工具栏添加_New_、Open、Save、_Copy_和_Paste_等选项。

注意:在本教程中,您将开发一个实现上述所有菜单和选项的示例应用程序。您可以使用此示例应用程序作为创建文本编辑器项目的起点。

在本节中,您将学习如何使用 Python 和 PyQt 向 GUI 应用程序添加菜单栏、菜单和工具栏的基础知识。

在继续之前,您将创建一个示例 PyQt 应用程序,您将在本教程中使用该应用程序。在每个部分中,您将向此示例应用程序添加新特性和功能。该应用程序将是一个主窗口风格的应用程序。这意味着它将有一个菜单栏、一个工具栏、一个状态栏和一个中央小部件。

打开您最喜欢的代码编辑器或 IDE并创建一个名为sample_app.py. 然后在其中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码import sys

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow

class Window(QMainWindow):
"""Main Window."""
def __init__(self, parent=None):
"""Initializer."""
super().__init__(parent)
self.setWindowTitle("Python Menus & Toolbars")
self.resize(400, 200)
self.centralWidget = QLabel("Hello, World")
self.centralWidget.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.setCentralWidget(self.centralWidget)

if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())

现在sample_app.py包含创建示例 PyQt 应用程序所需的所有代码。在这种情况下,Window继承自QMainWindow. 因此,您正在构建一个主窗口样式的应用程序。

注意:不幸的是,PyQt5 的官方文档有一些不完整的部分。要解决此问题,您可以查看PyQt4 文档或原始Qt 文档。

在类初始化程序中.__init__(),您首先使用 调用父类的初始化程序super()。然后使用 设置窗口的标题.setWindowTitle()并使用调整窗口大小.resize()。

注意:如果您不熟悉 PyQt 应用程序以及如何创建它们,那么您可以查看Python 和 PyQt:构建 GUI 桌面计算器。

窗口的中央小部件是一个QLabel对象,您将使用它来显示消息以响应某些用户操作。这些消息将显示在窗口的中央。要做到这一点,你叫.setAlignment()上QLabel对象与一对夫妇的对齐标志。

如果您从命令行运行该应用程序,那么您将在屏幕上看到以下窗口:

就是这样!您已经使用 Python 和 PyQt 创建了一个主窗口风格的应用程序。您将在本教程中即将出现的所有示例中使用此示例应用程序。

创建菜单栏

在 PyQt 主窗口风格的应用程序中,默认QMainWindow提供一个空QMenuBar对象。要访问此菜单栏,您需要调用.menuBar()您的QMainWindow对象。此方法将返回一个空的菜单栏。此菜单栏的父级将是您的主窗口对象。

现在返回到您的示例应用程序并在 的定义中添加以下方法Window:

1
2
3
4
ruby复制代码class Window(QMainWindow):
# Snip...
def _createMenuBar(self):
menuBar = self.menuBar()

这是在 PyQt 中创建菜单栏的首选方式。在这里,menuBar变量将包含一个空的菜单栏,这将是您的主窗口的菜单栏。

注意: PyQt 编程中的一个常见做法是将局部变量用于您不会使用或从其定义方法之外需要的对象。Python垃圾收集所有超出范围的对象,因此您可能认为menuBar在上面的示例中,一旦._createMenuBar() 返回就会消失。

事实是 PyQt 保留对本地对象的引用,例如menuBar使用它们的所有权或父子关系。换句话说,由于menuBar它归您的主窗口对象所有,Python 将无法对其进行垃圾收集。

向 PyQt 应用程序添加菜单栏的另一种方法是创建一个QMenuBar对象,然后使用.setMenuBar(). 考虑到这一点,您还可以._createMenuBar()按以下方式编写:

1
2
3
4
5
6
7
8
python复制代码from PyQt5.QtWidgets import QMenuBar
# Snip...

class Window(QMainWindow):
# Snip...
def _createMenuBar(self):
menuBar = QMenuBar(self)
self.setMenuBar(menuBar)

在上面的例子中,menuBar持有一个QMenuBar父级设置为的对象self,它是应用程序的主窗口。一旦你有了菜单栏对象,你就可以.setMenuBar()将它添加到你的主窗口中。最后,需要注意的是在这个例子中工作,你首先需要进口 QMenuBar的PyQt5.QWidgets。

在 GUI 应用程序中,菜单栏会根据底层操作系统显示在不同的位置:

  • Windows:在应用程序主窗口的顶部,标题栏下方
  • macOS:在屏幕顶部
  • Linux:在主窗口顶部或屏幕顶部,取决于您的桌面环境

为应用程序创建菜单栏的最后一步是._createMenuBar()从主窗口的初始化程序调用.__init__():

1
2
3
4
5
python复制代码class Window(QMainWindow):
"""Main Window."""
def __init__(self, parent=None):
# Snip...
self._createMenuBar()

如果您使用这些新更改运行示例应用程序,那么您将看不到应用程序主窗口中显示的菜单栏。那是因为您的菜单栏仍然是空的。要查看应用程序主窗口上的菜单栏,您需要创建一些菜单。这就是你接下来要学习的内容。

将菜单添加到菜单栏

菜单是菜单选项的下拉列表,您可以通过单击它们或按键盘快捷键来触发。在 PyQt 中,至少有三种方法可以将菜单添加到菜单栏对象:

  1. **QMenuBar.addMenu(menu)**将QMenu对象 ( menu)附加到菜单栏对象。它返回与此菜单关联的操作。
  2. **QMenuBar.addMenu(title)**创建一个QMenu以字符串 ( title) 作为标题的新对象并将其附加到菜单栏。菜单栏取得菜单的所有权,该方法返回新QMenu对象。
  3. **QMenuBar.addMenu(icon, title)**创建并追加新的QMenu物品与icon和title一个菜单栏对象。菜单栏取得菜单的所有权,该方法返回新QMenu对象。

如果使用第一个选项,则需要先创建自定义QMenu对象。为此,您可以使用以下构造函数之一:

  1. QMenu(parent)
  2. QMenu(title, parent)

在这两种情况下,parent是QWidget将持有QMenu对象的所有权。您通常会设置parent到您将在其中使用菜单的窗口。在第二个构造函数中,title将保存一个带有描述菜单选项的文本的字符串。

以下是将_File_、_Edit_和_Help_菜单添加到示例应用程序的菜单栏的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码from PyQt5.QtWidgets import QMenu
# Snip...

class Window(QMainWindow):
# Snip...
def _createMenuBar(self):
menuBar = self.menuBar()
# Creating menus using a QMenu object
fileMenu = QMenu("&File", self)
menuBar.addMenu(fileMenu)
# Creating menus using a title
editMenu = menuBar.addMenu("&Edit")
helpMenu = menuBar.addMenu("&Help")

首先,你导入 QMenu的PyQt5.QtWidgets。然后在 中._createMenuBar(),使用 的前两个变体向菜单栏添加三个菜单.addMenu()。第三个变体需要一个图标对象,但您还没有学会如何创建和使用图标。您将在使用 PyQt 中的图标和资源部分中了解如何使用图标。

如果您运行示例应用程序,那么您将看到您现在有一个如下所示的菜单栏:

PyQt 菜单栏

应用程序的菜单栏有菜单_File_、Edit_和_Help。当您单击这些菜单时,它们不会显示菜单选项的下拉列表。那是因为您还没有添加菜单选项。您将在使用操作填充菜单部分中了解如何向菜单添加菜单选项。

最后,请注意&包含在每个菜单标题中的与符号 ( ) 会在菜单栏显示中创建带下划线的字母。这在定义菜单和工具栏选项的键盘快捷键一节中有更详细的讨论。

创建工具栏

甲工具栏是保存按钮和其他部件,以提供到GUI应用的最普通的选项快速访问的可移动面板。工具栏按钮可以显示图标、文本或两者来表示它们执行的任务。PyQt 中工具栏的基类是QToolBar. 此类将允许您为 GUI 应用程序创建自定义工具栏。

当您向主窗口样式应用程序添加工具栏时,默认位置在窗口顶部。但是,您可以在以下四个工具栏区域之一中放置工具栏:

工具栏区域在 PyQt 中被定义为常量。如果您需要使用它们,那么您必须导入QtfromPyQt5.QtCore然后像 in 一样使用完全限定名称Qt.LeftToolBarArea。

在 PyQt 中,可以通过三种方法向主窗口应用程序添加工具栏:

  1. **QMainWindow.addToolBar(title)**创建一个新的空QToolBar对象并将其窗口标题设置为title. 此方法将工具栏插入顶部工具栏区域并返回新创建的工具栏。
  2. **QMainWindow.addToolBar(toolbar)**将QToolBar对象 ( toolbar) 插入顶部工具栏区域。
  3. **QMainWindow.addToolBar(area, toolbar)**将QToolBar对象 ( toolbar) 插入指定的工具栏区域 ( area)。如果主窗口已有工具栏,则toolbar放置在最后一个现有工具栏之后。如果toolbar已经存在于主窗口中,那么它只会被移动到area.

如果您使用最后两个选项之一,那么您需要自己创建工具栏。为此,您可以使用以下构造函数之一:

  1. QToolBar(parent)
  2. QToolBar(title, parent)

在这两种情况下,parent代表QWidget将拥有工具栏所有权的对象。您通常会将工具栏所有权设置为将在其中使用工具栏的窗口。在第二个构造函数中,title将是一个带有工具栏窗口标题的字符串。PyQt 使用这个窗口标题来构建一个默认的上下文菜单,允许你隐藏和显示你的工具栏。

现在您可以返回到您的示例应用程序并将以下方法添加到Window:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码from PyQt5.QtWidgets import QToolBar
# Snip...

class Window(QMainWindow):
# Snip...
def _createToolBars(self):
# Using a title
fileToolBar = self.addToolBar("File")
# Using a QToolBar object
editToolBar = QToolBar("Edit", self)
self.addToolBar(editToolBar)
# Using a QToolBar object and a toolbar area
helpToolBar = QToolBar("Help", self)
self.addToolBar(Qt.LeftToolBarArea, helpToolBar)

首先,您QToolBar从PyQt5.QtWidgets. 然后,在 中._createToolBars(),您首先使用标题创建_文件_工具栏.addToolBar()。接下来,您创建一个QToolBar带有标题的对象,”Edit”并使用.addToolBar()不传递工具栏区域将其添加到工具栏。在这种情况下,_编辑_工具栏位于顶部工具栏区域。最后,创建_帮助_工具栏并使用 将其放置在左侧工具栏区域Qt.LeftToolBarArea。

完成这项工作的最后一步是._createToolBars()从 的初始化程序调用Window:

1
2
3
4
5
python复制代码class Window(QMainWindow):
"""Main Window."""
def __init__(self, parent=None):
# Snip...
self._createToolBars()

._createToolBars()对初始化器内部的调用Window将创建三个工具栏并将它们添加到您的主窗口中。以下是您的应用程序现在的外观:

带有图标的 PyQt 菜单栏

现在,菜单栏正下方有两个工具栏,窗口左侧有一个工具栏。每个工具栏都有一条双虚线。当您将鼠标移到虚线上时,指针会变成一只手。如果单击并按住虚线,则可以将工具栏移动到窗口上的任何其他位置或工具栏区域。

如果右键单击工具栏,PyQt 将显示一个上下文菜单,允许您根据需要隐藏和显示现有工具栏。

到目前为止,您的应用程序窗口中有三个工具栏。这些工具栏仍然是空的——您需要添加一些工具栏按钮才能使它们起作用。为此,您可以使用 PyQt actions,它们是QAction. 您将在后面的部分中学习如何在 PyQt 中创建操作。现在,您将学习如何在 PyQt 应用程序中使用图标和其他资源。

在 PyQt 中使用图标和资源

在Qt库包括Qt的资源系统,这是增加的二进制文件,如图标,图像,翻译文件和其他资源对应用程序的一种便捷方式。

要使用资源系统,您需要在资源集合文件或.qrc文件中列出您的资源。一个.qrc文件是一个XML包含位置,或文件路径,文件系统中的每个资源的。

假设您的示例应用程序有一个resources目录,其中包含您要在应用程序的 GUI 中使用的图标。您有_New_、_Open_等选项的图标。您可以创建一个.qrc包含每个图标路径的文件:

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="file-new.svg">resources/file-new.svg</file>
<file alias="file-open.svg">resources/file-open.svg</file>
<file alias="file-save.svg">resources/file-save.svg</file>
<file alias="file-exit.svg">resources/file-exit.svg</file>
<file alias="edit-copy.svg">resources/edit-copy.svg</file>
<file alias="edit-cut.svg">resources/edit-cut.svg</file>
<file alias="edit-paste.svg">resources/edit-paste.svg</file>
<file alias="help-content.svg">resources/help-content.svg</file>
</qresource>
</RCC>

每个条目必须包含文件系统中资源的路径。指定的路径相对于包含.qrc文件的目录。在上面的例子中,resources目录需要和.qrc文件在同一个目录下。

alias 是一个可选属性,它定义了一个简短的替代名称,您可以在代码中使用它来访问每个资源。

一旦您拥有应用程序的资源,您就可以运行pyrcc5针对您的.qrc文件的命令行工具。pyrcc5随 PyQt 一起提供,并且必须在安装 PyQt 后在您的Python 环境中完全正常运行。

pyrcc5读取一个.qrc文件并生成一个 Python 模块,其中包含所有资源的二进制代码:

1
ruby复制代码$ pyrcc5 -o qrc_resources.py resources.qrc

此命令将读取resources.qrc并生成qrc_resources.py包含每个资源的二进制代码。您将能够通过导入在 Python 代码中使用这些资源qrc_resources。

注意:如果运行时出现问题pyrcc5,请确保您使用的是正确的 Python 环境。如果您在 Python 虚拟环境中安装 PyQt,那么您将无法pyrcc5在该环境之外使用。

这qrc_resources.py是对应于您的代码片段resources.qrc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码# -*- coding: utf-8 -*-

# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.9.5)
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore

qt_resource_data = b"\
\x00\x00\x03\xb1\
\
\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\
...

随着qrc_resources.py在地方,你可以将其导入到你的应用程序,并通过键入一个冒号(请参阅各资源:),然后无论是它的alias或它的路径。例如,要使用file-new.svg其别名进行访问,您可以使用访问字符串 “:file-new.svg”。如果您没有alias,则可以通过带有访问字符串的路径访问它”:resources/file-new.svg”。

如果您有别名,但由于某种原因您想通过其路径访问给定资源,那么您可能必须从访问字符串中删除冒号才能使其正常工作。

要在您的操作中使用图标,您首先需要导入您的资源模块:

1
arduino复制代码import qrc_resources

导入包含资源的模块后,您可以在应用程序的 GUI 中使用这些资源。

注意: Linters、编辑器和 IDE可能会将上述 import 语句标记为未使用,因为您的代码不会包含对它的任何显式使用。某些 IDE 可能会更进一步并自动删除该行。

在这些情况下,您必须覆盖您的 linter、编辑器或 IDE 的建议,并将该导入保留在您的代码中。否则,您的应用程序将无法显示您的资源。

要使用资源系统创建图标,您需要实例化QIcon,将别名或路径传递给类构造函数:

1
ini复制代码newIcon = QIcon(":file-new.svg")

在此示例中,您将QIcon使用文件创建一个对象,该对象file-new.svg位于您的资源模块中。这提供了一种在整个 GUI 应用程序中使用图标和资源的便捷方式。

现在返回到您的示例应用程序并更新最后一行._createMenuBar():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码from PyQt5.QtGui import QIcon

import qrc_resources
# Snip...

class Window(QMainWindow):
# Snip...
def _createMenuBar(self):
menuBar = self.menuBar()
# Using a QMenu object
fileMenu = QMenu("&File", self)
menuBar.addMenu(fileMenu)
# Using a title
editMenu = menuBar.addMenu("&Edit")
# Using an icon and a title
helpMenu = menuBar.addMenu(QIcon(":help-content.svg"), "&Help")

要使此代码正常工作,您首先需要QIcon从PyQt5.QtGui. 您还需要导入qrc_resources. 在最后突出显示的行中,您从资源模块中添加了一个helpMenu使用图标help-content.svg。

如果您使用此更新运行示例应用程序,您将获得以下输出:

带有小部件的 PyQt 工具栏

应用程序的主窗口现在在其_帮助_菜单上显示一个图标。当您单击该图标时,菜单会显示文本Help。在菜单栏中使用图标并不常见,但 PyQt 允许您这样做。

在 PyQt 中为 Python 菜单和工具栏创建操作

PyQt动作是表示应用程序中给定命令、操作或动作的对象。当您需要为不同的 GUI 组件(例如菜单选项、工具栏按钮和键盘快捷键)提供相同的功能时,它们非常有用。

您可以通过实例化QAction. 创建操作后,您需要将其添加到小部件中才能在实践中使用它。

您还需要将您的操作与某些功能联系起来。换句话说,您需要将它们连接到触发操作时要运行的函数或方法。这将允许您的应用程序执行操作以响应 GUI 中的用户操作。

行动是相当多才多艺的。它们允许您跨菜单选项、工具栏按钮和键盘快捷键重复使用并保持同步相同的功能。这在整个应用程序中提供了一致的行为。

例如,当用户单击_打开…_菜单选项、单击_打开_工具栏按钮或按键盘上的Ctrl+O时,他们可能希望应用程序执行相同的操作。

QAction 提供了一个抽象,允许您跟踪以下元素:

  • 菜单选项上的文字
  • 工具栏按钮上的文本
  • 工具栏选项上的帮助提示(工具提示)
  • 这是什么帮助提示
  • 状态栏上的帮助提示(状态提示)
  • 与选项关联的键盘快捷键
  • 与菜单和工具栏选项相关联的图标
  • 动作enabled或disabled状态
  • 动作on或off状态

要创建操作,您需要实例化QAction. 至少有三种通用方法可以做到这一点:

  1. QAction(parent)
  2. QAction(text, parent)
  3. QAction(icon, text, parent)

在所有三种情况下,都parent表示拥有操作所有权的对象。此参数可以是任何QObject. 最佳实践是将操作创建为您将在其中使用它们的窗口的子项。

在第二个和第三个构造函数中,text保存操作将在菜单选项或工具栏按钮上显示的文本。

操作文本在菜单选项和工具栏按钮上的显示方式不同。例如,文本&Open…显示为_打开…_菜单中的选项,如_打开_的工具栏按钮。

在第三个构造函数中,icon是一个QIcon保存动作图标的对象。此图标将显示在菜单选项中文本的左侧。图标在工具栏按钮中的位置取决于工具栏的.toolButtonStyle属性,可以采用以下值之一:

您还可以设置该操作的文本和图标通过各自的setter方法,.setText()和.setIcon()。

**注意:**有关QAction属性的完整列表,您可以查看文档。

以下是如何使用 的不同构造函数为示例应用程序创建一些操作QAction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码from PyQt5.QtWidgets import QAction
# Snip...

class Window(QMainWindow):
# Snip...
def _createActions(self):
# Creating action using the first constructor
self.newAction = QAction(self)
self.newAction.setText("&New")
# Creating actions using the second constructor
self.openAction = QAction("&Open...", self)
self.saveAction = QAction("&Save", self)
self.exitAction = QAction("&Exit", self)
self.copyAction = QAction("&Copy", self)
self.pasteAction = QAction("&Paste", self)
self.cutAction = QAction("C&ut", self)
self.helpContentAction = QAction("&Help Content", self)
self.aboutAction = QAction("&About", self)

在 中._createActions(),您为示例应用程序创建了一些操作。这些操作将允许您向应用程序的菜单和工具栏添加选项。

请注意,您将操作创建为实例属性,因此您可以._createActions()使用self. 这样,您就可以在菜单和工具栏上使用这些操作。

注意:在 中._createActions(),您不使用的第三个构造函数,QAction因为如果您还看不到操作,则使用图标是没有意义的。您将在使用操作填充工具栏部分中了解如何向操作添加图标。

下一步是调用._createActions()form 的初始化程序Window:

1
2
3
4
5
6
7
python复制代码class Window(QMainWindow):
"""Main Window."""
def __init__(self, parent=None):
# Snip...
self._createActions()
self._createMenuBar()
self._createToolBars()

如果您现在运行该应用程序,那么您将不会在 GUI 上看到任何更改。这是因为在将操作添加到菜单或工具栏之前不会显示它们。请注意,您在调用._createActions()之前先调用._createMenuBar(),._createToolBars()因为您将在菜单和工具栏上使用这些操作。

如果您向菜单添加操作,则该操作将成为菜单选项。如果向工具栏添加操作,则该操作将成为工具栏按钮。这就是接下来几节的主题。

在 PyQt 中为 Python 菜单添加选项

如果要向 PyQt 中的给定菜单添加选项列表,则需要使用操作。到目前为止,您已经学习了如何使用QAction. 在 PyQt 中创建菜单时,操作是一个关键组件。

在本节中,您将学习如何使用操作来填充带有菜单选项的菜单。

用动作填充菜单

要使用菜单选项填充菜单,您将使用操作。在菜单中,操作表示为一个水平选项,其中至少有一个描述性文本,如_New_、Open、_Save_等。菜单选项还可以在其左侧显示一个图标,并在其右侧显示快捷键序列,例如Ctrl+S。

您可以QMenu使用向对象添加操作**.addAction()**。此方法有多种变体。他们中的大多数被认为是即时创建操作。在本教程中,但是,你要使用的变化.addAction()是QMenu从继承QWidget。这是此变体的签名:

1
scss复制代码QWidget.addAction(action)

参数action表示QAction要添加到给定QWidget对象的对象。使用 的这种变体.addAction(),您可以预先创建您的操作,然后根据需要将它们添加到您的菜单中。

注意: QWidget还提供.addActions(). 此方法采用一系列操作并将它们附加到当前小部件对象。

使用此工具,您可以开始向示例应用程序的菜单添加操作。为此,您需要更新._createMenuBar():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码class Window(QMainWindow):
# Snip...
def _createMenuBar(self):
menuBar = self.menuBar()
# File menu
fileMenu = QMenu("&File", self)
menuBar.addMenu(fileMenu)
fileMenu.addAction(self.newAction)
fileMenu.addAction(self.openAction)
fileMenu.addAction(self.saveAction)
fileMenu.addAction(self.exitAction)
# Edit menu
editMenu = menuBar.addMenu("&Edit")
editMenu.addAction(self.copyAction)
editMenu.addAction(self.pasteAction)
editMenu.addAction(self.cutAction)
# Help menu
helpMenu = menuBar.addMenu(QIcon(":help-content.svg"), "&Help")
helpMenu.addAction(self.helpContentAction)
helpMenu.addAction(self.aboutAction)

通过对 的更新._createMenuBar(),您可以向示例应用程序的三个菜单添加许多选项。

现在_文件_菜单有四个选项:

  1. 新建用于创建新文件
  2. Open…用于打开现有文件
  3. Save用于保存对文件所做的更改
  4. 退出以关闭应用程序

在_编辑_菜单中有三个选项:

  1. 将内容复制到系统剪贴板
  2. Paste用于从系统剪贴板粘贴内容
  3. Cut用于将内容剪切到系统剪贴板

在_帮助_菜单中有两个选项:

  1. 用于启动应用程序帮助手册的帮助内容
  2. 关于用于显示关于对话框

选项在菜单中从上到下显示的顺序对应于您在代码中添加选项的顺序。

如果您运行该应用程序,您将在屏幕上看到以下窗口:

带选项的 PyQt 菜单

如果您单击某个菜单,则该应用程序会显示一个包含您之前看到的选项的下拉列表。

创建 Python 子菜单

有时您需要在 GUI 应用程序中使用子菜单。子菜单是一个嵌套的菜单,当您将光标移到给定的菜单选项上时会显示该菜单。要将子菜单添加到应用程序,您需要调用.addMenu()容器菜单对象。

假设您需要在示例应用程序的_Edit_菜单中添加一个子菜单。您的子菜单将包含用于查找和替换内容的选项,因此您将其称为_Find and Replace_。该子菜单将有两个选项:

  1. 查找…以查找一些内容
  2. 替换…用于查找旧内容并将其替换为新内容

以下是将此子菜单添加到示例应用程序的方法:

1
2
3
4
5
6
7
8
9
10
ruby复制代码class Window(QMainWindow):
# Snip...
def _createMenuBar(self):
# Snip...
editMenu.addAction(self.cutAction)
# Find and Replace submenu in the Edit menu
findMenu = editMenu.addMenu("Find and Replace")
findMenu.addAction("Find...")
findMenu.addAction("Replace...")
# Snip...

在突出显示的第一行中,您使用on将QMenu带有文本的对象添加”Find and Replace”到“_编辑”_菜单。下一步是使用您迄今为止所做的操作填充子菜单。如果您再次运行示例应用程序,您将在_Edit_菜单下看到一个新的菜单选项:.addMenu()editMenu

PyQt 子菜单

在_编辑_菜单现在有一个新的条目称为_查找和替换_。当您将鼠标悬停在这个新菜单选项上时,会出现一个子菜单,为您提供两个新选项,_Find…和_Replace…。就是这样!您已经创建了一个子菜单。

在 PyQt 中向工具栏添加选项

在使用 Python 和 PyQt 构建 GUI 应用程序时,工具栏是一个非常有用的组件。您可以使用工具栏向您的用户提供一种快速访问应用程序中最常用选项的方法。您还可以向工具栏添加诸如旋转框和组合框之类的小部件,以允许用户直接从应用程序的 GUI 修改某些属性和变量。

在以下几节中,您将学习如何使用操作向工具栏添加选项或按钮,以及如何使用.addWidget().

用动作填充工具栏

要将选项或按钮添加到工具栏,您需要调用.addAction()。在本节中,你会依靠的变化.addAction()是QToolBar从继承QWidget。因此,您将.addAction()使用动作作为参数进行调用。这将允许您在菜单和工具栏之间共享您的操作。

创建工具栏时,您通常会面临决定向其中添加哪些选项的问题。通常,您只想将最常用的操作添加到工具栏。

如果返回到示例应用程序,您会记得您添加了三个工具栏:

  1. File
  2. Edit
  3. Help

在_文件_工具栏中,您可以添加如下选项:

  • New
  • Open
  • Save

在_编辑_工具栏中,您可以添加以下选项:

  • Copy
  • Paste
  • Cut

通常,当您要向工具栏添加按钮时,首先要选择要在每个按钮上使用的图标。这不是强制性的,但它是最佳实践。选择图标后,您需要将它们添加到相应的操作中。

以下是向示例应用程序的操作添加图标的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码class Window(QMainWindow):
# Snip...
def _createActions(self):
# File actions
self.newAction = QAction(self)
self.newAction.setText("&New")
self.newAction.setIcon(QIcon(":file-new.svg"))
self.openAction = QAction(QIcon(":file-open.svg"), "&Open...", self)
self.saveAction = QAction(QIcon(":file-save.svg"), "&Save", self)
self.exitAction = QAction("&Exit", self)
# Edit actions
self.copyAction = QAction(QIcon(":edit-copy.svg"), "&Copy", self)
self.pasteAction = QAction(QIcon(":edit-paste.svg"), "&Paste", self)
self.cutAction = QAction(QIcon(":edit-cut.svg"), "C&ut", self)
# Snip...

要将图标添加到您的操作,请更新突出显示的行。在 的情况下newAction,您使用.setIcon(). 在其余的操作中,您使用带有icon、 atitle和parent对象作为参数的构造函数。

一旦您选择的操作具有图标,您可以通过调用.addAction()工具栏对象将这些操作添加到相应的工具栏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码class Window(QMainWindow):
# Snip...
def _createToolBars(self):
# File toolbar
fileToolBar = self.addToolBar("File")
fileToolBar.addAction(self.newAction)
fileToolBar.addAction(self.openAction)
fileToolBar.addAction(self.saveAction)
# Edit toolbar
editToolBar = QToolBar("Edit", self)
self.addToolBar(editToolBar)
editToolBar.addAction(self.copyAction)
editToolBar.addAction(self.pasteAction)
editToolBar.addAction(self.cutAction)

通过此更新._createToolBars(),您可以将_新建_、打开_和_保存_选项的按钮添加到_文件_工具栏。您还可以将_Copy、_Paste_和_Cut_选项的按钮添加到“_编辑”_工具栏。

注意:按钮在工具栏上从左到右显示的顺序对应于您在代码中添加按钮的顺序。

如果您现在运行示例应用程序,您将在屏幕上看到以下窗口:

带有按钮的 PyQt 工具栏

示例应用程序现在显示两个工具栏,每个工具栏都有几个按钮。您的用户可以单击这些按钮以快速访问应用程序最常用的选项。

注意:当您第一次._createToolBars()在创建工具栏部分回信时,您创建了一个帮助工具栏。此工具栏旨在展示如何使用不同的.addToolBar().

在 的上述更新中._createToolBars(),您去掉了_帮助_工具栏,只是为了使示例简短明了。

请注意,由于您在菜单和工具栏之间共享相同的操作,因此菜单选项也会在其左侧显示图标,这在生产力和资源使用方面是一个巨大的胜利。这是使用 PyQt 操作通过 Python 创建菜单和工具栏的优势之一。

向工具栏添加小部件

在某些情况下,您会发现将特定小部件(如旋转框、组合框或其他)添加到工具栏很有用。一个常见的例子是大多数文字处理器使用的组合框,允许用户更改文档的字体或所选文本的大小。

要将小部件添加到工具栏,您首先需要创建小部件,设置其属性,然后调用.addWidget()工具栏对象,将小部件作为参数传递。

假设您想向示例应用程序QSpinBox的“_编辑”_工具栏添加一个对象,以允许用户更改某些内容的大小,可能是字体大小。您需要更新._createToolBars():

1
2
3
4
5
6
7
8
9
10
11
python复制代码from PyQt5.QtWidgets import QSpinBox
# Snip...

class Window(QMainWindow):
# Snip...
def _createToolBars(self):
# Snip...
# Adding a widget to the Edit toolbar
self.fontSizeSpinBox = QSpinBox()
self.fontSizeSpinBox.setFocusPolicy(Qt.NoFocus)
editToolBar.addWidget(self.fontSizeSpinBox)

在这里,您首先导入旋转框类。然后您创建一个QSpinBox对象,将其设置focusPolicy为Qt.NoFocus,最后将其添加到您的_编辑_工具栏。

注意:在上面的代码中,您将focusPolicy旋转框的属性设置为,Qt.NoFocus因为如果此小部件获得焦点,则应用程序的键盘快捷键将无法正常工作。

现在,如果您运行该应用程序,那么您将获得以下输出:

带有小部件的 PyQt 工具栏

此处,“_编辑”_工具栏显示了一个QSpinBox对象,您的用户可以使用该对象来设置应用程序上的字体大小或任何其他数字属性。

自定义工具栏

PyQt 工具栏非常灵活且可定制。您可以在工具栏对象上设置一堆属性。下表显示了一些最有用的属性:

所有这些属性都有一个关联的 setter 方法。例如,您可以使用.setAllowedAreas()to set allowedAreas、.setFloatable()to setfloatable等。

现在,假设您不希望用户在窗口周围移动_文件_工具栏。在这种情况下,您可以设置movable为False使用.setMovable():

1
2
3
4
5
6
7
ruby复制代码class Window(QMainWindow):
# Snip...
def _createToolBars(self):
# File toolbar
fileToolBar = self.addToolBar("File")
fileToolBar.setMovable(False)
# Snip...

突出显示的线使这里变得神奇。现在您的用户无法在应用程序窗口周围移动工具栏:

PyQt 工具栏自定义

该_文件_的工具栏不显示双虚线了,所以你的用户将无法将其移动。请注意,_编辑_工具栏仍然是可移动的。您可以使用相同的方法更改工具栏上的其他属性,并根据您的需要自定义它们。

组织菜单和工具栏选项

为了在 GUI 应用程序中增加清晰度并改善用户体验,您可以使用分隔符来组织菜单选项和工具栏按钮。分隔符呈现为分隔或分隔菜单选项的水平线或分隔工具栏按钮的垂直线。

要在菜单、子菜单或工具栏对象中插入或添加分隔符,您可以调用.addSeparator()这些对象中的任何一个。

例如,您可以使用分隔符将“_文件”_菜单上的“_退出”_选项与其余选项分开,以明确“_退出”_与菜单上​​的其余选项在逻辑上无关。您还可以使用分隔符将“_编辑”_菜单上的“_查找和替换”_选项与遵循相同规则的其余选项分开。

转到您的示例应用程序并._createMenuBar()按照以下代码进行更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ruby复制代码class Window(QMainWindow):
# Snip...
def _createMenuBar(self):
# File menu
# Snip...
fileMenu.addAction(self.saveAction)
# Adding a separator
fileMenu.addSeparator()
fileMenu.addAction(self.exitAction)
# Edit menu
# Snip...
editMenu.addAction(self.cutAction)
# Adding a separator
editMenu.addSeparator()
# Find and Replace submenu in the Edit menu
findMenu = editMenu.addMenu("Find and Replace")
# Snip...

在突出显示的第一行中,在“_文件”_菜单中的“_保存”_和“_退出”_选项之间添加一个分隔符。在第二个突出显示的行中,添加一个分隔符,将“_查找和替换”_选项与“_编辑”_菜单中的其余选项分开。以下是这些添加的工作原理:

带分隔符的 PyQt 菜单

您的“_文件”_菜单现在显示一条水平线,将“_编辑”_选项与菜单中的其余选项分开。在_编辑_菜单中还显示,在选项的下拉列表中的最后一个分隔符。分隔符的连贯使用可以巧妙地提高菜单和工具栏的清晰度,使您的 GUI 应用程序更加用户友好。

作为练习,您可以转到 的定义._createToolBars()并添加一个分隔符,将QSpinBox对象与工具栏上的其余选项分开。

在 PyQt 中构建上下文或弹出菜单

上下文菜单,也称为弹出菜单,是一种特殊类型的菜单,它会响应某些用户操作(例如右键单击给定的小部件或窗口)而出现。这些菜单提供了一小部分选项,这些选项在您使用的操作系统或应用程序的给定上下文中可用。

例如,如果您右键单击 Windows 计算机的桌面,您将获得一个菜单,其中包含与操作系统的特定上下文或空间相对应的选项。如果您右键单击文本编辑器的工作区,您将获得一个完全不同的上下文菜单,具体取决于您使用的编辑器。

在 PyQt 中,您有多种创建上下文菜单的选项。在本教程中,您将了解其中两个选项:

  1. 将contextMenuPolicy特定小部件的属性设置为Qt.ActionsContextMenu
  2. 通过处理应用程序窗口上的上下文菜单事件contextMenuEvent()

第一个选项是两者中最常见和用户友好的,因此您将首先了解它。

第二个选项稍微复杂一些,并且依赖于处理用户事件。在 GUI 编程中,事件是应用程序上的任何用户操作,例如单击按钮或菜单、从组合框中选择项目、在文本字段中输入或更新文本、按下键盘上的键等.

通过上下文菜单策略创建上下文菜单

所有派生自的 PyQt 图形组件或小部件都QWidget继承了一个名为contextMenuPolicy. 此属性控制小部件如何显示上下文菜单。此属性最常用的值之一是Qt.ActionsContextMenu。这使得小部件将其内部操作列表显示为上下文菜单。

要使小部件根据其内部操作显示上下文菜单,您需要运行两个步骤:

  1. 使用 向小部件添加一些操作QWidget.addAction()。
  2. 设置contextMenuPolicy于Qt.ActionsContextMenu上使用的小工具.setContextMenuPolicy()。

设置contextMenuPolicy为Qt.ActionsContextMenu使具有操作的小部件在上下文菜单中显示它们。这是使用 Python 和 PyQt 创建上下文菜单的一种非常快速的方法。

使用这种技术,您可以向示例应用程序的中央小部件添加上下文菜单,并为您的用户提供一种快速访问某些应用程序选项的方法。为此,您可以将以下方法添加到Window:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码class Window(QMainWindow):
# Snip...
def _createContextMenu(self):
# Setting contextMenuPolicy
self.centralWidget.setContextMenuPolicy(Qt.ActionsContextMenu)
# Populating the widget with actions
self.centralWidget.addAction(self.newAction)
self.centralWidget.addAction(self.openAction)
self.centralWidget.addAction(self.saveAction)
self.centralWidget.addAction(self.copyAction)
self.centralWidget.addAction(self.pasteAction)
self.centralWidget.addAction(self.cutAction)

在 中._createContextMenu(),您首先设置contextMenuPolicy为Qt.ActionsContextMenu使用 setter 方法.setContextMenuPolicy()。然后.addAction()像往常一样向小部件添加操作。最后一步是._createContextMenu()从 的初始化程序调用Window:

1
2
3
4
5
6
python复制代码class Window(QMainWindow):
"""Main Window."""
def __init__(self, parent=None):
# Snip...
self._createToolBars()
self._createContextMenu()

如果您在添加这些内容后运行示例应用程序,那么当您右键单击该应用程序的中央小部件时,您会看到它显示一个上下文菜单:

PyQt 上下文菜单策略

现在,您的示例应用程序有一个上下文菜单,只要您右键单击应用程序的中央小部件,就会弹出该菜单。中央小部件伸展以占据窗口中的所有可用空间,因此您不仅限于右键单击标签文本以查看上下文菜单。

最后,由于您在整个应用程序中使用相同的操作,上下文菜单上的选项显示相同的图标集。

通过事件处理创建上下文菜单

在 PyQt 中创建上下文菜单的另一种方法是处理应用程序主窗口的上下文菜单事件。为此,您需要运行以下步骤:

  1. 覆盖对象.contextMenuEvent()上的事件处理程序方法QMainWindow。
  2. 创建一个QMenu传递小部件(上下文小部件)作为其父对象的对象。
  3. 用动作填充菜单对象。
  4. 使用QMenu.exec()事件.globalPos()作为参数启动菜单对象。

这种管理上下文菜单的方式有点复杂。但是,它使您可以很好地控制调用上下文菜单时发生的情况。例如,您可以根据应用程序的状态等启用或禁用菜单选项。

注意:在继续本节之前,您需要禁用您在上一节中编写的代码。为此,只需转到的初始化程序Window并注释掉调用self._createContextMenu().

以下是如何重新实现示例应用程序的上下文菜单,覆盖主窗口对象上的事件处理程序方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码class Window(QMainWindow):
# Snip...
def contextMenuEvent(self, event):
# Creating a menu object with the central widget as parent
menu = QMenu(self.centralWidget)
# Populating the menu with actions
menu.addAction(self.newAction)
menu.addAction(self.openAction)
menu.addAction(self.saveAction)
menu.addAction(self.copyAction)
menu.addAction(self.pasteAction)
menu.addAction(self.cutAction)
# Launching the menu
menu.exec(event.globalPos())

在 中contextMenuEvent(),您首先创建一个QMenu对象 ( menu)centralWidget作为其父小部件。接下来,您使用.addAction. 最后,调用.exec()上QMenu的对象,以显示在屏幕上。

的第二个参数.contextMenuEvent()表示该方法捕获的事件。在这种情况下,event将右键单击应用程序的中央小部件。

在对 的调用中.exec(),您将其event.globalPos()用作参数。当用户单击 PyQt 窗口或小部件时,此方法返回鼠标指针的全局位置。鼠标位置将告诉.exec()窗口上显示上下文菜单的位置。

如果您使用这些新更改运行示例应用程序,那么您将获得与上一节中相同的结果。

组织上下文菜单选项

与菜单和工具栏不同,在上下文菜单中,您不能使用.addSeparator()添加分隔符并根据它们之间的关系在视觉上分隔菜单选项。在组织上下文菜单时,您需要创建一个分隔符操作:

1
2
scss复制代码separator = QAction(parent)
separator.setSeparator(True)

.setSeparator(True)对动作对象的调用将把该动作变成一个分隔符。完成分隔符操作后,您需要使用 将其插入上下文菜单中的正确位置QMenu.addAction()。

如果您回顾一下您的示例应用程序,那么您可能希望在视觉上将来自_File_菜单的选项与来自_Edit_菜单的选项分开。为此,您可以更新.contextMenuEvent():

1
2
3
4
5
6
7
8
9
10
11
12
ruby复制代码class Window(QMainWindow):
# Snip...
def contextMenuEvent(self, event):
# Snip...
menu.addAction(self.saveAction)
# Creating a separator action
separator = QAction(self)
separator.setSeparator(True)
# Adding the separator to the menu
menu.addAction(separator)
menu.addAction(self.copyAction)
# Snip...

在前两行突出显示的行中,您创建了分隔符操作。在第三个突出显示的行中,您使用 将分隔符操作添加到菜单中.addAction()。

这将在_文件_选项和_编辑_选项之间添加一条水平线。以下是添加此内容的上下文菜单的外观:

带分隔符的 PyQt 上下文菜单

现在,您的上下文菜单包含一条水平线,可直观地将来自_File_的选项与来自_Edit_的选项分开。这样,您改进了菜单的视觉质量并提供了更好的用户体验。

在菜单和工具栏中连接信号和插槽

在 PyQt 中,您使用信号和槽为 GUI 应用程序提供功能。每次在PyQt 小部件上发生诸如鼠标单击、按键或窗口大小调整等事件时,它们都会发出信号。

一个插槽是一个Python可调用,您可以连接到一个小部件的信号,以响应用户事件执行某些操作。如果连接了一个信号和一个插槽,那么每次发出信号时都会自动调用该插槽。如果给定的信号未连接到插槽,则在发出信号时不会发生任何事情。

为了让你的菜单选项和工具栏按钮在用户点击它们时启动一些操作,你需要将底层操作的信号与一些自定义或内置插槽连接起来。

QAction物体可以发出各种信号。但是,菜单和工具栏中最常用的信号是.triggered()。每次用户单击菜单选项或工具栏按钮时都会发出此信号。要.triggered()与插槽连接,您可以使用以下语法:

1
2
3
scss复制代码action = QAction("Action Text", parent)
# Connect action's triggered() with a slot
action.triggered.connect(slot)

在这个例子中,slot是一个 Python 可调用的。换句话说,slot可以是一个函数、一个方法、一个类或一个实现 的类的实例.__call__()。

您的示例应用程序中已经有一组操作。现在,您需要对每次用户单击菜单选项或工具栏按钮时调用的插槽进行编码。转到的定义Window并添加以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
python复制代码class Window(QMainWindow):
# Snip...
def newFile(self):
# Logic for creating a new file goes here...
self.centralWidget.setText("<b>File > New</b> clicked")

def openFile(self):
# Logic for opening an existing file goes here...
self.centralWidget.setText("<b>File > Open...</b> clicked")

def saveFile(self):
# Logic for saving a file goes here...
self.centralWidget.setText("<b>File > Save</b> clicked")

def copyContent(self):
# Logic for copying content goes here...
self.centralWidget.setText("<b>Edit > Copy</b> clicked")

def pasteContent(self):
# Logic for pasting content goes here...
self.centralWidget.setText("<b>Edit > Paste</b> clicked")

def cutContent(self):
# Logic for cutting content goes here...
self.centralWidget.setText("<b>Edit > Cut</b> clicked")

def helpContent(self):
# Logic for launching help goes here...
self.centralWidget.setText("<b>Help > Help Content...</b> clicked")

def about(self):
# Logic for showing an about dialog content goes here...
self.centralWidget.setText("<b>Help > About...</b> clicked")

这些方法将扮演示例应用程序的插槽的角色。每次用户单击相应的菜单选项或工具栏按钮时都会调用它们。

一旦有了提供功能的插槽,就需要将它们与动作的.triggered()信号连接起来。这样,应用程序将根据用户事件执行操作。要进行这些连接,请转到示例应用程序并将以下方法添加到Window:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码class Window(QMainWindow):
# Snip...
def _connectActions(self):
# Connect File actions
self.newAction.triggered.connect(self.newFile)
self.openAction.triggered.connect(self.openFile)
self.saveAction.triggered.connect(self.saveFile)
self.exitAction.triggered.connect(self.close)
# Connect Edit actions
self.copyAction.triggered.connect(self.copyContent)
self.pasteAction.triggered.connect(self.pasteContent)
self.cutAction.triggered.connect(self.cutContent)
# Connect Help actions
self.helpContentAction.triggered.connect(self.helpContent)
self.aboutAction.triggered.connect(self.about)

此方法会将您所有操作的.triggered()信号与其各自的插槽或回调连接起来。通过此更新,您的示例应用程序将在QLabel您设置为中央小部件的对象上显示一条消息,告诉您单击了哪个菜单选项或工具栏按钮。

在 的情况下exitAction,您将其triggered()信号与内置插槽连接QMainWindow.close()。这样,如果您选择_File → Exit_,那么您的应用程序将关闭。

最后,转到 的初始化程序Window并添加对 的调用._connectActions():

1
2
3
4
5
6
python复制代码class Window(QMainWindow):
"""Main Window."""
def __init__(self, parent=None):
# Snip...
# self._createContextMenu()
self._connectActions()

通过此最终更新,您可以再次运行该应用程序。以下是所有这些更改的工作原理:

PyQt 连接信号和插槽

如果单击菜单选项、工具栏按钮或上下文菜单选项,则应用程序窗口中央的标签会显示一条消息,指示已执行的操作。此功能在学习环境之外不是很有用,但它可以让您了解如何在用户与 GUI 交互时让您的应用程序执行现实世界的操作。

最后,当您选择_File → Exit 时_,应用程序将关闭,因为 的.triggered()信号exitAction已连接到内置插槽QMainWindow.close()。

作为练习,您可以尝试为_查找和替换_子菜单中的_查找…_和_替换…_选项创建自定义插槽,然后将它们的信号连接到这些插槽以使其生效。您还可以尝试使用您在本节中编写的插槽并尝试用它们做新的事情。.triggered()

动态填充 Python 菜单

为应用程序创建菜单时,有时需要使用创建应用程序 GUI 时未知的选项填充这些菜单。例如,文本编辑器中的“_打开最近”_菜单显示最近打开的文档列表。您无法在创建应用程序的 GUI 时填充此菜单,因为每个用户都会打开不同的文档,并且无法提前知道此信息。

在这种情况下,您需要动态填充菜单以响应用户操作或应用程序的状态。QMenu有一个称为.aboutToShow()您可以连接到自定义插槽的信号,以在菜单对象显示在屏幕上之前动态填充它。

要继续开发示例应用程序,假设您需要在_文件_下创建一个_打开最近的_子菜单,并用最近打开的文件或文档动态填充它。为此,您需要运行以下步骤:

  1. 在_File_下创建_Open 最近的_子菜单。
  2. 编写动态生成操作以填充菜单的自定义插槽。
  3. 将.aboutToShow()菜单信号与自定义插槽连接。

下面是创建子菜单的代码:

1
2
3
4
5
6
7
8
9
ruby复制代码class Window(QMainWindow):
# Snip...
def _createMenuBar(self):
# Snip...
fileMenu.addAction(self.openAction)
# Adding an Open Recent submenu
self.openRecentMenu = fileMenu.addMenu("Open Recent")
fileMenu.addAction(self.saveAction)
# Snip...

在突出显示的行中,您在“_文件”_菜单下添加一个标题为 的子菜单”Open Recent”。这个子菜单还没有菜单选项。您需要动态创建操作以填充它。

您可以通过编写一种方法来动态创建操作并将它们添加到子菜单来实现此目的。这是一个示例,显示了您可以使用的一般逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码from functools import partial
# Snip...

class Window(QMainWindow):
# Snip...
def populateOpenRecent(self):
# Step 1. Remove the old options from the menu
self.openRecentMenu.clear()
# Step 2. Dynamically create the actions
actions = []
filenames = [f"File-{n}" for n in range(5)]
for filename in filenames:
action = QAction(filename, self)
action.triggered.connect(partial(self.openRecentFile, filename))
actions.append(action)
# Step 3. Add the actions to the menu
self.openRecentMenu.addActions(actions)

在 中.populateOpenRecent(),首先使用 删除菜单中的旧选项(如果有).clear()。然后添加用于动态创建和连接操作的逻辑。最后,您使用 将操作添加到菜单中.addActions()。

在for循环中,您使用functools.partial()来连接.triggered()信号 ,.openRecentFile()因为您想filename作为参数传递给.openRecentFile()。当将信号与需要额外参数的插槽连接时,这是一种非常有用的技术。要使其正常工作,您需要partial()从functools.

注意:本示例第二步中的逻辑并没有真正加载最近打开的文件列表。它只是创建了list五个假设文件中的一个,其唯一目的是展示实现此技术的方法。

下一步是连接.aboutToShow()的信号.openRecentMenu到.populateOpenRecent()。为此,请在末尾添加以下行._connectActions():

1
2
3
4
5
6
7
ruby复制代码class Window(QMainWindow):
# Snip...
def _connectActions(self):
# Snip...
self.aboutAction.triggered.connect(self.about)
# Connect Open Recent to dynamically populate it
self.openRecentMenu.aboutToShow.connect(self.populateOpenRecent)

在突出显示的行中,您将.aboutToShow信号与连接.populateOpenRecent()。这可确保您的菜单在显示之前就被填充。

现在您需要编码.openRecentFile()。这是当您的用户单击任何动态创建的操作时您的应用程序将调用的方法:

1
2
3
4
5
ruby复制代码class Window(QMainWindow):
# Snip...
def openRecentFile(self, filename):
# Logic for opening a recent file goes here...
self.centralWidget.setText(f"<b>{filename}</b> opened")

此方法将更新QLabel您用作示例应用程序的中央小部件的对象的文本。

以下是动态创建的子菜单在实践中的工作方式:

PyQt 动态创建的菜单

当您的鼠标指针悬停在_打开最近_菜单上时,菜单会发出.aboutToShow()信号。这会导致调用.populateOpenRecent(),从而创建并连接操作。如果单击文件名,您将看到中央标签相应地更改以显示消息。

定义菜单和工具栏选项的键盘快捷键

键盘快捷键是 GUI 应用程序中的一项重要功能。键盘快捷键是一个组合键,您可以在键盘上按下它以快速访问应用程序中的一些最常见选项。

以下是键盘快捷键的一些示例:

  • Ctrl+ 将C某些内容复制到剪贴板。
  • Ctrl+V从剪贴板粘贴一些东西。
  • Ctrl+Z撤消上次操作。
  • Ctrl+O打开文件。
  • Ctrl+S保存文件。

在下面的部分中,您将学习如何向应用程序添加键盘快捷键以提高用户的工作效率和体验。

使用按键序列

到目前为止,您已经了解到这QAction是一个用于填充菜单和工具栏的多功能类。QAction还提供了一种用户友好的方式来定义菜单选项和工具栏按钮的键盘快捷键。

QAction实施**.setShortcut()**. 此方法将QKeySequence对象作为参数并返回键盘快捷键。

QKeySequence提供了几个构造函数。在本教程中,您将了解其中两个:

  1. **QKeySequence(ks, format)**将基于字符串的键序列 ( ks) 和格式 ( format) 作为参数并创建一个QKeySequence对象。
  2. **QKeySequence(key)**接受一个StandardKey常量作为参数并创建一个QKeySequence与底层平台上的键序列匹配的对象。

第一个构造函数识别以下字符串:

  • “Ctrl”
  • “Shift”
  • “Alt”
  • “Meta”

您可以通过将这些字符串与字母、标点符号、数字、命名键(Up、Down、Home)和功能键(”Ctrl+S”、”Ctrl+5”、”Alt+Home”、”Alt+F4”)组合来创建基于字符串的键序列。您最多可以在逗号分隔列表中传递四个基于字符串的键序列。

注:有关在不同平台上的标准快捷的完整参考,请参阅标准快捷键部分中的QKeySequence文档。

如果您正在开发多平台应用程序并希望坚持每个平台的标准键盘快捷键,则第二个构造函数很方便。例如,QKeySequence.Copy将返回用于将对象复制到剪贴板的平台标准键盘快捷键。

**注意:**有关 PyQt 提供的标准密钥的完整参考,请参阅QKeySequence.StandardKey 文档。

有了关于如何在 PyQt 中为操作定义键盘快捷键的一般背景,您可以返回示例应用程序并添加一些快捷键。为此,您需要更新._createActions():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码from PyQt5.QtGui import QKeySequence
# Snip...

class Window(QMainWindow):
# Snip...
def _createActions(self):
# File actions
# Snip...
# Using string-based key sequences
self.newAction.setShortcut("Ctrl+N")
self.openAction.setShortcut("Ctrl+O")
self.saveAction.setShortcut("Ctrl+S")
# Edit actions
# Snip...
# Using standard keys
self.copyAction.setShortcut(QKeySequence.Copy)
self.pasteAction.setShortcut(QKeySequence.Paste)
self.cutAction.setShortcut(QKeySequence.Cut)
# Snip...

您首先需要导入QKeySequence. 在里面._createActions(),前三个突出显示的行使用基于字符串的键序列创建键盘快捷键。这是向您的操作添加键盘快捷键的快速方法。在后三个突出显示的行中,您用于QKeySequence提供标准键盘快捷键。

如果您运行带有这些添加的示例应用程序,那么您的菜单将如下所示:

PyQt 键盘快捷键

您的菜单选项现在会在其右侧显示键盘快捷键。如果您按这些组合键中的任何一个,那么您将执行相应的操作。

使用键盘加速器

您可以使用另一种替代方法将键盘快捷键或键盘加速器添加到应用程序的菜单选项中。

您可能已经注意到,当您为菜单或菜单选项设置文本时,通常会&在文本中插入一个与符号 ( )。这样做是为了当显示在菜单或菜单选项的文本中时,紧跟在&符号之后的字母将带有下划线。例如,如果您在“_文件”_菜单 ( )的标题中的字母_F_之前放置一个与号,则在显示菜单标题时_F_将带有下划线。”&File”

注意:如果您需要在菜单文本上显示与号符号,则需要使用双与号 ( &&) 来逃避此符号的默认功能。

在菜单栏的情况下,使用与号允许您通过Alt与菜单标题中带下划线的字母组合按下来调用任何菜单。

启动菜单后,您可以通过按选项文本中带下划线的字母来访问任何菜单选项。例如,在_文件中,_您可以通过按字母_E_访问_退出_选项。

注意:当您使用与号来提供键盘加速器时,请记住在同一菜单下不能有两个选项共享相同的访问字母。

如果您将_C_设置为_Copy_选项的访问字母,则不能将_C_设置为_Cut_选项的访问字母。换句话说,在给定的菜单下,访问字母必须是唯一的。

此功能将允许您为喜欢使用键盘来处理您的应用程序的用户提供快速键盘加速器。此技术对于不提供显式键盘快捷键的选项特别有用。

创建菜单和工具栏:最佳实践和技巧

当您使用 Python 和 PyQt 创建菜单和工具栏时,您应该遵循一些通常被认为是 GUI 编程最佳实践的标准。这是一个快速列表:

  1. **按照普遍接受的顺序排列菜单。**例如,如果您有一个_文件_菜单,那么它应该是从左到右的第一个菜单。如果你有一个_编辑_菜单,那么它应该是第二个。_帮助_应该是最右边的菜单,依此类推。
  2. **使用您正在开发的应用程序类型的常用选项填充您的菜单。**例如,在文本编辑器中,文件_菜单通常包括诸如_New、Open、Save_和_Exit 之类的选项。编辑_菜单通常包括_复制、粘贴、剪切、_撤消_等选项。
  3. **对常用选项使用标准键盘快捷键。**例如,使用Ctrl+C进行_复制_,Ctrl+V用于_粘贴_,Ctrl+X用于_切割_,等等。
  4. **使用分隔符分隔不相关的选项。**这些视觉提示将使您的应用程序更易于导航。
  5. **将省略号 ( …)添加到启动其他对话框的选项的标题。**例如,使用_Save As…而不是_Save As,使用_About…而不是_About,等等。
  6. **&在菜单选项中使用与号 ( ) 来提供方便的键盘加速器。**例如,”&Open代替”Open”,”&Exit”代替”Exit”。

如果您遵循这些准则,那么您的 GUI 应用程序将为您的用户提供熟悉且诱人的体验。

在 PyQt 中构建 Python 状态栏

甲状态栏是水平面板通常在GUI应用程序放置在底部的主窗口。它的主要目的是显示有关应用程序当前状态的信息。状态栏也可以分为多个部分,以显示每个部分的不同信息。

根据Qt 文档,状态指示器分为三种类型:

  1. 临时指示器会在短时间内占据几乎整个状态栏以显示工具提示文本、菜单项和其他时间敏感信息。
  2. 普通指示器占据状态栏的一部分并显示用户可能希望定期参考的信息,例如文字处理器中的字数统计。这些可能会被临时指标暂时隐藏。
  3. 永久指示器始终显示在状态栏中,即使临时指示器被激活也是如此。它们用于显示有关应用程序当前模式的重要信息,例如按下 Caps Lock 键的时间。

您可以使用以下选项之一向主窗口样式的应用程序添加状态栏:

  • 调用.statusBar()你的QMainWindow对象。.statusBar()创建并返回主窗口的空状态栏。
  • 创建一个QStatusBar对象,然后.setStatusBar()使用状态栏对象作为参数调用主窗口。这样,.setStatusBar()将您的状态栏对象设置为主窗口的状态栏。

在这里,您有两种替代实现来向示例应用程序添加状态栏:

1
2
3
4
5
6
7
8
ruby复制代码# 1. Using .statusBar()
def _createStatusBar(self):
self.statusbar = self.statusBar()

# 2. Using .setStatusBar()
def _createStatusBar(self):
self.statusbar = QStatusBar()
self.setStatusBar(self.statusbar)

两种实现产生相同的结果。但是,大多数情况下,您将使用第一个实现来创建状态栏。请注意,要使第二个实现工作,您需要QStatusBar从PyQt5.QtWidgets.

将上述实现之一添加到您的应用程序Window,然后调用._createStatusBar()类初始值设定项。通过这些添加,当您再次运行您的应用程序时,您将看到一个如下所示的窗口:

PyQt 状态栏

您的应用程序现在在其主窗口底部有一个状态栏。状态栏几乎不可见,但如果仔细观察,您会注意到窗口右下角有一个小的虚线三角形。

显示临时状态消息

状态栏的主要目的是向应用程序的用户显示状态信息。要在状态栏中显示临时状态消息,您需要使用QStatusBar.showMessage(). 此方法采用以下两个参数:

  1. message 将状态指示消息作为字符串保存。
  2. timeout 保存消息将显示在状态栏上的毫秒数。

如果timeout是0,这是其默认值,则消息将保留在状态栏上,直到您调用.clearMessage()或.showMessage()状态栏上。

如果您的状态栏上有一条活动消息并且您.showMessage()用新消息呼叫,那么新消息将掩盖或替换旧消息。

转到您的示例应用程序并将以下行添加到._createStatusBar():

1
2
3
4
5
6
ruby复制代码class Window(QMainWindow):
# Snip...
def _createStatusBar(self):
self.statusbar = self.statusBar()
# Adding a temporary message
self.statusbar.showMessage("Ready", 3000)

最后一行._createStatusBar()将使您的应用程序Ready在应用程序的状态栏上显示一条消息3000几毫秒:

PyQt 状态消息

运行应用程序时,状态栏会显示消息Ready。之后3000毫秒,此消息消失,状态栏被清除,并准备展现出新的状态信息。

在状态栏中显示永久消息

您还可以在应用程序的状态栏上显示永久消息。一条永久消息让用户了解应用程序的一些一般状态。例如,在文本编辑器中,您可能希望显示一条永久消息,其中包含有关当前打开文件的文本编码的信息。

要将永久消息添加到状态栏,请使用QLabel对象来保存消息。然后通过调用将标签添加到状态栏.addPermanentWidget()。此方法将给定的小部件永久添加到当前状态栏。小部件的父级设置为状态栏。

.addPermanentWidget() 采用以下两个参数:

  1. widget保存要添加到状态栏的小部件对象。这个角色的一些常用小部件QLabel,QToolButton以及QProgressBar。
  2. stretch用于随着状态栏的增长和收缩计算小部件的合适大小。它默认为0,这意味着小部件将占用最少的空间。

请记住,永久小部件不会被临时消息遮蔽或替换。.addPermanentWidget()在状态栏的右侧定位小部件。

注意:您.addPermanentWidget()不仅可以使用在状态栏上显示永久消息,还可以向用户显示进度条以监控给定操作的持续时间。您还可以在状态栏上提供按钮,以允许用户在文本编辑器上更改文件编码等属性。

当您在状态栏上使用这些类型的小部件时,尽量坚持使用最常用的小部件来满足您正在开发的应用程序类型。这样,您的用户就会有宾至如归的感觉。

假设您想将示例应用程序转换为文本编辑器,并且您想向状态栏添加一条消息,以显示有关当前文件字数的信息。为此,您可以创建一个调用的方法.getWordCount(),然后使用.addPermanentWidget()和QLabel对象添加永久消息:

1
2
3
4
5
ruby复制代码class Window(QMainWindow):
# Snip...
def getWordCount(self):
# Logic for computing the word count goes here...
return 42

该方法添加了计算当前打开文档中字数的逻辑。现在,您可以将此信息显示为永久消息:

1
2
3
4
5
6
7
8
9
ruby复制代码class Window(QMainWindow):
# Snip...
def _createStatusBar(self):
self.statusbar = self.statusBar()
# Adding a temporary message
self.statusbar.showMessage("Ready", 3000)
# Adding a permanent message
self.wcLabel = QLabel(f"{self.getWordCount()} Words")
self.statusbar.addPermanentWidget(self.wcLabel)

在最后两行中,您首先创建一个QLabel对象 ( wcLabel) 来保存有关字数的消息。要创建消息,请使用f-string,在其中插入对 的调用.getWordCount()以获取字数信息。然后使用 将标签添加到状态栏.addPermanentWidget()。

在这种情况下,您将QLabel对象创建为实例属性,因为需要根据用户对当前文件所做的更改来更新字数。

如果您使用此更新运行应用程序,那么您将在状态栏的右侧看到字数统计消息:

带有永久小部件的 PyQt 状态栏

状态栏会显示一条消息,通知用户假设当前文件中的字数。在状态栏中向用户显示永久信息或其他选项的能力非常有用,可以帮助您极大地改善应用程序的用户体验。

向操作添加帮助提示

在创建 GUI 应用程序时,向用户提供有关应用程序界面特定功能的帮助提示非常重要。帮助提示是短消息,可为用户提供有关应用程序提供的某些选项的快速指南。

PyQt 操作允许您定义以下类型的帮助提示:

  • 状态提示是当用户将鼠标指针悬停在菜单选项或工具栏按钮上时应用程序显示在状态栏上的帮助提示。默认情况下,状态提示包含一个空字符串。
  • 工具提示是当用户将鼠标指针悬停在工具栏按钮或小部件上时应用程序显示为浮动消息的帮助提示。默认情况下,工具提示包含标识手头操作的文本。

注意: PyQt 还提供了What’s This帮助提示,您可以在小部件和动作中使用它来显示对小部件或动作提供的功能的更丰富的描述。但是,该主题超出了本教程的范围。

要了解帮助提示的工作原理,您可以向示例应用程序添加一些状态提示和工具提示。转到._createActions()并添加以下代码行:

1
2
3
4
5
6
7
8
9
10
11
12
13
ruby复制代码class Window(QMainWindow):
# Snip...
def _createActions(self):
# File actions
# Snip...
self.saveAction.setShortcut("Ctrl+S")
# Adding help tips
newTip = "Create a new file"
self.newAction.setStatusTip(newTip)
self.newAction.setToolTip(newTip)
# Edit actions
self.copyAction = QAction(QIcon(":edit-copy.svg"), "&Copy", self)
# Snip...

三个突出显示的行将消息设置”Create a new file”为“_新建”_选项的状态和工具提示。如果您现在运行该应用程序,您将看到_New_选项向用户显示了一个简短但描述性的帮助提示:

当您单击_File_菜单并将鼠标指针放在_New 上时_,您可以看到状态栏左侧显示的帮助提示消息。另一方面,如果您将鼠标指针移到“_新建”_工具栏按钮上,则您可以在状态栏上看到消息,也可以在鼠标指针旁边看到一个小的浮动框。

通常,向 Python 菜单和工具栏添加帮助提示被认为是最佳实践。它将使您的 GUI 应用程序更易于用户导航和学习。作为最后的练习,您可以继续向示例应用程序的其余操作添加帮助提示,并查看完成后的效果。

结论

菜单、工具栏和状态栏是大多数GUI 应用程序的常见且重要的图形组件。您可以使用它们为您的用户提供一种快速访问应用程序选项和功能的方法。它们还使您的应用程序看起来精美和专业,并为您的用户提供出色的体验。

在本教程中,您学习了如何:

  • 以编程方式创建菜单、工具栏和状态栏
  • 使用 PyQt操作填充菜单和工具栏
  • 使用状态栏提供状态信息

在此过程中,您学习了一些在 GUI 应用程序中添加和使用菜单、工具栏和状态栏时值得考虑的最佳编程实践。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringCloudAlibaba全网最全讲解6️⃣之Se

发表于 2021-08-28

这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战

🌈专栏简介

**感谢阅读,希望能对你有所帮助,博文若有瑕疵请在评论区留言或在主页个人介绍中添加我私聊我,感谢每一位小伙伴不吝赐教。我是XiaoLin,既会写bug也会唱rap的男孩,这个专栏主要是介绍目前微服务最主流的解决方案,SpringCloudAlibaba,将会分组件介绍。专栏地址: [SpringCloudAlibaba](https://juejin.cn/column/7001291481705086990)。**
  • SpringCloudAlibaba全网最全讲解5️⃣之Feign(建议收藏)
  • SpringCloudAlibaba全网最全讲解4️⃣之Ribbon(建议收藏)
  • SpringCloudAlibaba全网最全讲解3️⃣之Nacos(建议收藏)
  • SpringCloudAlibaba全网最全讲解2️⃣(建议收藏)
  • SpringCloudAlibaba全网最全讲解1️⃣(建议收藏)

八、流量防护:Sentinel

8.1、高并发带来的问题

在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。

8.2、模拟高并发

8.2.1、编写SentinelController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RestController
public class SentinelController {

@RequestMapping("/sentinel1")
public String sentinel1(){
//模拟一次网络延时
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "sentinel1";
}
@RequestMapping("/sentinel2")
public String sentinel2(){
return "测试高并发下的问题";
}
}

8.2.2、修改Tomcat的并发数

1
2
3
yaml复制代码  tomcat:
threads:
max: 10 #tomcat的最大并发值修改为10,

8.2.3、使用压力测试模拟高并发

下载地址jmeter.apache.org/

修改配置,支持中文

进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN。

image-20210505214428132

然后点击jmeter.bat启动软件。

image-20210505214459838

添加线程组

image-20201029120234401

image-20201029124003847

添加http请求

image-20201029122851926

image-20210505214830428

访问

我们去访问http://localhost:8082/sentinel2,会发现一直在转圈,这就是服务器雪崩的雏形。

8.3、服务器雪崩

在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。


由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “**雪崩效应**” 。


服务器一步步雪崩的流程如下:

image-20210506110941736

服务器的雪崩效应其实就是由于某个微小的服务挂了,导致整一大片的服务都不可用.类似生活中的雪崩效应,由于落下的最后一片雪花引发了雪崩的情况.


雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。


雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。

8.4、常见解决方案

要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措施, 下面介绍常见的服务容错思路和组件。


常见的容错思路有隔离、超时、限流、熔断、降级这几种。

8.4.1、隔离机制

比如服务A内总共有100个线程, 现在服务A可能会调用服务B,服务C,服务D.我们在服务A进行远程调用的时候,给不同的服务分配固定的线程,不会把所有线程都分配给某个微服务. 比如调用服务B分配30个线程,调用服务C分配30个线程,调用服务D分配40个线程. 这样进行资源的隔离,保证即使下游某个服务挂了,也不至于把服务A的线程消耗完。比如服务B挂了,这时候最多只会占用服务A的30个线程,服务A还有70个线程可以调用服务C和服务D。

image-20201029142100450

8.4.2、超时机制

在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。

image-20201029143237828

8.4.3、限流机制

限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。

image-20201029143206491

8.4.4、熔断机制

在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。

服务熔断一般有三种状态:

  1. 熔断关闭状态(Closed):服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制。
  2. 熔断开启状态(Open):后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法。
  3. 半熔断状态(Half-Open):尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。

image-20201029143128555

8.4.5、降级机制

降级其实就是为服务提供一个兜底方案,一旦服务无法正常调用,就使用兜底方案。

image-20201029143456888

8.5、常见的熔断组件

8.5.1、Hystrix

Hystrix是由Netflflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。

8.5.2、Resilience4J

Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。

8.5.3、Sentinel

Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。

8.6、Sentinel实战

8.6.1、什么是Sentinel

Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于**服务容错**的综合性解决方案。它以流量为切入点, 从**流量控制、熔断降级、系统负载保护**等多个维度来保护服务的稳定性。

Sentinel 具有以下特征:

  1. 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景, 例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  2. 完备的实时监控:Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒级数据, 甚至 500 台以下规模的集群的汇总运行情况。
  3. 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块, 例如与 SpringCloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。

8.6.2、Sentinel组成部分

Sentinel分为两部分:
  1. 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo /Spring Cloud 等框架也有较好的支持。
  2. 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

8.7、集成Sentinel

微服务集成Sentinel非常简单, 只需要加入Sentinel的依赖即可。

8.7.1、加入依赖

1
2
3
4
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

8.7.2、编写控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RestController
public class SentinelController {

@RequestMapping("/sentinel1")
public String sentinel1(){
//模拟一次网络延时
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "sentinel1";
}
@RequestMapping("/sentinel2")
public String sentinel2(){
return "测试高并发下的问题";
}
}

8.7.3、安装Sentinel控制台

下载jar包

Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。我们需要去[下载Sentinel控制台的jar包](https://github.com/alibaba/Sentinel/releases)。

修改application.yml

1
2
3
4
5
6
yaml复制代码spring:
cloud:
sentinel:
transport:
port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:8080 # 指定控制台服务的地址

启动控制台

1
2
shell复制代码# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目) 
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.0.jar

测试

通过浏览器访问localhost:8080 进入控制台 ( 默认用户名密码是 sentinel/sentinel )

image-20210506114024909

8.7.4、控制台的原理

Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上,即在微服务中指定控制台的地址, 并且还要开启一个跟控制台传递数据的端口, 控制台也可以通过此端口调用微服务中的监控程序获取微服务的各种信息。

image-20210506113704987

8.8、实现一个接口限流

点击簇点链路->流控

image-20210506114220936

在单机阈值中写数值

在单机阈值填写一个数值,表示每秒上限的请求数

image-20210506114332147

测试

快速访问几次,可以发现出错了。

image-20210506114408464

8.9、Sentinel基本概念和功能

8.9.1、基本概念

8.9.1.1、资源

资源就是Sentinel要保护的东西。资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,可以是一个服务,也可以是一个方法,甚至可以是一段代码。


我们上面例子的一个sentinel2方法就是一个资源。

8.9.1.2、规则

规则就是用来定义如何进行保护资源的。作用在资源之上, 定义以什么样的方式保护资源,主要包括流量控制规则、熔断降级规则以及系统保护规则。


我们上面的例子给sentinel2增加流控规则,限制了sentinel2的流量。

8.9.2、重要功能

image-20210506115159276

Sentinel的主要功能就是容错,主要体现为下面这三个:
  • 流量控制

流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是
随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。
Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。

  • 熔断降级

当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则
对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。

Sentinel 对这个问题采取了两种手段:

+ 通过并发线程数进行限制:Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收求。
+ 通过响应时间对资源进行降级:除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
  • 系统负载保护

Sentinel 同时提供系统维度的自适应保护能力。当系统负载较高的时候,如果还持续让
请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其
它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保
护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请
求。

**总结:我们需要做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能。**

8.10、Sentinel流控规则

流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

image-20210506140317192

资源名:唯一名称,默认是请求路径,可自定义。


针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制。


阈值类型/单机阈值:
  • QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流。
  • 线程数:当调用该接口的线程数达到阈值的时候,进行限流。

8.10.1、线程数限流

前面我们已经测试过了QPS限流,所以现在我们改为线程数限流。

8.10.1.1、添加流控规则

image-20210506141529265

8.10.1.2、在Jmeter中新增线程

image-20210506141801025

image-20210506141733911

8.10.1.3、测试

image-20210506141941996

8.10.2、流控模式

点击上面设置流控规则的**编辑**按钮,然后在编辑页面点击**高级选项**,会看到有流控模式一栏。

image-20210506142200222

他有三种流控模式:

  1. 直接(默认):接口达到限流条件时,开启限流。
  2. 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]。
  3. 链路:当从某个接口过来的资源达到限流条件时,开启限流

8.10.2.1、关联流控模式

关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流。


比如:当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢。


我们测试的时候可以关联sentinel1这个资源。

image-20210506142740700

我们使用Jmeter软件连续向/sentinel1连续发送请求,注意QPS一定要大于2,我们访问/sentinel2的时候发现被限流了。

image-20210506143303102

8.10.2.2、链路流控模式

链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。

修改application.yml

1
2
3
4
java复制代码spring:
cloud:
sentinel:
web-context-unify: false

TraceServiceImpl

1
2
3
4
5
6
7
8
java复制代码@Service
@Slf4j
public class TraceServiceImpl {
@SentinelResource(value = "tranceService")
public void tranceService(){
log.info("调用tranceService方法");
}
}

新增TraceController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RestController
public class TraceController {
@Autowired
private TraceServiceImpl traceService;
@RequestMapping("/trace1")
public String trace1(){
traceService.tranceService();
return "trace1";
}
@RequestMapping("/trace2")
public String trace2(){
traceService.tranceService();
return "trace2";
}
}

重新启动订单服务并添加链路流控规则

image-20210506144520611

测试

我们去访问 /trace1 和 /trace2 访问, 发现/trace2没问题, /trace1的被限流了。

image-20210506144645394

image-20210506144629866

8.10.3、流控效果

  1. 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果。
  2. Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的
    1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。
  3. 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设
    置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。

8.11、Sentinel降级规则

降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:
  • 慢调用比例: 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例: 当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

8.11.1、慢调用比例

新增FallBackController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RestController
@Slf4j
public class FallBackController {
@RequestMapping("/fallBack1")
public String fallBack1(){
try {
log.info("fallBack1执行业务逻辑");
//模拟业务耗时
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "fallBack1";
}
}

新增降级规则

image-20210506150006903

上面配置表示,如果在1S之内,有【超过1个的请求】且这些请求中【响应时间>最大RT】的【请求数量比例>10%】,就会触发熔断,在接下来的10s之内都不会调用真实方法,直接走降级方法。


比如: 最大RT=900,比例阈值=0.1,熔断时长=10,最小请求数=10
  • 情况1: 1秒内的有20个请求,只有10个请求响应时间>900ms, 那慢调用比例=0.5,这种情况就会触发熔断。
  • 情况2: 1秒内的有20个请求,只有1个请求响应时间>900ms, 那慢调用比例=0.05,这种情况不会触发熔断。
  • 情况3: 1秒内的有8个请求,只有6个请求响应时间>900ms, 那慢调用比例=0.75,这种情况不会触发熔断,因为最小请求数这个条件没有满足。
**我们做实验的时候把最小请求数设置为1,因为在1秒内,手动操作很难在1s内发两个请求过去,所以要做出效果,最好把最小请求数设置为1。**

8.11.2、异常数

在方法中新增一个异常

在Shop-order-server项目的FallBackController.java类新增fallBack3方法。
1
2
3
4
5
6
7
8
java复制代码  @RequestMapping("/fallBack3")
public String fallBack3(String name){
log.info("fallBack3执行业务逻辑");
if("xiaolin".equals(name)){
throw new RuntimeException();
}
return "fallBack3";
}

配置降级规则

image-20210506150810028

在1s之内,,有【超过3个的请求】,请求中超过2个请求出现异常就会触发熔断,熔断时长为10s。

测试

image-20210506150845623

8.12、Sentinel热点规则

热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制。
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制.
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

新增HotSpotController

一定需要在请求方法上贴@SentinelResource注解,否则热点规则无效

1
2
3
4
5
6
7
8
9
10
java复制代码@RestController
@Slf4j
public class HotSpotController {
@RequestMapping("/hotSpot1")
@SentinelResource(value = "hotSpot1")
public String hotSpot1(Long productId){
log.info("访问编号为:{}的商品",productId);
return "hotSpot1";
}
}

新增热点规则

因为我们就一个参数,所以参数索引是0。

image-20210506152252494

访问一下/hotSpot1,再编辑热点规则

**添加后再去热点规则中编辑规则,在编辑之前一定要先访问一下/hotSpot1,不然参数规则无法新增。**

image-20210506152452142

新增参数规则

image-20210506153033157

image-20210506153054676

测试

访问:[http://localhost:8082/hotSpot1?productId=2,无论怎么样访问都无济于事。](http://localhost:8082/hotSpot1?productId=2%EF%BC%8C%E6%97%A0%E8%AE%BA%E6%80%8E%E4%B9%88%E6%A0%B7%E8%AE%BF%E9%97%AE%E9%83%BD%E6%97%A0%E6%B5%8E%E4%BA%8E%E4%BA%8B%E3%80%82)


访问:[http://localhost:8082/hotSpot1?productId=1,多次访问后会降级。](http://localhost:8082/hotSpot1?productId=1%EF%BC%8C%E5%A4%9A%E6%AC%A1%E8%AE%BF%E9%97%AE%E5%90%8E%E4%BC%9A%E9%99%8D%E7%BA%A7%E3%80%82)

image-20210506153210123

8.13、Sentinel授权规则

很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
  1. 若配置白名单,则只有请求来源位于白名单内时才可通过;
  2. 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。

新增一个工具类,定义请求来源如何获取

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
/**
* 定义从请求的什么地方获取来源信息
* 比如我们可以要求所有的客户端需要在请求头中携带来源信息
*/
String serviceName = request.getParameter("serviceName");
return serviceName;
}
}

新增AuthController

1
2
3
4
5
6
7
8
9
java复制代码@RestController
@Slf4j
public class AuthController {
@RequestMapping("/auth1")
public String auth1(String serviceName){
log.info("应用:{},访问接口",serviceName);
return "auth1";
}
}

新增规则

image-20210506155905910

测试

访问http://localhost:8082/auth1?serviceName=pc不能访问

访问http://localhost:8082/auth1?serviceName=app可以访问

image-20210506160038198

8.14、系统规则

系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。


系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。
  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发。
  • 系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
  • CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。

8.15、自定义异常返回

常见的异常大致分为这几类:
  1. FlowException:限流异常 。
  2. DegradeException:降级异常。
  3. ParamFlowException:参数限流异常。
  4. AuthorityException:授权异常。
  5. SystemBlockException:系统负载异常。
在Shop-order-server项目中定义异常返回处理类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
response.setContentType("application/json;charset=utf-8");
ResultData data = null;
if (e instanceof FlowException) {
data = new ResultData(-1, "接口被限流了");
} else if (e instanceof DegradeException) {
data = new ResultData(-2, "接口被降级了");
}else if (e instanceof ParamFlowException) {
data = new ResultData(-3, "参数限流异常");
}else if (e instanceof AuthorityException) {
data = new ResultData(-4, "授权异常");
}else if (e instanceof SystemBlockException) {
data = new ResultData(-5, "接口被降级了...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor
//无参构造
class ResultData {
private int code;
private String message;
}

image-20210506171904602

8.16、@SentinelResource的使用

@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。主要参数如下:
属性 作用
value 资源名称,必需项(不能为空)
entryType entry 类型,可选项(默认为 EntryType.OUT)
blockHandler/blockHandlerClass blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallback/fallbackClass fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:1. 返回值类型必须与原函数返回值类型一致; 2.方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。3.fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback 默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:1. 返回值类型必须与原函数返回值类型一致;2. 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。 3. defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
直接将限流或降级后执行的方法。

image-20210506173330519

8.17、 Sentinel规则持久化

通过前面的讲解,我们已经知道,可以通过Dashboard来为每个Sentinel客户端设置各种各样的规则,但是这里有一个问题,就是这些规则默认是存放在内存中,极不稳定,所以需要将其持久化。


本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则。以本地文件数据源为例,推送过程如下图所示:

image-20201030135911029

首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。

编写处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
java复制代码public class FilePersistence implements InitFunc {
@Value("${spring.application.name}")
private String appcationName;

@Override
public void init() throws Exception {
String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + appcationName;
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";

this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);

// 流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);

// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);

// 系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);

// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
authorityRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);

// 热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}

private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);

private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);

private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);

private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}

private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
之后我们重启发现配置的规则还在,说明持久化成功!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…546547548…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%