Spring Cloud 之RabbitMQ的学习【详细】

news2024/11/25 0:36:00

服务通信

分布式系统通信两种方式:

  1. 直接远程调用(同步)
  2. 借助第三方间接通信(异步)

同步通讯的问题

Feign就属于同步通讯。存在的如下问题

  • 耦合度高,每次添加新的模块就要修改原有模块的代码
  • 性能下降,调用者需要等待服务者返回的结果,如果调用链过长,则响应的时间越长
  • 资源浪费,在等待的过程中,不会释放CPU与内存资源,在高并发的场景下占用浪费资源过大
  • 级联失败,当调用链中一个服务宕机,那么调用者也会出现问题。

异步调用方案

异步调用常见的实现方式为事件驱动模式

事件驱动模式优点:

  • 服务解耦,添加模块不需要更改其他服务的代码
  • 性能提升,在用户请求的模块可以直接返回结果,不需要等待其他服务执行完毕后再返回结果
  • 服务没有强依赖关系,一个服务宕机不会影响到其他服务
  • 流量削峰

缺点:

  • 依赖了第三方组件,第三方组件需要保证可靠性、安全性、吞吐能力
  • 架构复杂,业务没有明显流程线,不好追踪管理
  • 一致性问题

MQ

MQ:Message Queue消息队列,是消息在传输的过程中保存消息的容器。多用于分布式系统之间进行通信

Kafka适用于数据量大但对数据安全性不高的场景比如说日志的传输

RabbitMQ与RocketMQ适用于对数据安全要求较高的场景,比如说业务之间的传输信息

满足什么条件才可以使用MQ?

  1. 生产者不需要从消费者处获取任何信息
  2. 容许短暂不一致性
  3. 使用MQ的效果收益大于管理MQ成本

RabbitMQ的下载

在虚拟机上启动dokcer服务后拉去rabbitmq镜像

systemctl start docker
docker pull rabbitmq

RabbitMQ的启动

docker run \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=admin \
--name mq \
--hostname mql \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:latest

命令解释:

-e RABBITMQ_DEFAULT_USER=admin :指定登录账号为admin

-e RABBITMQ_DEFAULT_PASS=admin :指定登录密码为admin

--name mq :容器名为mq

--hostname mq1 主机名为mq1(做集群时使用,不添加也可以)

-p 15672:15672 端口映射

-p 5672:5672

-d 后台允许

rabbitmq:latest

访问15672端口输入密码登录

可能会遇到的问题

1、关闭防火墙后访问端口仍然无法访问15672端口

解决方法:

开启防火墙
systemctl start firewalld
开放端口
firewall-cmd --zone=public --add-port=15672/tcp --permanent
重新加载配置文件
firewall-cmd --reload

2、即使开放了端口15672也无法访问页面

解决方法:

如果是docker拉取的rabbitmq镜像,需要手动进入容器下载rabbitmq的管理插件

进入容器
docker exec -it 容器名 bash
下载rabbitmq的管理插件
rabbitmq-plugins enable rabbitmq_management
修改配置文件
cd  /etc/rabbitmq/conf.d/
echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
退出镜像
exit
重启rabbitmq
docker restart 容器名

RabbitMQ的结构与概念

RabbitMQ中的几个概念

  • channel:操作MQ的工具
  • exchange:路由消息到队列中
  • queue:缓存消息
  • virtualhost:虚拟主机,是对queue、exchange等资源的逻辑分组

常见消息模型

不使用交换机的

  • 基本消息队列
  • 工作消息队列

使用交换机的

  • Fanout Exchange:广播
  • Direct Exchange:路由
  • Topic Exchange:主题

简单消息队列的实现

只存在三种角色:

  • publisher:消息发布者,将消息发送到队列queue
  • queue:消息队列,负责接受并缓存消息
  • consumer:订阅队列,处理队列中的消息

示例代码:

引入依赖

