zoukankan      html  css  js  c++  java
  • SpringCloud简介

     Eureka架构图

    对于微服务架构、开发的建议:

      1. 应用程序的核心是业务逻辑、按照业务或客户需求组织资源(重、难)

      2. 做有生命的产品,而不是项目

      3. 全栈化

      4. 后台服务贯彻Single Responsibility Principle(单一职责原则)

      5. VM -> Dockers

      6. DevOps

    SpringCloud 简介

      Spring Cloud 提供了分布式系统(非模块化开发)(配置管理、服务发现、熔断、路由、微代理、控制总线、一次性Token、全局锁、Leader选举、分布式Session、集群状态)

    -------------------------------分布式开发,一个服务一个项目一个目录,每一个目录都是一个工程---------------------------------------

    Spring Cloud 创建统一的依赖管理

       Spring Cloud 项目都是基于Spring Boot进行开发,并且都是使用Maven做项目管理工具,所以创建一个依赖管理项目作为Maven的Parent项目使用,对jar包版本的统一管理

    工程目录:vacc-spring-cloud-dependencies

    Spring Cloud 服务注册与发现

       Spring Cloud提供的动态的获取服务ip;组件,Spring Cloud Netflix 的 Eureka ,Eureka 是一个服务注册和发现模块

    工程目录:vacc-spring-cloud-eureka

    // 入口类,springboot应用程序
    package com.22bat.vacc.spring.cloud.eureka ; @SpringBootApplication @EnableEurekaServer
    // 开启Eureka Server public class EurekaApplication { public static void main (String[] args) { SpringApplication.run(EurekaApplication.class, args); } }

    resources--->>>application.yml

    Spring:
      application:
        name: vacc-spring-cloud-eureka
    
    server:
      port:8761
    
    eureka:
      instance:
        hostname:localhost
      client:
        registerWithEureka: false
        fetchRegistry: false
        serviceUrl:
          defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

    运行后访问:localhost:8761

    Spring Cloud 创建服务提供者

    工程目录:vacc-spring-cloud-service-admin  

    package  com.22bat.vacc.spring.cloud.service.admin;
    @SpringBootApplication
    @EnableEurekaClient // 开启Eureka Client
    public class ServiceAdiminApplication {
        public static void main (String[] args) {
            SpringApplication.run(ServiceAdiminApplication.class, args); 
      }
    }

    resources--->>>application.yml

    Spring:
      application:
        name: vacc-spring-cloud-service-admin 
    
    server:
      port:8762
    
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/

    启动服务器 Eureka -->后启动客户端访问:localhost:8762  因该服务未提供任何服务所以没东西 ;localhost:8761 可以看到启动的服务->管理// 提供Rest服务

    // 创建服务

    package
    com.22bat.vacc.spring.cloud.service.admin.controller;
    @RestController
    public class AdminController {   
      @Value("$(server.port)")
      private String port;

      @RequestMapping(value = "hi", method = RequestMethod.GET)
      public String sayHi(String message){
        return String.format("Hi your message is : %s prot : %s", message, port);
      }
    }

    重启 ServiceAdminApplication -> 访问 localhost:8762/hi?message=HelloSpringCloud

    Spring Cloud 创建服务消费者(Ribbon)

       在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯时基于http restful的。Spring Cloud有两种调用方式,一种是ribbon + restTemplate,另一种是feign.

      ribbon+rest:Ribbon 是一个负载均衡客户端,很好的控制 http 和 tcp 的一些行为。

    工程目录:vacc-spring-cloud-web-admin-ribbon // 客户端的admin,ribbon方式

    package  com.22bat.vacc.spring.cloud.web.admin.ribbon;
    @SpringBootApplication
    @EnableDiscoveryClient 
    public class WebAdiminRibbonApplication {
        public static void main (String[] args) {
            SpringApplication.run(WebAdiminRibbonApplication.class, args); 
      }
    }

    resources--->>>application.yml

    Spring:
      application:
        name: vacc-spring-cloud-web-admin-ribbon 
    thymeleaf:
    cache: false
    mode: LEGACYHTML5
    encoding: UTF-8
    servlet:
    content-type: text/html
    server: port:8764 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/

    启动服务器 Eureka -->后启动客户端访问:localhost:8762  因该服务未提供任何服务所以没东西 ;localhost:8761 可以看到启动的服务->管理// 提供Rest服务

    // 创建服务

    package com.22bat.vacc.spring.cloud.service.admin.controller;
    @RestController public class AdminController {   
      @Value("$(server.port)")
      private String port;

      @RequestMapping(value = "hi", method = RequestMethod.GET)
      public String sayHi(String message){
        return String.format("Hi your message is : %s prot : %s", message, port);
      }
    }

     配置注入 RestTemplate 的 Bean, 并通过@LoadBalanced 注解表明开启负载均衡功能

    package com.22bat.vacc.spring.cloud.web.admin.ribbon.config;
    
    @Configuration
    public class RestTemplateConfiguration{
    
        @Bean
       @LoadBalanced
    public RestTemplate restTemplate() { return new RestTemplate(); } }

    把serviceAdmin端口号改成8763,启动,开启两台服务器,负载均衡 ->访问localhost:8763

    package com.22bat.vacc.spring.cloud.web.admin.ribbon.service;
    
    @Service
    public class AdminService{
    
        @Autowired
        private RestTemplate restTemplate;
        public String sayHi(String message){
          return restTemplate.getForObject("http://vacc-spring-cloud-service-adimin/hi?message="+message,port);  
        }        
    }
    package com.22bat.vacc.spring.cloud.web.admin.ribbon.controller;
    
    @RestController
    public class AdminController{
    
        @Autowired
        private AdminService adminService;
        
        @RequestMapping(value="hi", method = RequestMethod.GET)  
        public String sayHi(@RequestParam String message){
          return adminService.sayHi(message);  
        }        
    }

    启动WebAdminRibbonApplication,8764 -> localhost:8764/hi?message=HelloRibbon  刷新localhost:8761,会显示全部注册上边的服务

    Spring Cloud 创建服务消费者 (Feign)

       Feign 是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用Feign注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,并和Eureka结合,默认实现了负载均衡效果

      Feign 采用的是基于接口的注解

      Feign 整合了Ribbon

    创建一个工程:vacc-spring-cloud-web-admin-feign 服务消费者项目

    package com.22bat.vacc.spring.cloud.web.admin.feign;
    @SpringBootApplication
    @EnableFeignClients
    @EnableDiscoveryClient // 注册到Eureka public class WebAdiminFeignApplication { public static void main (String[] args) { SpringApplication.run(WebAdiminFeignApplication.class, args);
      }
    }

    resources-> application.yml

    Spring:
      application:
        name: vacc-spring-cloud-web-admin-feign
      thymeleaf:
        cache: false
        mode: LEGACYHTML5
        encoding: UTF-8
        servlet:
          content-type: text/html
    
    server:
      port:8765
    
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/

    AdminService

    package com.22bat.vacc.spring.cloud.web.admin.feign.service;
    
    @FeignClient(value = "vacc-spring-cloud-service-admin")
    public interface AdminService{
    
        @RequestMapping(value = "hi", method = RequestMethod.GET)
        public String sayHi(@RequestParam(value = "message")String message);    
    }

    AdminController

    package com.22bat.vacc.spring.cloud.web.admin.feign.controller;
    
    @RestController
    public class AdminController{
    
        @value("${server.port}")
        private String port;
        
        @RequestMapping(value="hi", method = RequestMethod.GET)  
        public String sayHi(String message){
          return String.format("Hi your message is : %s port : %s",message, port);  
        }        
    }

    启动 8765 

    Spring Cloud 使用熔断器防止服务雪崩

       “雪崩”效应(熔断器模型的提出):为了保证高可用,单个服务器通常为集群部署。由于网络原因或自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时如有大量的请求涌入,Servlet 容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩效应”。

      Netflix 开源了 Hystrix 组件,实现了熔断器模式,Spring Cloud对这一组件进行了整合。

    Ribbon中使用熔断器

      在pom.xml中增加依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>

      在 Application 中添加  @EnableHystrix 注解

      在 Service 中添加 @HystrixCommand 注解

    package com.22bat.vacc.spring.cloud.web.admin.ribbon.service;
    
    @Service
    public class AdminService{
    
        @Autowired
        private RestTemplate restTemplate;
        
        @HystrixCommand(fallbackMethod = "hiError")  
        public String sayHi(String message){
          return restTemplate.getForObject("http://vacc-spring-cloud-service-adimin/hi?message="+message,port);  
        }        
    
        public String hiError(String message) {
           return "Hi, your message is :"" + message + "" but request error.";
        }    
    }

    测试熔断器 ,此时关闭服务提供者,再次请求 http://localhost:8764/hi?message=HelloRibbon

    Feign 中使用熔断器

    Feign 是自带熔断器的,但默认是关闭的。需要在配置文件中打开
    application.yml
    feign:
      hystrix:
        enabled: true

      创建熔断器类并实现对应的Feign 接口

    package com.22bat.vacc-spring-cloud.web.admin.feign.service.hystrix;
    
    public class AdminServiceHystrix implements AdminService {
        @Override
        public String sayHi(String message) {
           return "Hi, your message is :"" + message + "" but request error.";

    }
    }

      在 AdminService 接口注解中添加实现类

    @FeignClient(value = "vacc-spring-cloud-service-admin",fallback = AdminServiceHystrix.class)

    访问 localhost:8765

    Spring Cloud 使用熔断器仪表盘监控

     使用Ribbon 和 Feign 项目增加 Hystrix 仪表盘功能,两个项目的改造方式相同

    在pom.xml中增加依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    </dependency>

    在 Application 中增加 @EnableHystrixDashboard 注解

    创建 hystrix.stream 的Servlet配置

    SpringBoot2.x 版本开启 Hystrix Dashboard 与 SpringBoot1.x(增加注解即可)方式略有不同,需要增加一个 HystrixMetricsStreamServlet 的配置,代码如下:

    package com.22bat.vacc.spring.cloud.web.admin.ribbon.config;
    
    @Configuration
    public class HystrixDashboardConfiguration{

    @Bean
    public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); // 没有web.xml ,Java中配置Servlet ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStratup(1); registrationBean.setUrlMappings("/hystrix.stream"); // servlet的访问路径 registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } }

    测试 Hystrix Dashboard -> http://localhost:8765/hystrix

    Hystrix Dashboard 中访问 http:/localhost:8764/hystrix.stream    titile中随便起个名字:WebAdminRibbon

    刷新触发熔断器,可以在 Hystrix Dashboard 中监控到

    Spring Cloud 使用路由网关统一访问接口

     使用路由网关统一访问接口

      在微服务架构中,需要几个基础的服务治理组件,包括服务注册与发现、服务消费、负载均衡、熔断器、智能路由、配置管理等,由这几个基础组件相互写作,共同组建一个简单的微服务系统

      在 Spring Cloud微服务系统中,一种常见的负载均衡方式是,客户端的请求首先经过负载均衡(Zuul、Ngnix),再到达服务网关(Zuul集群),然后再到具体的服务。服务统一注册到高可用的服务注册中心集群,服务的所有的配置文件由配置服务管理,配置服务文件放在 Git 仓库,方便开发人员随时改配置。

      Zuul:主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如/api/usr 转发到 User 服务, /api/shop 转发到shop 服务。Zuul默认和Ribbon结合实现了负载均衡的功能。

     创建工程:vacc-spring-cloud-zuul ,托管pom.xml

    package com.22bat.vacc.spring.cloud.zuul;
    @SpringBootApplication
    @EnableEurekaClient
    @EnableZuulProxy
    public class ZuulApplication {
        public static void main (String[] args) {
            SpringApplication.run(ZuulApplication.class, args); 
      }
    }

    resources->application.yml

    Spring:
      application:
        name: vacc-spring-cloud-zuul
    
    server:
      port:8769
    
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/
    
    zuul:
      routes:
        api-a:
          path: /api/a/**
          serviceId: vacc-spring-cloud-web-admin-ribbon
        api-b:
          path: /api/b/**
          serviceId: vacc-spring-cloud-web-admin-feign

    访问统一网关:localhost:8769/api/a/hi?message=HelloSpringCloud

    配置网关路由失败时的回调

    package com.22bat.vacc.spring.cloud.zuul.provider;
    
    // 路由 vacc-spring-cloud-web-admin-feign 失败时的回调
    @Component
    public class WebAdminFeignFallbackProvider implements FallbackProvider {   
      @Override
      public String getRoute(){// 失败调用;如果需要所有调用都支持回退,则 return "*" 或 return null;
        return "vacc-spring-cloud-web-admin-feign";
      }

      @Override // 如果请求服务失败,返回指定的信息给调用者
      public ClientHttpResponse fallbackResponse(String route, Throwable cause){
        return new ClientHttpResponse() {
          // 网关 api 服务请求失败了,但是消费者客户端向网关发起的请求是成功的,
          // 不应该把 api 的 404,405,500 等问题抛给客户端
    // 网关和 api 服务集群对客户端来说是hi黑盒
          @Override
          public HttpStatus getStatusCode() throws IOException {
            return HttpStatus.OK;
          }

          @Override
          public int getRawStatusCode() throws IOException {
            return HttpStatus.OK.value();
          }

          @Override
          public String getStatusText() throws IOExcepiton{
            return HttpStatus.OK.getReasonPhrase();
          }

          @Override
          public void close() {
          }

          @Override
          public InputStream getBody() throws IOException {
            ObjectMapper objectMapper = new ObjectMapper();
            Map<String, Object> map = new HashMap<>();
            map.put("status", 200);
            map.put("message", "无法连接,请检查您的网络");
            return new ByteArrayInputSteam(objectMapper.writeValueAsString(map).getBytes("UTF-8"));
          }

          @Override
          public HttpHeaders getHeaders() {
            HttpHeaders headers = new HttpHeaders();
            // 和 getBody 中的编码一致
            headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
            return headers;
          }
        };
      }

    }

    Spring Cloud 使用路由网关的的服务过滤功能

    zuul不仅仅是路由,还有很强大的功能,一下是服务过滤功能,比如用在安全验证方面

    创建服务过滤器

    package com.22bat.vacc.spring.cloud.zuul.filter;
    
    @Component
    public class LoginFileter extends ZuulFileter {   @Override
      public String filterType() {
        return "pre";// pre:路由之前;routing:路由之时;post:路由之后;error:发送错误调用
      }
      @Override
      public int filterOrder() {
        return 0;// 过滤的顺序,数值越小执行越靠前
      }
      @Override
      public boolean shouldFilter() {
        return true;// 是否需要过滤,true是需要过滤
      }
      @Override// 过滤器的具体业务代码
      public Object run() throws ZuulException() {
        
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        logger.info("{}>>>{}", request.getMethod, request.getRequestURL.toString);
        String token = request.getParamter("token");
        if (token == null) {
          logger.warn("Token is empty");
          currnetContext.setSendZuulResponse(false);
          currentContext.setResponseStatusCode(401);
          try{
           // HttpServletResponse response = currentContext.getResponse();
           // response.setContextType("text/html;charset=utf-8");
            currentContext.getResponse().getWriter.wirte("Token is empty");
          } catch(IOException) {
            e.printStackTrace();
          }
        }
        return null;
      }
    }

    localhost:8769/api/a/hi?message=HelloSpringCloud&token=123

    SpringCloud 服务配置中心

     创建工程:vacc-spring-cloud-config

    package com.22bat.vacc.spring.cloud.config;
    @SpringBootApplication
    @EnableEurekaClient // 服务提供者 // DiscoverEureka服务消费者 // EnableEurekaServer是Eureka服务器
    @EnableConfigServer
    public class ZuulApplication {
        public static void main (String[] args) {
            SpringApplication.run(ZuulApplication.class, args); 
      }
    }

    resources->application.yml

    // 中央集中式管理

    Spring: application: name: vacc
    -spring-cloud-config
    cloud:
    config:
    label: master // 分支
    server:
    git:
    uri: https://github.com/grant_chen/spring-cloud-config //仓库地址
    search-paths: respo // 放配置的目录
    username: // 仓库的用户名和密码
    password:
    server: port:
    8888 // 默认端口,不能改,修改需要新建 bootstrap.properties(启动的意思) 添加 server.port=8889 // bootstrap.yml/properties是优先被加载 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/

    仓库中respo上传一个配置文件 web-admin-feign-dev.yml

    Spring:
      application:
        name: vacc-spring-cloud-web-admin-feign
      thymeleaf:
        cache: false
        mode: LEGACYHTML5
        encoding: UTF-8
        servlet:
          content-type: text/html
    server: port:
    8765

    feign:
    hystrix:
    enabled: true
    eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/

    启动配置服务中心:localhost:8888/web-admin-feign/dev/master 可以读到配置

    所有配置中添加配置中心的配置

    spring.cloud.config.uri // 配置服务中心的网址
    spring.cloud.config.name // 配置文件名称的前缀
    spring.cloud.config.label // 配置仓库的分支
    spring.cloud.config.profile // 配置文件的环境标识 dev/test/prod

    以feign为例,本地的application.yml文件修改为:

    spring:
      cloud:
        config:
          uri: http://localhost:8888
          name: web-admin-feign
          label: master
          profile: dev

    添加一个appliation-prod.yml

    spring:
      cloud:
        config:
          uri: http://localhost:8888
          name: web-admin-feign
          label: master
          profile: prod

    到feign项目目录下 打jar包,java -jar feign.jar 默认启动application.yml  dev环境的配置

    启动 prod的配置   装载->application-prod.yml   java -jar feign.jar --spring.profiles.active=prod

    Spring Cloud 服务的链路追踪 (ZipKin)

    zipKin简介:ZipKin是一个开放源代码的分布式跟踪系统,由Twitter公司开源,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。它的理论模型来自于Google Dapper论文。

    每个服务向ZipKin 报告计时数据,ZipKin会根据调用关系通过ZipKin UI生成依赖关系图,显示了多少跟踪请求通过每个服务,该系统让开发者可通过一个Web前端轻松收集和分析数据,例如用户每次请求i服务的处理时间等,可方便的检测系统中存在的瓶颈。

    服务追踪说明:微服务架构是通过业务来划分服务的,使用REST调用。对外暴漏一个接口,可能需要很多服务协同才能完整这个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败。随着业务的不断扩张,服务之间互相调用越来越复杂。

    术语:

    Span:基本工作单元,例如,在一个新建的Span中发送一个RPC等同于发送一个回应请求给RPC,Span通过一个64位ID唯一标识,Trace以另一个64位ID表示.

    Trace:一系列Spans组成的一个树状结构,例如,如果你正在运行一个分布式大数据工程,你可能需要创建一个Trace

    Annotation:用来即使记录一个事件的存在,一些核心Annotations用来定义个请求的开始和结束

      cs:Client Sent,客户端发起一个请求,这个Annotation描述了这个Span的开始

      sr:Server Received,服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络延迟

      ss:Server Sent:表明请求处理得完整(当请求返回客户端),如果ss减去sr时间戳便可得到服务端需要得处理请求事件

      cr:Client Received 表明Span得结束,客户端成功接收到服务端得回复,如果cr减去cs时间戳可得到客户端从服务端获取回复得所需事件

    创建一个工程(服务):vacc-spring-cloud-zipkin  -->>pom.xml

    package com.22bat.vacc.spring.cloud.zipkin;
    @SpringBootApplication
    @EnableZipkinServer
    @EnableEurekaClient
    public class ZipKinApplication {
        public static void main (String[] args) {
            SpringApplication.run(ZipKinApplication.class, args);   
      }
    }

    resource--->>>application-dev.yml

    spring:
      application:
         name: vacc-spring-cloud-zipkin
    
    server:
      port: 9411 // zipkin的默认端口
    
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/
    
    management:
      metrics:
        web:
          server:
            auto-time-requests: false

    在所有需要被追踪的项目中增加 spring-cloud-starter-zipkin 依赖

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>

    在这些项目的application.yml 配置文件中增加 ZipKin Server 的地址即可

    spring:
      zipkin:
        base-url: http://localhost:9411

    启动后访问 localhost:9411

    Spring Boot Admin 服务监控:(监控管理系统)

    两个角色,Spring Boot Admin Server,Spring Boot Admin Client ,主要对于各个微服务系统得健康状态、会话数量、并发数、服务资源、延迟等度量信息的收集;

    所有的服务都需要依赖Spring Boot Admin ;

    新建项目:vacc-spring-cloud-admin

    package com.22bat.vacc.spring.cloud.admin;
    @SpringBootApplication
    @EnableAdminServer
    @EnableEurekaClient
    public class AdminApplication {
        public static void main (String[] args) {
            SpringApplication.run(AdminApplication .class, args);      
      }
    }

    resource->application.yml

    spring:
      application:
         name: vacc-spring-cloud-admin
    zipkin:
    base-url: http://localhost:9411 server: port: 8084
    management: endpoint: health: show-details: always
    endpoints:
    web:
    exposure:
    include:["health","info"]
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/

    启动服务端-localhost:8084

    在所有pom.xml中增加客户端的依赖

    <dependency>
      <groupId>org.jolokia</groupId>
      <artifactId>jolokia-core</artifactId>
    </dependency>
    <dependency>
      <groupId>de.codecentric</groupId>
      <artifactId>spring-boot-admin-starter-client</artifactId>
    </dependency>

    所有配置文件中增加

    Spring.boot.admin.client.url: http://localhost:8084

    启动顺序:

    1. 注册与发现 EurekaApplication:8761

    2. 分布式配置中心 ConfigApplication:8888

    3. 服务提供者 ZipKinApplication:9411   AdminApplication:8084  ServiceAdminApplication:8763

    4. 服务消费者 WebAdminFeignApplication:8765

    5. API网关 ZuulApplication

    AdminApplication点开,服务监控

    -----------------------------------------------------------------------------------

    --极限开发--缺什么补什么--只管今天--但每天必须重构--优化

    -----------------------------------------------------------------------------------

    -----------------------------------------------------------------------------------

    -

  • 相关阅读:
    Jmeter参数化-用户定义的变量
    Jmeter进行文件下载
    Jmeter进行文件上传
    Jmeter进行HTTP接口测试
    Jmeter元件作用域及执行顺序
    activiti 汉化
    Spring boot web app项目
    spring boot整合activiti rest api详细教程
    Spring Boot自动配置原理
    spring bean注解使用详解
  • 原文地址:https://www.cnblogs.com/cgy-home/p/12022463.html
Copyright © 2011-2022 走看看