SpringCloud学习目录点击跳转对应的文章 | |
---|---|
Java之 Spring Cloud 微服务搭建(第一个阶段)【一】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务 Eureka (第一个阶段)【二】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建Ribbon(第一个阶段)【三】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建 Consul(第一个阶段)【四】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建 Feign组件(第二个阶段)【一】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建 Hystrix (第二个阶段)【二】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建Sentinel (第二个阶段)【三】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建网关 nginx,Zuul(第三个阶段)【一】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建网关SpringCloud Gateway微服务网关GateWay(第三个阶段)【二】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务的链路追踪 Sleuth 和 Zipkin(第三个阶段)【三】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务的 Spring Cloud Stream(第四个阶段)【一】【SpringBoot项目实现商品服务器端调用】 | |
Java之 Spring Cloud 微服务的 SpringCloud Config 配置中心(第四个阶段)【二】【SpringBoot项目实现商品服务器端调用】 | |
Java之 Spring Cloud 微服务的开源配置中心Apollo(第四个阶段)【三】【SpringBoot项目实现商品服务器端调用】 |
在Euraka的GitHub上,宣布Eureka 2.x闭源。近这意味着如果开发者继续使用作为 2.x 分支上现有工作repo 一部分发布的代码库和工件,则将自负风险。
Zookeeper
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
Consul
consul是近几年比较流行的服务发现工具,工作中用到,简单了解一下。consul的三个主要应用场景:服务发现、服务隔离、服务配置。
Nacos
Nacos 是阿里巴巴推出来的一个新开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。
与其它分布式服务注册与发现的方案,Consul 的方案更“一站式”,内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其它工具(比如 ZooKeeper 等)。
使用起来也较 为简单。Consul 使用 Go 语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与 Docker 等轻量级容器可无缝配合。
Consul 的优势:
特性:
Consul强一致性(CP)
服务注册相比Eureka会稍慢一些。因为Consul的raft协议要求必须过半数的节点都写入成功才认为注册成功Leader挂掉时,重新选举期间整个consul不可用。保证了强一致性但牺牲了可用性。Eureka保证高可用和最终一致性(AP)
服务注册相对要快,因为不需要等注册信息replicate到其他节点,也不保证注册信息是否
replicate成功当数据出现不一致时,虽然A, B上的注册信息不完全相同,但每个Eureka节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求A查不到,但请求B就能查到。如此保证了可用性但牺牲了一致性。
eureka就是个servlet程序,跑在servlet容器中Consul则是go编写而成,安装启动即可
Consul 不同于 Eureka 需要单独安装,访问Consul 官网下载 Consul 的最新版本,我这里是
consul1.5x。根据不同的系统类型选择不同的安装包,从下图也可以看出 Consul 支持所有主流系统。
在linux虚拟中下载consul服务
## 从官网下载的Consul服务
wget https://releases.hashicorp.com/consul/1.5.3/consul_1.5.3_linux_amd64.zip
##使用unzip命令解压
unzip consul_1.5.3_linux_amd64.zip
##测试一下
consul
这里使用的的是Centos7
开启端口号
firewall-cmd --zone=public --add-port=8500/tcp --permanent # 开放5672端口
firewall-cmd --reload # 配置立即生效
启动consul服务
##已开发者模式快速启动,-client指定客户端可以访问的ip地址
[root@node01 ~]# ./consul agent -dev -client=0.0.0.0
==> Starting Consul agent...
Version: 'v1.5.3'
Node ID: '49ed9aa0-380b-3772-a0b6-b0c6ad561dc5'
Node name: 'node01'
Datacenter: 'dc1' (Segment: '<all>')
Server: true (Bootstrap: false)
Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false,
Auto-Encrypt-TLS: false
启动成功之后访问: http://IP:8500
,可以看到 Consul 的管理界面
Consul 支持健康检查,并提供了 HTTP 和 DNS 调用的API接口完成服务注册,服务发现,以及K/V存储这些功能。
接下来通过发送HTTP请求的形式来了解一下Consul
通过Insomnia发送put请求到http://ip:8500/v1/catalog/register地址可以完成服务注册
{
"Datacenter": "dc1",
"Node": "node01",
"Address": "ip地址",
"Service": {
"ID":"mysql-01",
"Service": "mysql",
"tags": ["master","v1"],
"Address": "ip地址",
"Port": 3306
}
}
访问刚刚的管理界面 http://IP:8500
发送get请求到http://ip:8500/v1/catalog/services查看所有的服务列表
发送get请求到http://ip:8500/v1/catalog/service/服务名查看具体的服务详
将微服务注册到注册中心
服务的消费者从consul
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itbluebox</groupId>
<artifactId>spring_cloud_consul_demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring_cloud_consul_demo</artifactId>
<groupId>cn.itbluebox</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>product_service</artifactId>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--actuator的健康检查-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</project>
创建包结构
配置文件application.yml
server:
port: 9001 #端口
spring:
application:
name: service-product #服务名称
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud?useUnicode=true&characterEncoding=utf8
username: root
password: root
jpa:
database: MySQL
show-sql: true
open-in-view: true
详细代码省略
访问测试:
http://localhost:9002/order/buy/1
<!--springcloud 提供的对基于consul的服务发现-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--actuator的健康检查-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
server:
port: 9001 #端口
spring:
application:
name: service-product #服务名称
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud?useUnicode=true&characterEncoding=utf8
username: root
password: root
jpa:
database: MySQL
show-sql: true
open-in-view: true
###开始配置consul的服务注册
cloud:
consul:
host: ip #consul服务器的主机地址
port: 8500 #consul服务器的ip地址
discovery:
#是否需要注册
register: true
#注册的实例ID (唯一标志)
instance-id: ${spring.application.name}-1 #会获取到上面对应的配置信息
#服务的名称
service-name: ${spring.application.name} #会获取到上面对应的配置信息
#服务的请求端口
port: ${server.port} #会获取到上面对应的配置信息
#指定开启ip地址注册
prefer-ip-address: true #会获取到上面对应的配置信息
#当前服务的请求ip
ip-address: ${spring.cloud.client.ip-address} #会获取到上面对应的配置信息
访问http://ip:8500/ui/dc1/services
我们发现
Service Checks是叉
设置Consul的心跳默认关闭,在配置文件中开启即可
heartbeat:
enabled: true
重新运行测试
刷新页面
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring_cloud_consul_demo</artifactId>
<groupId>cn.itbluebox</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order_service</artifactId>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--springcloud 提供的对基于consul的服务发现-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--actuator的健康检查-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</project>
server:
port: 9002 #端口
spring:
application:
name: service-order #服务名称
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud?useUnicode=true&characterEncoding=utf8
username: root
password: root
jpa:
database: MySQL
show-sql: true
open-in-view: true
###开始配置consul的服务注册
cloud:
consul:
host: ip #consul服务器的主机地址
port: 8500 #consul服务器的ip地址
discovery:
#是否需要注册
register: true
#注册的实例ID (唯一标志)
instance-id: ${spring.application.name}-1
#服务的名称
service-name: ${spring.application.name}
#服务的请求端口
port: ${server.port}
#指定开启ip地址注册
prefer-ip-address: true
#当前服务的请求ip
ip-address: ${spring.cloud.client.ip-address}
heartbeat:
enabled: true
访问:http://ip:8500/ui/dc1/services
package cn.itbluebox.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EntityScan("cn.itbluebox.order.entity")
public class OrderApplication {
/* SpringCloud对Consul进行了进一步从处理 向其中继承了ribbon的支持 */
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
package cn.itbluebox.order.controller;
import cn.itbluebox.order.entity.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping(value = "/buy/{id}",method = RequestMethod.GET)
public Product findById(@PathVariable Long id) {
Product product = restTemplate.getForObject("http://service-product/product/1",Product.class);;
return product;
}
}
访问路径:http://localhost:9002/order/buy/1
此图是官网提供的一个事例系统图,图中的Server是consul服务端高可用集群,Client是consul客户端。
consul客户端不保存数据,客户端将接收到的请求转发给响应的Server端。
Server之间通过局域网或广域网通信实现数据一致性。
每个Server或Client都是一个consul agent。Consul集群间使用了GOSSIP协议通信和raft一致性算法。
agent:启动一个consul的守护进程
dev:开发者模式
client :是consul代理,和consul server交互
一个微服务对应一个client
微服务和client部署到一台机器上
server:真正干活的consul服务3-5个
上面这张图涉及到了很多术语:
在每个数据中心,client和server是混合的。一般建议有3-5台server。
这是基于有故障情况下的可用性和性能之间的权衡结果,因为越多的机器加入达成共识越慢。
然而,并不限制client的数量,它们可以很容易的扩展到数千或者数万台。
同一个数据中心的所有节点都必须加入gossip协议。这意味着gossip协议包含一个给定数据中心的所有节点。
这服务于几个目的:第一,不需要在client上配置server地址。发现都是自动完成的。
第二,检测节点故障的工作不是放在server上,而是分布式的。这是的故障检测相比心跳机制有更高的可扩展性。
第三:它用来作为一个消息层来通知事件,比如leader选举发生时。
每个数据中心的server都是Raft节点集合的一部分。
这意味着它们一起工作并选出一个leader,一个有额外工作的server。
leader负责处理所有的查询和事务。作为一致性协议的一部分,事务也必须被复制到所有其他的节点。
因为这一要求,当一个非leader得server收到一个RPC请求时,它将请求转发给集群leader。
server节点也作为WAN gossip Pool的一部分。
这个Pool不同于LAN Pool,因为它是为了优化互联网更高的延迟,并且它只包含其他Consul server节点。
这个Pool的目的是为了允许数据中心能够以low-touch的方式发现彼此。这使得一个新的数据中心可以很容易的加入现存的WAN gossip。
因为server都运行在这个pool中,它也支持跨数据中心请求。
当一个server收到来自另一个数据中心的请求时,它随即转发给正确数据中想一个server。
该server再转发给本地leader。
这使得数据中心之间只有一个很低的耦合,但是由于故障检测,连接缓存和复用,跨数据中心的请求都是相对快速和可靠的。
传统的监控,如ceilometer,由于每个节点都会向server报告状态,随着节点数量的增加server的压力随之增大。在所有的Agent之间(包括服务器模式和普通模式)运行着Gossip协议。
服务器节点和普通Agent都会加入这个Gossip集群,收发Gossip消息。
每隔一段时间,每个节点都会随机选择几个节点发送Gossip消息,其他节点会再次随机选择其他几个节点接力发送消息。这样一段时间过后,整个集群都能收到这条消息。示意图如下。
为了实现集群中多个ConsulServer中的数据保持一致性,consul使用了基于强一致性的RAFT算法。
在Raft中,任何时候一个服务器可以扮演下面角色之一:
Leader全权负责所有客户端的请求,以及将数据同步到Follower中(同一时刻系统中只存在一个Leader)。
Follower被动响应请求RPC,从不主动发起请求RPC。
Candidate由Follower向Leader转换的中间状态关于RAFT一致性算法有一个经典的动画http://thesecretlivesofdata.com/raft/,
其中详细介绍了选举,数据同步的步骤。
首先需要有一个正常的Consul集群,有Server,有Leader。
这里在服务器Server1、Server2、Server3上分别部署了Consul Server。
(这些服务器上最好只部署Consul程序,以尽量维护Consul Server的稳定)
服务器Server4和Server5上通过Consul Client分别注册Service A、B、C,这里每个Service分别部署在了两个服务器上,这样可以避免Service的单点问题。
(一般微服务和Client绑定)在服务器Server6中Program D需要访问Service B,这时候Program D首先访问本机Consul Client提供的HTTP API,
本机Client会将请求转发到Consul Server,Consul Server查询到Service B当前的信息返回。
##从官网下载最新版本的Consul服务
wget https://releases.hashicorp.com/consul/1.5.3/consul_1.5.3_linux_amd64.zip
##使用unzip命令解压
unzip consul_1.5.3_linux_amd64.zip
##将解压好的consul可执行命令拷贝到/usr/local/bin目录下
cp consul /usr/local/bin
##测试一下
consul
##登录s1虚拟机,以server形式运行
consul agent -server -bootstrap-expect 3 -data-dir /etc/consul.d -node=server-1
-bind=192.168.74.101 -ui -client 0.0.0.0 &
##登录s2 虚拟机,以server形式运行
consul agent -server -bootstrap-expect 2 -data-dir /etc/consul.d -node=server-2
-bind=192.168.74.102 -ui -client 0.0.0.0 &
##登录s3 虚拟机,以server形式运行
consul agent -server -bootstrap-expect 2 -data-dir /etc/consul.d -node=server-3
-bind=192.168.74.103 -ui -client 0.0.0.0 &
-server: 以server身份启动。
-bootstrap-expect:集群要求的最少server数量,当低于这个数量,集群即失效。
-data-dir:data存放的目录,更多信息请参阅consul数据同步机制
-node:节点id,在同一集群不能重复。
-bind:监听的ip地址。
-client:客户端的ip地址(0.0.0.0表示不限制)
& :在后台运行,此为linux脚本语法
至此三个Consul Server模式服务全部启动成功
##在本地电脑中使用client形式启动consul
consul agent -client=0.0.0.0 -data-dir /etc/consul.d -node=client-1
在s2,s3,s4 服务其上通过consul join 命令加入 s1中的consul集群中
##加入consul集群
consul join 192.168.74.101
##查看consul集群节点信息
consul members
当服务或者节点失效,Consul不会对注册的信息进行剔除处理,仅仅标记已状态进行标记(并且不可使
用)。如果担心失效节点和失效服务过多影响监控。可以通过调用HTTP API的形式进行处理
节点和服务的注销可以使用HTTP API:
注销任意节点和服务:/catalog/deregister
注销当前节点的服务:/agent/service/deregister/:service_id
如果某个节点不继续使用了,也可以在本机使用consul leave命令,或者在其它节点使用consul forceleave 节点Id。
在集群环境下,健康检查是由服务注册到的Agent来处理的,那么如果这个Agent挂掉了,那么此节点
的健康检查就处于无人管理的状态。
##在本地电脑中使用client形式启动consul
consul agent -client=0.0.0.0 -data-dir /etc/consul.d -node=client-1
##加入consul集群
consul join 192.168.74.101
##查看consul集群节点
信息
consul members
从实际应用看,节点上的服务可能既要被发现,又要发现别的服务,如果节点挂掉了,仅提供被发现的功能实际上服务还是不可用的。当然发现别的服务也可以不使用本机节点,可以通过访问一个Nginx实现的若干Consul节点的负载均衡来实现。
SpringCloud学习目录点击跳转对应的文章 | |
---|---|
Java之 Spring Cloud 微服务搭建(第一个阶段)【一】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务 Eureka (第一个阶段)【二】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建Ribbon(第一个阶段)【三】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建 Consul(第一个阶段)【四】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建 Feign组件(第二个阶段)【一】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建 Hystrix (第二个阶段)【二】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建Sentinel (第二个阶段)【三】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建网关 nginx,Zuul(第三个阶段)【一】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务搭建网关SpringCloud Gateway微服务网关GateWay(第三个阶段)【二】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务的链路追踪 Sleuth 和 Zipkin(第三个阶段)【三】【SpringBoot项目实现商品服务器端是调用】 | |
Java之 Spring Cloud 微服务的 Spring Cloud Stream(第四个阶段)【一】【SpringBoot项目实现商品服务器端调用】 | |
Java之 Spring Cloud 微服务的 SpringCloud Config 配置中心(第四个阶段)【二】【SpringBoot项目实现商品服务器端调用】 | |
Java之 Spring Cloud 微服务的开源配置中心Apollo(第四个阶段)【三】【SpringBoot项目实现商品服务器端调用】 |
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://code100.blog.csdn.net/article/details/121404763
内容来源于网络,如有侵权,请联系作者删除!