<dependencies>
  <!--rabbitMQ的Java客户端-->
  <dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.16.0</version>
  </dependency>
</dependencies>
/**
 * 发送消息方
 */
public class Producer_Hello {
    public static void main(String[] args) throws IOException, TimeoutException {

        //1、创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2、设置参数
        connectionFactory.setHost("192.168.116.131");
        connectionFactory.setPort(5672);//默认也是5672
        connectionFactory.setVirtualHost("/");//设置虚拟机 默认值是/
        connectionFactory.setUsername("admin");//默认值是guest
        connectionFactory.setPassword("admin");//默认值是guest
        //3、创建连接Connection
        Connection connection = connectionFactory.newConnection();
        //4、创建Channel
        Channel channel = connection.createChannel();
        //5、创建队列Queue
        /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         * 参数
         * 1.queue:队列名称
         * 2.durable:是否持久化
         * 3.exclusive:
         *      *是否独占。只能有一个消费者监听这队列当
         *      *Connection关闭时,是否删除队列全
         * 4.autoDelete:是否自动删除。当没有Consumer时,自动删除掉
         * 5.arguments:参数。
         */
        //如果没有一个交helloWorld的队列,那么会自动创建一个
        channel.queueDeclare("hello_World",true,false,false,null);
        //6、发送消息
        /**
         * String exchange, String routingKey, BasicProperties props, byte[] body
         * 1、exchange交换机(简单模式下不会使用交换机,默认使用"")
         * 2、routingKey:路由名称
         * 3、props:配置信息
         * 4、body:发送消息数据
         */
        String body="Hello";
        channel.basicPublish("","hello_World",null,body.getBytes());

        //7、释放资源
        channel.close();
        connection.close();
    }
}

首先看到目前没有连接

打断点启动

当Connection connection = connectionFactory.newConnection()运行结束后。查看控制台连接信息

接下来启动消费者

