使用activeMQ的准备工作
1、安装VMware(虚拟机)
安装地址:http://www.mamicode.com/info-detail-3009804.html
2、使用VMware安装Ubuntu和CentOS
安装地址Ubuntu:http://mirrors.163.com/
安装地址CentOS:http://mirrors.163.com/centos/8/isos/x86_64/
3、在VMware上安装Ubuntu
安装步骤参考地址:https://blog.csdn.net/stpeace/article/details/78598333
4、安装ActiveMQ
官网下载地址:http://activemq.apache.org/components/classic/download/
activeMQ
1、特点
- 高可用
- 集群和容错配置
- 持久化
- 延时发送/定时投递
- 签收机制
- spring整合
- Java语言
2、解决的问题
- 解耦
- 异步
- 削峰
4、方式
- 队列:点对点模式
- 主题:发布订阅模式
6、JMS创建编码(queue队列)
(1)pom.xml文件
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
(2)消息生产者编码
public class JmsProduce {
public static final String ACTIVEMQ_URL="tcp://localhost:61616";
public static final String QUEUE_NMAE="queue";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory=new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过创建工厂,获得连接connection并启动访问
Connection connection=activeMQConnectionFactory.createConnection();
connection.start();
//3、创建会话session,里面有两个参数:事务和签收
Session session=connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(具体时间队列还是主题)
Queue queue=session.createQueue(QUEUE_NMAE);
//5、创建消息的生产者
MessageProducer messageProducer=session.createProducer(queue);
//持久化数据
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
//6、通过使用messageProducer产生三条消息发送到mq的队列中
for(int i=0;i<=3;i++){
//7、创建消息(好比一个字符串2)
TextMessage textMessage=session.createTextMessage("msg:"+i);
//8、通过messageProducer发送给mq
messageProducer.send(textMessage);
}
//9、关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("发送消息结束");
}
(3)消息消费者(监听、堵塞和限制时间)编码
public class JmsComsumer {
public static final String ACTIVEMQ_URL="tcp://localhost:61616";
public static final String QUEUE_NMAE="queue";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过创建工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3、创建会话session,里面有两个参数:事务和签收
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
//4、创建目的地(具体时间队列还是主题)
Queue queue = session.createQueue(QUEUE_NMAE);
//5、创建消费者
MessageConsumer messageConsumer=session.createConsumer(queue);
// while (true){
// //与生产者的消息类型一样,接收消息receive(),限制时间接收消息
// TextMessage textMessage=(TextMessage) messageConsumer.receive(4000L);
// if(null!=textMessage){
// System.out.println("消费者接受到的消息:"+textMessage.getText());
// }else {
// break;
// }
// }
//5、通过监听器的方式来接收消息
messageConsumer.setMessageListener((message)->{
if(null!=message && message instanceof TextMessage){
TextMessage textMessage=(TextMessage) message;
try {
System.out.println("消费者接受到的消息:"+textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
try {
textMessage.acknowledge();
} catch (JMSException e) {
e.printStackTrace();
}
}
});
//保证控制台不灭
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
一个生产者多个消费者,采用轮询机制。
6、总结(JMS开发的基本步骤)
1)创建一个connection factory
2)通过connection factory来创建JMS connection
3)启动JMS connection
4)通过connection创建JMS session
5)创建JMS destaination(队列或主题)
6)创建JMS producer或JMS message并设置destaination(队列或主题)
7)创建JMS consumer或注册一个JMS message listener
8)发送或接收JMS message
9)关闭所有的资源
7、创建编码(topic)
有时间限制,1:N关系,先起消费者再启动生产者
(1)发布主题生产者
public class JmsProducer {
public static final String ACTIVEMQ_URL="tcp://localhost:61616";
public static final String TOPIC_NMAE="topic";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory=new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过创建工厂,获得连接connection并启动访问
Connection connection=activeMQConnectionFactory.createConnection();
connection.start();
//3、创建会话session,里面有两个参数:事务和签收
Session session=connection.createSession(true,Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(具体时间队列还是主题)
Topic topic=session.createTopic(TOPIC_NMAE);
//5、创建消息的生产者
MessageProducer messageProducer=session.createProducer(topic);
//6、通过使用messageProducer产生三条消息发送到mq的队列中
for(int i=0;i<=3;i++){
//7、创建消息(好比一个字符串2)
TextMessage textMessage=session.createTextMessage("topic msg:"+i);
//8、通过messageProducer发送给mq
messageProducer.send(textMessage);
}
//9、关闭资源
messageProducer.close();
session.commit();
session.close();
connection.close();
System.out.println("topic发送消息结束");
}
}
(2)订阅消息的消费者
public class JmsComsumer {
public static final String ACTIVEMQ_URL="tcp://localhost:61616";
public static final String TOPIC_NMAE="topic";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过创建工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3、创建会话session,里面有两个参数:事务和签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(具体时间队列还是主题)
Topic topic = session.createTopic(TOPIC_NMAE);
//5、创建消费者
MessageConsumer messageConsumer=session.createConsumer(topic);
//通过监听器的方式来接收消息(拉姆达表达式)
messageConsumer.setMessageListener((message)->{
if(null!=message && message instanceof TextMessage){
TextMessage textMessage=(TextMessage) message;
try {
System.out.println("消费者接受到的topic消息:"+textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
//保证控制台不灭
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
生产者发布的所有消息,每个订阅者各自都拥有所有的消息。发送和接收的消息类型必须一致。
8、队列(queue)和主题(Topic)的区别
比较项目 | Topic | queue |
---|---|---|
工作模式 | 订阅-发布模式 | 负载均衡模式(点对点模式) |
状态 | 无状态 | 队列数据默认会在mq服务器上以文件形式保存 |
传递完整性 | 若无订阅者,消息会被丢弃 | 消息不会丢弃 |
处理效率 | 会随订阅者增加而明显降低 | 一条消息对应一个消费者 |
数量 | 一对多 | 一对一 |
先启动消费者在启动生产者 | 先启动生产者在启动消费者 |
9、消息的可靠性
-
持久化(PERSISTENT):服务器宕机,消息依然存在
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT)
-
事务:将创建会话session的第一个参数为true,则在关闭session之前必须session.commit();为false是自动提交。使用事务时,加强容错,commit避免消息的重复消费。
Session session=connection.createSession(true,Session.AUTO_ACKNOWLEDGE); //session.commit(); //session.close(); try{//没问题直接提交 session.commit(); }catch (Exception e){//报异常,回滚 session.rollback(); }finally { if(null!=session){//关闭session session.close(); } }
-
签收(acknowledge):消费者端将创建会话session的第二个参数就是签收,分为:自动(Session.AUTO_ACKNOWLEDGE)、手动(Session.CLIENT_ACKNOWLEDGE)和可允许重复签收(Session.DUPS_OK_ACKNOWLEDGE)。经常使用自动和手动,避免在消费者消息的重复消费。若开事务之后,就不可以需要手动反馈消息。
使用手动签收时,需要对消息作出反馈:
//设置手动签收模式 Session session=connection.createSession(true,Session.CLIENT_ACKNOWLEDGE); TextMessage textMessage=(TextMessage) message; System.out.println("消费者接受到的消息:"+textMessage.getText()); //手动反馈消息 textMessage.acknowledge();
事务和签收的关系:
(1)在事务性会话中,当一个事物被成功提交则消息被自动签收,如果事务回滚,则消息会被再次传送。
(2)非事务性会话中,消息合适被确认取决于创建绘画室的应答模式(acknowledgement mode)。
10、创建持久化(Topic)编码
(1)发布者(publisher)
public class JmsProducer {
public static final String ACTIVEMQ_URL="tcp://localhost:61616";
public static final String TOPIC_NMAE="topic";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory=new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过创建工厂,获得连接connection并启动访问
Connection connection=activeMQConnectionFactory.createConnection();
// connection.start();
//3、创建会话session,里面有两个参数:事务和签收
Session session=connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(具体时间队列还是主题)
Topic topic=session.createTopic(TOPIC_NMAE);
//5、创建消息的生产者
MessageProducer messageProducer=session.createProducer(topic);
//消息持久化
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
//启动
connection.start();
//6、通过使用messageProducer产生三条消息发送到mq的队列中
for(int i=0;i<=3;i++){
//7、创建消息(好比一个字符串2)
TextMessage textMessage=session.createTextMessage("topic msg:"+i);
//8、通过messageProducer发送给mq
messageProducer.send(textMessage);
}
//9、关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("topic发送消息结束");
}
}
(2)订阅者(subscriber)
public class JmsComsumer {
public static final String ACTIVEMQ_URL="tcp://localhost:61616";
public static final String TOPIC_NMAE="queue";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过创建工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
//connection.start();
connection.setClientID("张三");
//3、创建会话session,里面有两个参数:事务和签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(具体时间队列还是主题)
Topic topic = session.createTopic(TOPIC_NMAE);
//发布订阅模式
TopicSubscriber topicSubscriber=session.createDurableSubscriber(topic,"remark");
//启动
connection.start();
//5、创建消息的接收
Message message=topicSubscriber.receive();
while (null!=message){
TextMessage textMessage=(TextMessage) message;
System.out.println("消费者接受到的topic消息:"+textMessage.getText());
message=topicSubscriber.receive(5000L);
}
session.close();
connection.close();
}
}
持久化Topic总结:一定要先运行消费者,类似于向MQ注册,再运行生产者去发送消息,这样在消费者都会接收到生产者发送的消息,若消费者离线,重启之后也会接收到生产者发送的消息。
11、ActiveMQ的broker
相当于一个ActiveMQ的实例。简单来说就是实现了用代码的形式启动ActiveMQ将MQ嵌入到java代码中,以便随时能够启动。
实现:
1、配置activeMQ.xml
命令:./activemq start xbean:file:/myactiveMQ/apache-activemq-5.15.9/config/activemq02.xml
2、导入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
3、代码实现
public class EmbeBroker {
public static void main(String[] agrs) throws Exception {
BrokerService brokerService=new BrokerService();
brokerService.setUseJmx(true);
brokerService.addConnector("tcp://localhost:616161");
brokerService.start();
}
}
12、Spring整合ActiveMQ
-
pom.xml的maven依赖(最重要的两个包)
<!--spring整合activemq--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jms</artifactId> <version>4.3.23.RELEASE</version> </dependency> <!--activemq所需的pool--> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-pool</artifactId> <version>5.15.9</version> </dependency>
-
编写Spring配置文件applicationContext.xml
<!--开启自动扫描的包--> <context:component-scan base-package="com.springboot.springbootactivemq.spring"></context:component-scan> <!--配置生产者--> <bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop"> <property name="connectionFactory"> <bean class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://localhost:61616"></property> </bean> </property> <property name="maxConnections" value="100"></property> </bean> <!--目的地(queue还是topic)--> <!--queue--> <bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg index="0" value="spring-activemq-queue"></constructor-arg> </bean> <!--topic--> <bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic"> <constructor-arg index="0" value="spring-activemq-queue"></constructor-arg> </bean> <!--jms工具类--> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory" ref="jmsFactory"></property> <!--监听队列--> <!--<property name="defaultDestination" ref="destinationQueue"/>--> <!--监听主题--> <property name="defaultDestination" ref="destinationTopic"/> <property name="messageConverter"> <bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/> </property> </bean> <!--監聽器--> <bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="connectionFactory" ref="jmsFactory"></property> <!--监听队列--> <!--<property name="destination" ref="destinationQueue"/>--> <!--监听主题--> <property name="destination" ref="destinationTopic"/> <property name="messageListener" ref="myMessageListener"></property> </bean>
-
生产者
/** * 监听器:不啟動消費者,通過監聽的方式 */ @Component public class MyMessageListener implements MessageListener { @Override public void onMessage(Message message) { if(null!=message && message instanceof TextMessage){ TextMessage textMessage=(TextMessage) message; try { System.out.println("消费者接受到的消息:"+textMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } } }
/** * 生产者 */ @Service public class SpringMQ_Produce { @Autowired private JmsTemplate jmsTemplate; public static void main(String [] agrs){ ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml"); SpringMQ_Produce produce= (SpringMQ_Produce) applicationContext.getBean("springMQ_Produce"); produce.jmsTemplate.send((session -> { TextMessage textMessage=session.createTextMessage("spring and activemq:"); return textMessage; })); System.out.println("****send over*****"); } }
-
消费者
/** * 消费者 */ @Service public class SpringMQ_consumer { @Autowired private JmsTemplate jmsTemplate; public static void main(String [] agrs) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); SpringMQ_consumer consumer = (SpringMQ_consumer) applicationContext.getBean("springMQ_consumer"); String rec=(String) consumer.jmsTemplate.receiveAndConvert(); System.out.println("****消費者接受的消息:"+rec); } }
13、SpringBoot整合ActiveMQ
-
pom.xml的maven依赖
<!--springboot——> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> </parent> <!--springboot整合activemq--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> <version>2.1.5.RELEASE</version> </dependency>
-
application.yml文件
server: port: 99999 #端口号 spring: application: name: springbootMQ-Queue #项目名 activemq: broker-url: tcp://localhost:61616 #ip user: admin #用户名 password: admin #密码 jms: pub--sub-domain: false #false表示队列(queue)true表示主题(topic)
-
生产者
//配置类 @Component @EnableJms public class ConfigBean { @Value("${spring.application.name}") //application.yml里面的配置项目名 private String myqueue; //队列 @Bean public Queue queue(){ return new ActiveMQQueue(myqueue); } //topic主题 @Bean public Topic topic(){ return new ActiveMQTopic(myqueue); } }
//生产者类 @Component public class SpringBootMQ_Produce { @Autowired private JmsMessagingTemplate jmsMessagingTemplate; @Autowired private Queue queue; //触发投送消息(test测试) public void produce(){ jmsMessagingTemplate.convertAndSend(queue,"***"+ UUID.randomUUID().toString().substring( 0,6)); } //每個3秒自动發送一個消息 @Scheduled(fixedDelay = 3000) public void produceMsgScheduled(){ jmsMessagingTemplate.convertAndSend(queue,"***"+ UUID.randomUUID().toString().substring( 0,6)); System.out.println("****每個3秒發送一個消息****"); }
//程序入口 @SpringBootApplication @EnableScheduling public class ActiveMQProduceApplication { public static void main(String[] args) { SpringApplication.run(ActiveMQProduceApplication.class, args); } }
-
消费者
@Component public class SpringBootMQ_Consumer { @JmsListener(destination = "${spring.application.name}") //监听注解 public void recevie(TextMessage textMessage){ try { System.out.println("消费者接收到的消息:"+textMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } }
14、ActiveMQ的协议(NIO或auto+nio)
默认使用TCP协议,具有性能好、稳定性强;使用字节流方式传递,效率高;跨平台。NIO提高的性能比TCP更好。
NIO和NIO+auto协议的配置分别在安装目录下activemq.xml中添加:<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true"/>
和<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61618maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
15、ActiveMQ的持久化机制
(1)AMQ
基于文件的存储方式,文件默认大小为32M,适用于activeMQ 5.3之前的版本,已过时。
(2)KahaDB(activeMQ5.4之后默认使用)
是目前默认的存储方式,可适用于任何场景,提高性能和恢复力,消息存储使用一个事务日志(db-number.log)和一个索引文件(db.data)来存储它所有的地址,是一个专门针对消息持久化的解决方案,对典型的消息使用模式进行了优化。数据被加载到data log中,当不再需要log文件是,log文件会被丢弃(删除或归档)。http://activemq.apache.org/persistence.html
(3)JDBC 消息存储
对于长时间的存储,推荐使用JDBC,使用持久化messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);步骤如下:
-
下载mysql驱动包,放到activemq安装目录的lib文件夹下;
-
修改activemq下config的active.xml文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFrK0U0C-1600420512324)(C:\Users\jha04\AppData\Roaming\Typora\typora-user-images\1599199349178.png)]
-
重新启动active。成功之后,数据库自动创建三张表。
ACTIVEMQ_MSGS:消息表,queue和topic都存储在里面,消费之后,消息会被删除,表中各字段的意思如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZqVOxRFc-1600420512328)(C:\Users\jha04\AppData\Roaming\Typora\typora-user-images\1599200035453.png)]
ACTIVEMQ_ACKS:存储持久订阅的信息和最后一个持久订阅接收的信息ID ,表中各字段的意思如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9z5X3UOW-1600420512329)(C:\Users\jha04\AppData\Roaming\Typora\typora-user-images\1599199957299.png)]
ACTIVEMQ _LOCK:在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,其他的只能作为备份等待MAster Broker怒可用,才可能成为下一个Master Broker。用于记录·哪个Broker是当前的Master Broker。
(4) 带高速缓存的JDBC Journal
带高速缓存的日志消息存储,提高了性能,当消费者的消息速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。修改如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a0tXtFlx-1600420512331)(C:\Users\jha04\AppData\Roaming\Typora\typora-user-images\1599202422028.png)]
(5)LevelDB
基于文件的本地数据库存储
16、ActiveMQ的高级特性
(1)异步投递
默认消息是使用异步发送方式,除非明确指定使用同步发送的方式或者在未使用事务的前提下发送持久化的消息,这两种情况都是同步发送。开启异步投递是,需要设置回调函数
//开启异步投递,在允许失败的情况下有少量的数据丢失,
(ActiveMQConnectionFactory )activeMQConnectionFactory.setAlwaysSyncSend(true);
String msgID = message.getJMSMessageID();
//异步投递的回调方法
messageProducer.send(message, new AsyncCallback() {
@Override
public void onException(JMSException e) {
System.out.println(msgID+"false");
}
@Override
public void onSuccess() {
System.out.println(msgID+"ok");
}
});
(2)延时投递和定时投递
修改active.xml文件在broker中加上schedulerSupport=“true”;
java代码的修改
//延时3s
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,delay);
//间隔4s
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD,period);
//重复5次
message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT,repeat);
(3)消息重试机制
什么情况会发生消息重试机制:
- 客户端使用了事务切在session中调用了rollback();
- 客户端使用了事务切在调用commit()之前关闭了session()或没有commit();
- 客户端在lCLIENT_ACKNOWLEDGE的签收模式下,session()调用了recover()。
以上情况会导致消息重试机制,默认每1秒钟重发6次。
修改系统默认的消息重发机制如下:
//自定义重发机制
RedeliveryPolicy redeliveryPolicy=new RedeliveryPolicy() redeliveryPolicy.setMaximumRedeliveries(3);
activeMQConnectionFactory.setRedeliveryPolicy(redeliveryPolicy);
重发机制的属性说明:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H3M8Lodz-1600420512333)(C:\Users\jha04\AppData\Roaming\Typora\typora-user-images\1599468196965.png)]