zoukankan      html  css  js  c++  java
  • Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十三):配置中心(Config、Bus)

    在线演示

    演示地址:http://139.196.87.48:9002/kitty

    用户名:admin 密码:admin

    技术背景

    如今微服务架构盛行,在分布式系统中,项目日益庞大,子项目日益增多,每个项目都散落着各种配置文件,且随着服务的增加而不断增多。此时,往往某一个基础服务信息变更,都会导致一系列服务的更新和重启,运维也是苦不堪言,而且还很容易出错。于是,配置中心便由此应运而生了。

    目前市面上开源的配置中心有很多,像Spring家族的Spring Cloud Config, Apache的Apache Commons Configuration,淘宝的diamond, 百度的disconf, 360的QConf等等,都是为了解决这类问题。当下Spring体系大行其道,我们当然也优先选择Spring Cloud Config了。

    Spring Cloud Config

    Spring Cloud Config 是一套为分布式系统中的基础设施和微服务应用提供集中化配置的管理方案,它分为服务端与客户端两个部分。服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息。客户端则是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理服务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。

    Spring Cloud Config对服务端和客户端中的环境变量和属性配置 实现了抽象映射,所以它除了适用于 Spring 应用,也是可以在任何其他语言应用中使用的。Spring Cloud Config 实现的配置中心默认采用 Git 来存储配置信息,所以使用 Spring Cloud Config 构建的配置服务器,天然就支持对微服务应用配置信息的版本管理,并且可以通过 Git 客户端工具非常方便的管理和访问配置内容。当然它也提供了对其他存储方式的支持,比如:SVN 仓库、本地化文件系统等。

    实现案例

    准备配置文件

     首先在GIT下,新建config-repo目录,用来存放配置文件,如下图所示,分别模拟了三个环境的配置文件。

     分别编辑三个文件,配置 comsumer.hello 属性的值为 comsumer.hello=hello, this is xx configurations.

    服务端实现

    新建工程

    新建 kitty-conifg 工程,作为配置中心的服务端,负责把GIT仓库的配置文件发布为RESTFul接口。

    添加依赖

    除了Spring Cloud依赖之外,添加配置中心依赖包。

    pom.xml

    复制代码
    <!--spring config-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
    </dependencies>
    复制代码

    启动类

    启动类添加注解 @EnableConfigServer,开启配置服务支持。

    KittyConfigApplication.java

    复制代码
    package com.louis.kitty.config;
    

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.config.server.EnableConfigServer;

    @EnableConfigServer
    @EnableDiscoveryClient
    @SpringBootApplication
    public class KittyConfigApplication {

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> main(String[] args) {
        SpringApplication.run(KittyConfigApplication.</span><span style="color: #0000ff;">class</span><span style="color: #000000;">, args);
    }
    

    }

    复制代码

    配置文件

    修改配置文件,添加如下内容。如果是私有仓库需要填写用户名密码,如果是公开仓库,可以不配置密码。

    application.yml

    复制代码
    server:
      port: 8020
    spring:
      application:
        name: kitty-config
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
        config:
       label: master  # git仓库分支 server: git: uri: https:
    //gitee.com/liuge1988/kitty.git # 配置git仓库的地址 search-paths: config-repo # git仓库地址下的相对地址,可以配置多个,用,分割。 username: username # git仓库的账号 password: password # git仓库的密码
    复制代码

    Spring Cloud Config也提供本地存储配置的方式,只需设置属性spring.profiles.active=native,Config Server会默认从应用的src/main/resource目录下检索配置文件。另外也可以通过spring.cloud.config.server.native.searchLocations=file:D:/properties/属性来指定配置文件的位置。虽然Spring Cloud Config提供了这样的功能,但是为了更好的支持内容管理和版本控制,还是比较推荐使用GIT的方式。

    测试效果

    启动注册中心,配置中心,访问 http://localhost:8020/kitty-consumer/dev,返回结果如下。

    复制代码
    {
        "name": "kitty-consumer",
        "profiles": ["dev"],
        "label": null,
        "version": "1320259308dfdf438f5963f95cbce9e0a76997b7",
        "state": null,
        "propertySources": [{
            "name": "https://gitee.com/liuge1988/kitty.git/config-repo/kitty-consumer-dev.properties",
            "source": {
                "consumer.hello": "hello, this is dev configurations. "
            }
        }]
    }
    复制代码

    访问 http://localhost:8020/kitty-consumer/pro,返回结果如下。

    复制代码
    {
        "name": "kitty-consumer",
        "profiles": ["pro"],
        "label": null,
        "version": "1320259308dfdf438f5963f95cbce9e0a76997b7",
        "state": null,
        "propertySources": [{
            "name": "https://gitee.com/liuge1988/kitty.git/config-repo/kitty-consumer-pro.properties",
            "source": {
                "consumer.hello": "hello, this is pro configurations. "
            }
        }]
    }
    复制代码

    上述的返回的信息包含了配置文件的位置、版本、配置文件的名称以及配置文件中的具体内容,说明server端已经成功获取了git仓库的配置信息。

    访问:http://localhost:8020/kitty-consumer-dev.properties,返回结果如下。

     

    修改一下dev配置文件内容如下(末尾加了一个 2):

     

    再次访问:http://localhost:8020/kitty-consumer-dev.properties,返回结果如下。

     

    发现读取的是修改后提交的信息,说明服务端会自动读取最新提交的数据。

    仓库中的配置文件会被转换成相应的WEB接口,访问可以参照以下的规则:

    • /{application}/{profile}[/{label}]
    • /{application}-{profile}.yml
    • /{label}/{application}-{profile}.yml
    • /{application}-{profile}.properties
    • /{label}/{application}-{profile}.properties

    以kitty-consumer-dev.properties为例子,它的application是kitty-consumer,profile是dev。client会根据填写的参数来选择读取对应的配置。

    客户端实现

    添加依赖

    打开kitty-consumer工程,添加相关依赖。

    pom.xml

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

    配置文件

    添加一个bootstrap.yml配置文件,添加配置中心,并把注册中心的配置移到这里,因为在通过配置中心查找配置时需要通过注册中心的发现服务。

    bootstrap.yml

    复制代码
    spring:
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
        config:
          discovery:
            enabled: true  # 开启服务发现
            serviceId: kitty-config # 配置中心服务名称
          name: kitty-consumer  # 对应{application}部分
          profile: dev  # 对应{profile}部分
          label: master  # 对应git的分支,如果配置中心使用的是本地存储,则该参数无用
    复制代码

    配置说明:

    • spring.cloud.config.uri:配置中心的具体地址
    • spring.cloud.config.name:对应{application}部分
    • spring.cloud.config.profile:对应{profile}部分
    • spring.cloud.config.label:对应git的分支。如果配置中心使用的是本地存储,则该参数无用
    • spring.cloud.config.discovery.service-id:指定配置中心的service-id,便于扩展为高可用配置集群。

    特别注意:

    上面这些与spring cloud相关的属性必须配置在bootstrap.yml中,这样config部分内容才能被正确加载。

    因为config的相关配置会先于application.yml,而bootstrap.yml的加载也是先于application.yml文件的。

    application.yml

    复制代码
    server:
      port: 8005
    spring:
      application:
        name: kitty-consumer
      boot:
        admin:
          client:
            url: "http://localhost:8000"
      zipkin:
        base-url: http://localhost:9411/
      sleuth:
        sampler:
          probability: 1 #样本采集量,默认为0.1,为了测试这里修改为1,正式环境一般使用默认值
    # 开放健康检查接口
    management:
      endpoints:
        web:
          exposure:
            include: "*"
      endpoint:
        health:
          show-details: ALWAYS
    #开启熔断器
    feign:
      hystrix:
        enabled: true
    复制代码

    控制器

    添加一个 SpringConfigController 控制器, 添加注解 @Value("${comsumer.hello}"),声明hello属性从配置文件读取。

    SpringConfigController.java

    复制代码
    package com.louis.kitty.consumer.controller;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    

    @RestController
    class SpringConfigController {

    @Value(</span>"${comsumer.hello}"<span style="color: #000000;">)
    </span><span style="color: #0000ff;">private</span><span style="color: #000000;"> String hello;
    
    @RequestMapping(</span>"/hello"<span style="color: #000000;">)
    </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> String from() {
        </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">this</span><span style="color: #000000;">.hello;
    }
    

    }

    复制代码

    测试效果

    启动注册中心、配置中心,访问: http://localhost:8500,确认相关服务启动并注册。

    访问 http://localhost:8005/hello,返回结果。

    说明客户端已经成功从服务端读取了配置信息。

    现在手动修改一下仓库配置文件的内容,移除末尾数字 2,修改完成并提交。

     再次访问 http://localhost:80052/hello,效果如下。

     

    我们发现返回结果并没有读取最新提交的内容,这是因为Spring Boot项目只有在启动的时候才会获取配置文件的内容,虽然GIT配置信息被修改了,但是客户端并没有重新去获取,所以导致读取的信息仍然是旧配置。那么该如何去解决这个问题呢?这就是我们下一章要讲的 Spring Cloud Bus。

    Refresh机制

    我们在上面讲到,Spring Boot程序只在启动的时候加载配置文件信息,这样在GIT仓库配置修改之后,虽然配置中心服务器能够读取最新的提交信息,但是配置中心客户端却不会重新读取,以至于不能及时的读取更新后的配置信息。这个时候就需要一种通知刷新机制来支持了。

    refresh机制是Spring Cloud Config提供的一种刷新机制,它允许客户端通过POST方法触发各自的/refresh,只要依赖spring-boot-starter-actuator包就拥有了/refresh的功能,下面我们为我们的客户端加上刷新功能,以支持更新配置的读取。

    添加依赖

    我们的 kitty-consumer 在之前已经添加过actuator依赖,所以这里就不用添加了,如果之前没有添加需要加上。actuator是健康检查依赖包,依赖包里携带了 /refresh 的功能。

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

    在使用配置属性的类型加上 @RefreshScope 注解,这样在客户端执行 /refresh 的时候就会刷新此类下面的配置属性了。

    复制代码
    package com.louis.kitty.consumer.controller;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.context.config.annotation.RefreshScope;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    

    @RefreshScope
    @RestController
    class SpringConfigController {

    @Value(</span>"${consumer.hello}"<span style="color: #000000;">)
    </span><span style="color: #0000ff;">private</span><span style="color: #000000;"> String hello;
    
    @RequestMapping(</span>"/hello"<span style="color: #000000;">)
    </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> String from() {
        </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">this</span><span style="color: #000000;">.hello;
    }
    

    }

    复制代码

    修改配置

    健康检查接口开放需要在配置文件添加以下内容,开放refresh的相关接口,因为这个我们在之前也配置过了,所以也不需添加了。

    management:
      endpoints:
        web:
          exposure:
            include: "*"

    通过上面的接口开放配置,以后以post请求的方式访问 http://localhost:8005/actuator/refresh 时,就会更新修改后的配置文件了。

    特别注意:

    这里存在着版本大坑,1.x跟2.x的配置不太一样,我们用的是2.0+版本,务必注意。

    1.安全配置变更

    新版本

    management.endpoints.web.exposure.include="*"

    老版本

    management.security.enabled=false

    2.访问地址变更

    新版本

    http://localhost:8005/actuator/refresh

    老版本

    http://localhost:8005/refresh

    这里还是解释一下上面这个配置起到了什么具体作用,其实actuator是一个健康检查包,它提供了一些健康检查数据接口,refresh功能也是其中的一个接口,但是为了安全起见,它默认只开放了health和info接口(启动信息会包含如下图所示信息),而上面的配置就是设置要开放哪些接口, 我们设置成 “*”,是开放所有接口。你也可以指定开发几个,比如: health,info,refresh,而这里因为我们需要用的refresh功能,所以需要把refresh接口开放出来。

    设置成 “*” 后,启动信息会包含以下信息,而这个叫refresh的post方法,就是我们需要的,上面说的接口地址变更从这里也可以看得出来。

    测试效果

    重新启动服务,访问 http://localhost:8005/hello,返回结果如下。

    修改仓库配置内容,末尾加个数字 5,如下图所示。

    再次访问 http://localhost:8005/hello,如我们所料,结果并没有更新,因为我们还没有调refresh方法。

    通过工具或自写代码发送post请求 http://localhost:8005/actuator/refresh,刷新配置。

    这里通过在线测试网站发送,地址:https://getman.cn/Mo2FX 。

    注意:先让你的Chrome支持跨域。设置方法:在快捷方式的target后加上 --disable-web-security --user-data-dir,重启即可。

    刷新之后,再次访问 http://localhost:8005/hello,返回结果如下。

    查看返回结果,刷新之后已经可以获取最新提交的配置内容,但是每次都需要手动刷新客户端还是很麻烦,如果客户端数量一多就简直难以忍受了,有没有什么比较好的办法来解决这个问题呢,那是当然的,答案就是:Spring Cloud Bus。

    Spring Cloud Bus

    Spring Cloud Bus,被大家称为消息总线,它通过轻量级的消息代理来连接各个分布的节点,可以利用像消息队列的广播机制在分布式系统中进行消息传播,通过消息总线可以实现很多业务功能,其中对于配置中心客户端刷新,就是一个非常典型的使用场景。

    下面这张图可以很好的解释消息总线的作用流程(图片描述来源:纯洁的微笑:配置中心博文)。

    Spring Cloud Bus 进行配置更新步骤如下:

      1、提交代码触发post请求给/actuator/bus-refresh

      2、server端接收到请求并发送给Spring Cloud Bus

      3、Spring Cloud bus接到消息并通知给其它客户端

      4、其它客户端接收到通知,请求Server端获取最新配置

      5、全部客户端均获取到最新的配置

    安装RabbitMQ

    因为我们需要用到消息队列,我们这里选择RabbitMQ,使用Docker进行安装。

    拉取镜像

    执行以下命令,拉取镜像。

    docker pull rabbitmq:management

    完成之后执行以下命令查看下载镜像。

    docker images

    创建容器

    执行以下命令,创建docker容器。

    docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management

    启动成功之后,可以执行以下命令查看启动容器。

    docker ps

    登录界面

    容器启动之后就可以访问web管理界面了,访问 http://宿主机IP:15672。

    系统提供了默认账号。 用户名:guest  密码: guest

    管理界面

    客户端实现

    添加依赖

    打开客户端 kitty-consumer,添加消息总线相关依赖。

    pom.xml

    <!-- bus-amqp -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>

    修改配置

    修改配置,添加RebbitMq的相关配置,这样客户端代码就改造完成了。

    bootstrap.yml

    复制代码
    spring:
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
        config:
          discovery:
            enabled: true  # 开启服务发现
            serviceId: kitty-config # 配置中心服务名称
          name: kitty-consumer  # 对应{application}部分
          profile: dev  # 对应{profile}部分
          label: master  # 对应git的分支,如果配置中心使用的是本地存储,则该参数无用
      rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
    复制代码

    服务端实现

    添加依赖

    修改 kitty-conifg,添加相关依赖。

    pom.xml

    复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
    复制代码

    修改配置,添加RabbitMq的和接口开放相关配置,这样服务端代码也改造完成了。

    application.yml 

    复制代码
    server:
      port: 8020
    spring:
      application:
        name: kitty-config
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
        config:
          label: master  # git仓库分支
          server:
            git:
              uri: https://gitee.com/liuge1988/kitty.git  # 配置git仓库的地址
              search-paths: config-repo  # git仓库地址下的相对地址,可以配置多个,用,分割。
              username: username  # git仓库的账号
              password: password  # git仓库的密码
      rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    复制代码

    测试效果

    1.启动服务端,成功集成消息总线后,启动信息中可以看到如下图中的信息。

    2.启动客户端,发现居然报错了,网上也找不到相关资料,也没见其他人提过相关问题。猜测是网上教程多是使用Euraka,而这里用的时Consul,瞎鼓捣了好久,反正是不想换回Euraka,2.0停止开发消息出来以后,将来还不定什么情况,只能硬着头皮解决了。

    复制代码
    org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'configServerRetryInterceptor' available
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:685) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1210) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:291) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:204) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.getDelegate(AnnotationAwareRetryOperationsInterceptor.java:180) ~[spring-retry-1.2.2.RELEASE.jar:na]
        at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:151) ~[spring-retry-1.2.2.RELEASE.jar:na]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) ~[spring-aop-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.cloud.config.client.ConfigServerInstanceProvider$$EnhancerBySpringCGLIB$$dd44720b.getConfigServerInstances(<generated>) ~[spring-cloud-config-client-2.0.0.RELEASE.jar:2.0.0.RELEASE]
        at org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration.refresh(DiscoveryClientConfigServiceBootstrapConfiguration.java:84) [spring-cloud-config-client-2.0.0.RELEASE.jar:2.0.0.RELEASE]
        at org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration.startup(DiscoveryClientConfigServiceBootstrapConfiguration.java:69) [spring-cloud-config-client-2.0.0.RELEASE.jar:2.0.0.RELEASE]
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131]
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
        at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
    复制代码

    然后就跟踪代码,发现是在下图中的位置找不到相应的Bean,那么答案就比较明显了,要么是程序有BUG,不过可能性不大,那应该是就是缺包了,在缺失的包里有这个Bean。但是这个Bean是在哪个包?排查了半天也没找到,网上也没有想过资料,对比了一下网上消息总线的配置,依赖也没有少加什么。

    没有办法,最后只能自己上手了,不就是在刷新的时候缺少一个拦截器吗,自己给他弄一个试试呗。

    使用就加了一个配置类,并在resources下新建了META-INF目录和一个spring。factories文件。

    RetryConfiguration.java

    复制代码
    package com.louis.kitty.consumer;
    

    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.retry.interceptor.RetryInterceptorBuilder;
    import org.springframework.retry.interceptor.RetryOperationsInterceptor;

    public class RetryConfiguration {
    @Bean
    @ConditionalOnMissingBean(name
    = "configServerRetryInterceptor")
    public RetryOperationsInterceptor configServerRetryInterceptor() {
    return RetryInterceptorBuilder.stateless().backOffOptions(1000, 1.2, 5000).maxAttempts(10).build();
    }
    }

    复制代码

    spring.factories

    org.springframework.cloud.bootstrap.BootstrapConfiguration=com.louis.kitty.consumer.RetryConfiguration

    在这里指定新建的拦截器,这样系统初始化时会加载这个Bean。

    然后重启启动,果然没有报错了,还是先别高兴,看看能不能用先。

    4.先访问一下 http://localhost:8005/hello,效果如下图所示。

     

    5.修改仓库配置文件,把数字5改成15,修改完成提交。

     

    再次访问发现还是旧信息。

    6.再用工具发送post请求 http://localhost:8020/actuator/bus-refresh 。

    注意这次是向注册中心服务端发送请求,发送成功之后服务端会通过消息总线通知所有的客户端进行刷新。

    另外开启消息总线后的请求地址是 /actuator/bus-refresh,不再是refresh了。

    7.给服务端发送刷新请求之后,再次访问 http://localhost:8005/hello,结果如下(需要一点刷新时间)。

     

    我们愉快的发现客户端已经能够通过消息总线获取最新配置了。

    源码下载

    后端:https://gitee.com/liuge1988/kitty

    前端:https://gitee.com/liuge1988/kitty-ui.git


    作者:朝雨忆轻尘
    出处:https://www.cnblogs.com/xifengxiaoma/ 
    版权所有,欢迎转载,转载请注明原文作者及出处。

  • 相关阅读:
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    【2020-MOOC-浙江大学-陈越、何钦铭-数据结构】图(第七周的笔记和编程作业)
    PTA刷题笔记(C语言) | 7-6 厘米换算英尺英寸 (15分)
    数据包设置了不允许分片Don't fragment: Set
    今晚直播:Oracle Nologging 全面总结
    DM online-远大见未来 大咖讲堂丨大数据+,信息化应用与技术落地
    PTA刷题笔记(C语言) | 7-2 I Love GPLT (5分)
    用 C 语言开发一门编程语言 — 抽象语法树
  • 原文地址:https://www.cnblogs.com/jpfss/p/11926096.html
Copyright © 2011-2022 走看看