public class Consumer_Hello {
    public static void main(String[] args) throws Exception {
        //1、创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2、设置参数
        connectionFactory.setHost("192.168.116.131");
        connectionFactory.setPort(5672);//默认也是5672
        connectionFactory.setVirtualHost("/");//设置虚拟机 默认值是/
        connectionFactory.setUsername("admin");//默认值是guest
        connectionFactory.setPassword("admin");//默认值是guest
        //3、创建连接Connection
        Connection connection = connectionFactory.newConnection();
        //4、创建Channel
        Channel channel = connection.createChannel();
        //5、创建队列Queue
        /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         * 参数
         * 1.queue:队列名称
         * 2.durable:是否持久化,当mq重启之后,还在
         * 3.exclusive:
         *      *是否独占。只能有一个消费者监听这队列当
         *      *Connection关闭时,是否删除队列全
         * 4.autoDelete:是否自动删除。当没有Consumer时,自动删除掉
         * 5.arguments:参数。
         */
        //如果没有一个交helloWorld的队列,那么会自动创建一个
        channel.queueDeclare("hello_World",true,false,false,null);

        /**
         * String queue, boolean autoAck, Consumer callback
         * queue:队列名称
         * autoAck:是否自动确认
         * callback:回调对象
         */
        Consumer consumer =new DefaultConsumer(channel){
            /*
            回调方法,收到消息后,自动执行
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("envelope:"+envelope.getExchange());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
            }
        };

        channel.basicConsume("hello_World",true,consumer);
        //消费者需要监听因此不需要关闭资源
    }
}

生产者与消费者都需要声明队列是为了避免队列不存在的情况

SpringAMQP的使用

AMQP是用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。而SpringAMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现

生产者实现

引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

在application.yml配置如下信息

spring:
  rabbitmq:
    host: 192.168.116.131
    port: 5672
    username: admin
    password: admin
    virtual-host: /

编写测试类

@SpringBootTest
@RunWith(SpringRunner.class)
public class test {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSend2SimpleQueue() throws Exception {
        String queueName ="hello";
        String message = "hello, spring amqp";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

运行测试观察rabbit控制台

消费者实现

引入依赖和配置相关信息与消费者相同,不同的是,编写一个监听器去监听队列

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "hello")
    public void listenSimpleQueueMessage(String msg){
        System.out.println("接收到消息:"+msg);
    }
}

启动引导类观察控制台

Work Queue工作队列

提高消息处理速度, 避免消息的堆积问题

案例实现:

生产者1秒内生产50条消息

    @Test
    public void testWorkQueueSendMessage() throws Exception {
        String queueName ="hello";
        String message = "hello, spring amqp__";
        for (int i = 0; i < 50; i++) {
            rabbitTemplate.convertAndSend(queueName,message+i);
            Thread.sleep(20);
        }
    }

而消费者代码如下

@Component
public class SpringRabbitListener {
    @RabbitListener(queues = "hello")
    public void listenWorkQueueMessage1(String msg) throws InterruptedException {
        System.out.println("消费者1接收到消息:"+msg);
        Thread.sleep(30);
    }

    @RabbitListener(queues = "hello")
    public void listenWorkQueueMessage2(String msg) throws InterruptedException {
        System.out.println("====消费者2接收到消息:"+msg);
        Thread.sleep(50);
    }
}

运行结果如下

可以看到每个消费者各处理25条,消费者1处理更快处理结束不会去处理更多的消息而是等待消费者2处理结束。

这种情况是因为Rabbit中存在消息预取的行为,当消息处理前会从Channel中提前拿去一部分消息(类似于轮询平均分配)后再去处理,当我们希望处理更快的设备能够读取更多的消息时,我们可以设置消息预取限制。在application.yml文件中添加如下配置

spring:
  rabbitmq:
    host: 192.168.116.131
    port: 5672
    username: admin
    password: admin
    virtual-host: /
    listener:
      simple:
        prefetch: 1 #每次最多获取一条消息,处理完成后才能获取下一条消息

修改完后再次执行观察控制台

可以看到消费能力更强的处理消息更多。

工作队列模式应用于任务过重或任务过多的场景(比如说发送短信)

发布订阅模式

前两种模式只是将消息发送给一个消费者,而发布订阅模式可以将消息发送给多个消费者。实现方式是加入了exchange(交换机)。exchange只负责路由,不负责存储。路由失败则消息丢失。

Fanout交换机(广播模式)

@Configuration
public class FanoutConfig {
    //声明交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanoutExchange");
    }
    //声明队列
    @Bean
    public Queue queue1(){
        return new Queue("fanoutQueue1");
    }
    @Bean
    public Queue queue2(){
        return new Queue("fanoutQueue2");
    }

    //声明绑定关系
    @Bean
    public Binding binding1(Queue queue1,FanoutExchange exchange){
        return BindingBuilder
                .bind(queue1)
                .to(exchange);
    }

    @Bean
    public Binding binding2(Queue queue2,FanoutExchange exchange){
        return BindingBuilder
                .bind(queue2)
                .to(exchange);
    }
}

重启消费者观察Rabbit控制台

编写监听器

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "fanoutQueue1")
    public void listenFanoutQueueMessage1(String msg){
        System.out.println("从队列queue1中获取到消息:"+msg);
    }

    @RabbitListener(queues = "fanoutQueue2")
    public void listenFanoutQueueMessage2(String msg){
        System.out.println("从队列queue2中获取到消息:"+msg);
    }
}

编写生产者测试类

    @Test
    public void testFanoutQueueSendMessage() throws Exception {
        String exchangeName = "fanoutExchange";
        String message = "hello, fanout";
        rabbitTemplate.convertAndSend(exchangeName,"",message);
    }

启动观察Rabbit控制台

Direct交换机(路由模式)

  1. 每一个Queue都与Exchange设置一个BindingKey
  2. 发布者发送消息时,指定消息的RoutingKey
  3. Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

案例实现

如果和Fanout模式一样去声明绑定关系的话,会比较麻烦,编写代码较多,我们可以采用注解的方式去声明绑定关系。

@Component
public class SpringRabbitListener {
    @RabbitListener(bindings = @QueueBinding(value = @Queue("directQueue1")
                    ,exchange = @Exchange(value = "directExchange"
                    ,type = ExchangeTypes.DIRECT),
                    key = {"red","blue"}))
    public void listenDirectQueueMessage1(String msg){
        System.out.println("从队列queue1中获取到消息:"+msg);
    }

    @RabbitListener(bindings = @QueueBinding(value = @Queue("directQueue2")
            ,exchange = @Exchange(value = "directExchange"
            ,type = ExchangeTypes.DIRECT),
            key = {"red","yellow"}))
    public void listenDirectQueueMessage2(String msg){
        System.out.println("从队列queue1中获取到消息:"+msg);
    }
}

运行消费者后观察Rabbit控制台

编写生产者测试类代码

    @Test
    public void testDirectQueueSendMessage() throws Exception {
        String exchangeName = "directExchange";
        String message = "hello, direct";
        rabbitTemplate.convertAndSend(exchangeName, "blue", message);
        rabbitTemplate.convertAndSend(exchangeName, "red", message + " red");
        rabbitTemplate.convertAndSend(exchangeName, "yellow", message + " yellow");
    }

运行观察控制台

TopicExchange(话题模式)

案例实现

@Component
public class SpringRabbitListener {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("topicQueue1"),
            exchange = @Exchange(value = "topicExchange",type = ExchangeTypes.TOPIC),
            key = "china.#"
    ))
    public void listenTopicQueueMessage1(String msg){
        System.out.println("从中国话题队列中获取到消息:"+msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("topicQueue2"),
            exchange = @Exchange(value = "topicExchange",type = ExchangeTypes.TOPIC),
            key = "#.news"
    ))
    public void listenTopicQueueMessage2(String msg){
        System.out.println("从新闻话题队列中获取到消息:"+msg);
    }
}

运行观察Rabbit控制台

编写生产者测试类代码

    @Test
    public void testTopicQueueSendMessage() throws Exception {
        String exchangeName = "topicExchange";
        String message = "hello, topic";
        rabbitTemplate.convertAndSend(exchangeName, "china.news", message+" 中国新闻");
        rabbitTemplate.convertAndSend(exchangeName, "china.#", message + "晴朗");
        rabbitTemplate.convertAndSend(exchangeName, "#.news", message + "战争");
    }

运行观察Rabbit控制台

发送三条消息但共有5条消息

消息转换器

在简单消息队列的实现中,我们发送消息发送的是字节数组。但是接收的消息反而是String类型的字符。那是因为。Spring中对消息的处理是由org.springframework.amqp.support.converter.MessageConverter处理默认使用SimpleMessageConverter来实现序列化(基于JDK的ObjectOutputStream实现)

进行一个测试,创建一个object.queue队列,发送一个Map类型的数据

    @Test
    public void testSendObject() throws Exception {
        Map<String, Object> map = new HashMap<>();
        map.put("name","zmbwcx");
        String queueName = "object.queue";
        rabbitTemplate.convertAndSend(queueName,map);
    }

观察Rabbit控制台

消息内容被JDK序列化为上图内容,这种序列化方式不安全且占用内存更大。增加了传输成本。

我们可以修改为JSON的序列化方式,具体操作如下

<dependency>
  <groupId>com.fasterxml.jackson.dataformat</groupId>
  <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
@Bean
public MessageConverter jsonMessageConverter(){
    return new Jackson2JsonMessageConverter();
}

重新发送一条消息,观察Rabbit控制台

生产者与消费者应该使用同一个消息转换器,因此,消费者也应进行相同操作

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

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

相关文章

私有云:【8】VCenter安装Connection服务

私有云&#xff1a;【8】VCenter安装Connection服务 1、安装Connection服务 服务器创建好后配置IP&#xff0c;加入域以及添加域管理员cloudadmin&#xff0c;可参考安装sqlserver部分 1、安装Connection服务 使用cloudadmin用户登录Connection服务器 将connection安装包复制到…

导入的xls文件,数字和日期都是文本格式,到df3都正常,但df4报错,什么原因?...

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 脱我战时袍&#xff0c;著我旧时裳。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python最强王者交流群【斌】问了一个Pandas数据处理的问题&…

云闪付app拉新更新政策啦

云闪付app拉新一手平台 “聚量推客” 目前平台有3个云闪付的版本 1.云闪付高价版 2.云闪付京东版 3.云闪付普通版 普通版和最老的版本是一样的&#xff0c;高价办和京东版都是依托京东进行完成 云闪付拉新是什么&#xff1f;在拉新市场受各个地推人员和网推人员的追捧&am…

3D RPG Course | Core 学习日记一:初识URP

前言 最近开始学习Unity中文课堂M_Studio&#xff08;麦大&#xff09;的3D RPG Course&#xff0c;学习一下3D RPG游戏核心功能的实现&#xff0c;第一课我们学习到的是地图场景的编辑&#xff0c;其中涉及到了URP渲染。 我们首先进入Unity资源商店把地图素材和人物素材导入好…

提高微星笔记本Linux下散热性能,MSI-EC 驱动新补丁发布

导读近日消息&#xff0c;今年早些时候&#xff0c;Linux 6.4 中添加了 MSI-EC 驱动程序&#xff0c;允许对 Linux 系统微星笔记本电脑进行更多控制。 MSI-EC 驱动程序近日迎来新补丁&#xff0c;为微星笔记本带来 Cooler Boost 功能。该功能允许提高笔记本电脑的风扇转速&…

MyBatis的增删改查

2023.10.29 本章学习MyBatis的基本crud操作。 insert java程序如下&#xff1a; ①使用map集合传参 Testpublic void testInsertCar(){SqlSession sqlSession SqlSessionUtil.openSession();//先将数据放到Map集合中&#xff0c;在sql语句中使用 #{map集合的key} 来完成传…

服务熔断保护实践--Hystrix

概述 微服务有很多互相调用的服务&#xff0c;构成一系列的调用链路&#xff0c;如果调用链路中某个服务失效或者网络堵塞等问题&#xff0c;而有较多请求都需要调用有问题的服务时&#xff0c;这是就会造成多个服务的大面积失效&#xff0c;造成服务“雪崩”效应。 服务“雪…

React Hooks 实战案例

文章目录 一、React Hooks 简介二、React Hooks 的基本用法1. 使用 useState 创建状态2. 使用 useEffect 添加副作用 三、React Hooks 的常见问题1. 循环引用问题2. 副作用问题 四、React Hooks 实战案例1. 使用 useReducer 和 Redux&#xff1a;2. 使用 useContext&#xff1a…

HashJoin 在 Apache Arrow 和PostgreSQL 中的实现

文章目录 背景PostgreSQL HashJoin实现PG 执行器架构HashJoin 基本流程HashJoin 实现细节Join 类型HashJoin 的划分阶段HashJoin 的分批处理阶段JOIN 类型的状态机转换HashJoin 的投影和过滤 Arrow Acero HashJoin实现Acero 基本框架HashJoin 基本流程 总结 背景 近两个月转到…

C++ 中的仿函数 functor

一 仿函数的概念 1. 定义 仿函数&#xff08;functor&#xff09;是一种使用上像函数的类&#xff0c;其本质是一个实现了 operato() 函数的类&#xff0c;这种类就有了类似于函数一样的使用行为&#xff0c;这就是仿函数的类。 仿函数在 C STL标准库中被大量使用。 2. 特…

图神经网络和分子表征:5. Completeness

大家都知道 “两点确定一线&#xff0c;三点确定一平面”&#xff0c;那么多少个变量可以确定一个分子呢&#xff1f;这是最近顶刊们热烈讨论的话题。 &#xff08;据笔者不完全统计&#xff09;最早在 SphereNet &#xff08;2022 ICLR&#xff09;论文里&#xff0c;摘要上就…

【多态-动态绑定-向上转型-抽象类】

文章目录 静态绑定动态绑定多态的具体实现向上转型多态的优缺点抽象类抽象类的作用 总结 静态绑定 重载就是典型例子 动态绑定 多态的具体实现 //多态 class Animal{public String name;public int age;//无参构造方法public Animal() {}//有参构造方法public Animal(Strin…

MySQL安装『适用于 CentOS 7』

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; MySQL 学习 &#x1f383;操作环境&#xff1a; CentOS 7.6 腾讯云远程服务器 &#x1f381;软件版本&#xff1a; MySQL 5.7.44 文章目录 1.MySQL 的清理与安装1.1查看是否存在 MySQL 服务1.2.卸载原有服务1.…

每日Python:十个实用代码技巧

1、Jpg转Png 示例代码&#xff1a; # 图片格式转换, Jpg转Png# 方法① from PIL import Imageimg Image.open(demo.jpg) img.save(demo_open.png)# 方法② from cv2 import imread, imwriteimage imread("demo.jpg", 1) imwrite("demo_imread.png", im…

近年来上海高考数学命题趋势和备考建议,附1990年以来真题和解析

这篇文章六分成长为您介绍上海高考数学科目的一些分析和如何备考2024年的上海高考数学&#xff0c;并且为您提供1990-2023年的34年的上海高考数学真题和答案解析&#xff0c;供您反复研究。 一、上海高考数学题近年来的命题特点和趋势 1. 注重基础知识和基本技能&#xff1a;…

一文带你在GPU环境下配置YOLO8目标跟踪运行环境

本文介绍GPU下YOLO8目标跟踪任务环境配置、也即GPU下YOLO8目标检测任务环境配置。 YOLO8不仅仅可以实现目标检测&#xff0c;其还内置有Byte-Tracker、Bot-Tracker多目标跟踪算法。可以实现行人追踪统计、车流量跟踪统计等功能。值得注意的是Byte-Tracker、Bot-Tracker多目标跟…

全面详细讲解OSEK直接网络管理,并对比Autosar网管。

搞了两年的Autosar&#xff0c;用到的网络管理都是Autosar网络管理&#xff0c;虽然偶尔有听到或看到Osek网络管理&#xff0c;但是一直没机会具体进行开发和测试。最近有机会具体接触和开发到&#xff0c;弄完之后感受就是&#xff1a;还是Autosar的网络管理好用&#xff0c;O…

10、SpringCloud -- 优化重复下单

目录 优化重复下单问题的产生:需求:思路:代码:测试:优化重复下单 之前超卖、重复下单的解决方式 问题的产生: 比如这个秒杀活动没人去玩,只有一个人去参与秒杀,然后秒杀成功了,因为有联合索引,所以这个人他没法重复下单,但是他依然去请求秒杀,在秒杀的10个商品没…

设计模式大赏(一):桥接模式,组合模式

设计模式大赏&#xff08;一&#xff09;&#xff1a;桥接模式&#xff0c;组合模式 导言 本篇文章是设计模式大赏中的第一篇文章&#xff0c;这个系列的文章中我们主要将介绍一些常见的设计模式&#xff0c;主要是我在看Android源码中发现用到的一些设计模式。本篇文章将主要…

Android 快速实现隐私协议跳转链接

首先在string.xml创建对应字串 <string name"link">我已仔细阅读并同意<annotation value"privacy_policy">《隐私政策》</annotation>和<annotation value"service_agreement">《服务协议》</annotation></st…