SpringBoot整合Spring Boot Admin监控

本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringBoot相关知识相关知识,打造完整的SpringBoot学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringBoot专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
专栏汇总:专栏汇总

监控

​ 在说监控之前,需要回顾一下软件业的发展史。最早的软件完成一些非常简单的功能,代码不多,错误也少。随着软件功能的逐步完善,软件的功能变得越来越复杂,功能不能得到有效的保障,这个阶段出现了针对软件功能的检测,也就是软件测试。伴随着计算机操作系统的逐步升级,软件的运行状态也变得开始让人捉摸不透,出现了不稳定的状况。伴随着计算机网络的发展,程序也从单机状态切换成基于计算机网络的程序,应用于网络的程序开始出现,由于网络的不稳定性,程序的运行状态让使用者更加堪忧。互联网的出现彻底打破了软件的思维模式,随之而来的互联网软件就更加凸显出应对各种各样复杂的网络情况之下的弱小。计算机软件的运行状况已经成为了软件运行的一个大话题,针对软件的运行状况就出现了全新的思维,建立起了初代的软件运行状态监控。

​ 什么是监控?就是通过软件的方式展示另一个软件的运行情况,运行的情况则通过各种各样的指标数据反馈给监控人员。例如网络是否顺畅、服务器是否在运行、程序的功能是否能够整百分百运行成功,内存是否够用,等等等等。

​ 本章要讲解的监控就是对软件的运行情况进行监督,但是springboot程序与非springboot程序的差异还是很大的,为了方便监控软件的开发,springboot提供了一套功能接口,为开发者加速开发过程。

监控的意义

​ 对于现代的互联网程序来说,规模越来越大,功能越来越复杂,还要追求更好的客户体验,因此要监控的信息量也就比较大了。由于现在的互联网程序大部分都是基于微服务的程序,一个程序的运行需要若干个服务来保障,因此第一个要监控的指标就是服务是否正常运行,也就是监控服务状态是否处理宕机状态。一旦发现某个服务宕机了,必须马上给出对应的解决方案,避免整体应用功能受影响。其次,由于互联网程序服务的客户量是巨大的,当客户的请求在短时间内集中达到服务器后,就会出现各种程序运行指标的波动。比如内存占用严重,请求无法及时响应处理等,这就是第二个要监控的重要指标,监控服务运行指标。虽然软件是对外提供用户的访问需求,完成对应功能的,但是后台的运行是否平稳,是否出现了不影响客户使用的功能隐患,这些也是要密切监控的,此时就需要在不停机的情况下,监控系统运行情况,日志是一个不错的手段。如果在众多日志中找到开发者或运维人员所关注的日志信息,简单快速有效的过滤出要看的日志也是监控系统需要考虑的问题,这就是第三个要监控的指标,监控程序运行日志。虽然我们期望程序一直平稳运行,但是由于突发情况的出现,例如服务器被攻击、服务器内存溢出等情况造成了服务器宕机,此时当前服务不能满足使用需要,就要将其重启甚至关闭,如果快速控制服务器的启停也是程序运行过程中不可回避的问题,这就是第四个监控项,管理服务状态。以上这些仅仅是从大的方面来思考监控这个问题,还有很多的细节点,例如上线了一个新功能,定时提醒用户续费,这种功能不是上线后马上就运行的,但是当前功能是否真的启动,如果快速的查询到这个功能已经开启,这也是监控中要解决的问题,等等。看来监控真的是一项非常重要的工作。

​ 通过上述描述,可以看出监控很重要。那具体的监控要如何开展呢?还要从实际的程序运行角度出发。比如现在有3个服务支撑着一个程序的运行,每个服务都有自己的运行状态。

在这里插入图片描述

​ 此时被监控的信息就要在三个不同的程序中去查询并展示,但是三个服务是服务于一个程序的运行的,如果不能合并到一个平台上展示,监控工作量巨大,而且信息对称性差,要不停的在三个监控端查看数据。如果将业务放大成30个,300个,3000个呢?看来必须有一个单独的平台,将多个被监控的服务对应的监控指标信息汇总在一起,这样更利于监控工作的开展。

在这里插入图片描述

​ 新的程序专门用来监控,新的问题就出现了,是被监控程序主动上报信息还是监控程序主动获取信息?如果监控程序不能主动获取信息,这就意味着监控程序有可能看到的是很久之前被监控程序上报的信息,万一被监控程序宕机了,监控程序就无法区分究竟是好久没法信息了,还是已经下线了。所以监控程序必须具有主动发起请求获取被监控服务信息的能力。

在这里插入图片描述

​ 如果监控程序要监控服务时,主动获取对方的信息。那监控程序如何知道哪些程序被自己监控呢?不可能在监控程序中设置我监控谁,这样互联网上的所有程序岂不是都可以被监控到,这样的话信息安全将无法得到保障。合理的做法只能是在被监控程序启动时上报监控程序,告诉监控程序你可以监控我了。看来需要在被监控程序端做主动上报的操作,这就要求被监控程序中配置对应的监控程序是谁。

​ 被监控程序可以提供各种各样的指标数据给监控程序看,但是每一个指标都代表着公司的机密信息,并不是所有的指标都可以给任何人看的,乃至运维人员,所以对被监控指标的是否开放出来给监控系统看,也需要做详细的设定。

​ 以上描述的整个过程就是一个监控系统的基本流程。

总结

  1. 监控是一个非常重要的工作,是保障程序正常运行的基础手段
  2. 监控的过程通过一个监控程序进行,它汇总所有被监控的程序的信息集中统一展示
  3. 被监控程序需要主动上报自己被监控,同时要设置哪些指标被监控

思考

​ 下面就要开始做监控了,新的问题就来了,监控程序怎么做呢?难道要自己写吗?肯定是不现实的,如何进行监控,咱们下节再讲。

可视化监控平台

​ springboot抽取了大部分监控系统的常用指标,提出了监控的总思想。然后就有好心的同志根据监控的总思想,制作了一个通用性很强的监控系统,因为是基于springboot监控的核心思想制作的,所以这个程序被命名为Spring Boot Admin

​ Spring Boot Admin,这是一个开源社区项目,用于管理和监控SpringBoot应用程序。这个项目中包含有客户端和服务端两部分,而监控平台指的就是服务端。我们做的程序如果需要被监控,将我们做的程序制作成客户端,然后配置服务端地址后,服务端就可以通过HTTP请求的方式从客户端获取对应的信息,并通过UI界面展示对应信息。

​ 下面就来开发这套监控程序,先制作服务端,其实服务端可以理解为是一个web程序,收到一些信息后展示这些信息。

服务端开发

步骤①:导入springboot admin对应的starter,版本与当前使用的springboot版本保持一致,并将其配置成web工程

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <version>2.5.4</version>
</dependency>

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

​ 上述过程可以通过创建项目时使用勾选的形式完成。

在这里插入图片描述

步骤②:在引导类上添加注解@EnableAdminServer,声明当前应用启动后作为SpringBootAdmin的服务器使用

@SpringBootApplication
@EnableAdminServer
public class Springboot25AdminServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot25AdminServerApplication.class, args);
    }
}

​ 做到这里,这个服务器就开发好了,启动后就可以访问当前程序了,界面如下。

在这里插入图片描述

​ 由于目前没有启动任何被监控的程序,所以里面什么信息都没有。下面制作一个被监控的客户端程序。

客户端开发

​ 客户端程序开发其实和服务端开发思路基本相似,多了一些配置而已。

步骤①:导入springboot admin对应的starter,版本与当前使用的springboot版本保持一致,并将其配置成web工程

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.5.4</version>
</dependency>

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

​ 上述过程也可以通过创建项目时使用勾选的形式完成,不过一定要小心,端口配置成不一样的,否则会冲突。

步骤②:设置当前客户端将信息上传到哪个服务器上,通过yml文件配置

spring:
  boot:
    admin:
      client:
        url: http://localhost:8080

​ 做到这里,这个客户端就可以启动了。启动后再次访问服务端程序,界面如下。

在这里插入图片描述

​ 可以看到,当前监控了1个程序,点击进去查看详细信息。

在这里插入图片描述

​ 由于当前没有设置开放哪些信息给监控服务器,所以目前看不到什么有效的信息。下面需要做两组配置就可以看到信息了。

  1. 开放指定信息给服务器看

  2. 允许服务器以HTTP请求的方式获取对应的信息

    配置如下:

server:
  port: 80
spring:
  boot:
    admin:
      client:
        url: http://localhost:8080
management:
  endpoint:
    health:
      show-details: always
  endpoints:
    web:
      exposure:
        include: "*"

​ 上述配置对于初学者来说比较容易混淆。简单解释一下,到下一节再做具体的讲解。springbootadmin的客户端默认开放了13组信息给服务器,但是这些信息除了一个之外,其他的信息都不让通过HTTP请求查看。所以你看到的信息基本上就没什么内容了,只能看到一个内容,就是下面的健康信息。

在这里插入图片描述

​ 但是即便如此我们看到健康信息中也没什么内容,原因在于健康信息中有一些信息描述了你当前应用使用了什么技术等信息,如果无脑的对外暴露功能会有安全隐患。通过配置就可以开放所有的健康信息明细查看了。

management:
  endpoint:
    health:
      show-details: always

​ 健康明细信息如下:
在这里插入图片描述

​ 目前除了健康信息,其他信息都查阅不了。原因在于其他12种信息是默认不提供给服务器通过HTTP请求查阅的,所以需要开启查阅的内容项,使用*表示查阅全部。记得带引号。

endpoints:
  web:
    exposure:
      include: "*"

​ 配置后再刷新服务器页面,就可以看到所有的信息了。

在这里插入图片描述

​ 以上界面中展示的信息量就非常大了,包含了13组信息,有性能指标监控,加载的bean列表,加载的系统属性,日志的显示控制等等。

配置多个客户端

​ 可以通过配置客户端的方式在其他的springboot程序中添加客户端坐标,这样当前服务器就可以监控多个客户端程序了。每个客户端展示不同的监控信息。

​ 进入监控面板,如果你加载的应用具有功能,在监控面板中可以看到3组信息展示的与之前加载的空工程不一样。

  • 类加载面板中可以查阅到开发者自定义的类:

在这里插入图片描述
在这里插入图片描述

  • 映射中可以查阅到当前应用配置的所有请求
    在这里插入图片描述在这里插入图片描述

  • 性能指标中可以查阅当前应用独有的请求路径统计数据

在这里插入图片描述
在这里插入图片描述

总结

  1. 开发监控服务端需要导入坐标,然后在引导类上添加注解@EnableAdminServer,并将其配置成web程序即可
  2. 开发被监控的客户端需要导入坐标,然后配置服务端服务器地址,并做开放指标的设定即可
  3. 在监控平台中可以查阅到各种各样被监控的指标,前提是客户端开放了被监控的指标

思考

​ 之前说过,服务端要想监控客户端,需要主动的获取到对应信息并展示出来。但是目前我们并没有在客户端开发任何新的功能,但是服务端确可以获取监控信息,谁帮我们做的这些功能呢?咱们下一节再讲。

监控原理

​ 通过查阅监控中的映射指标,可以看到当前系统中可以运行的所有请求路径,其中大部分路径以/actuator开头

在这里插入图片描述

​ 首先这些请求路径不是开发者自己编写的,其次这个路径代表什么含义呢?既然这个路径可以访问,就可以通过浏览器发送该请求看看究竟可以得到什么信息。

在这里插入图片描述

​ 通过发送请求,可以得到一组json信息,如下

{
    "_links": {
        "self": {
            "href": "http://localhost:81/actuator",
            "templated": false
        },
        "beans": {
            "href": "http://localhost:81/actuator/beans",
            "templated": false
        },
        "caches-cache": {
            "href": "http://localhost:81/actuator/caches/{cache}",
            "templated": true
        },
        "caches": {
            "href": "http://localhost:81/actuator/caches",
            "templated": false
        },
        "health": {
            "href": "http://localhost:81/actuator/health",
            "templated": false
        },
        "health-path": {
            "href": "http://localhost:81/actuator/health/{*path}",
            "templated": true
        },
        "info": {
            "href": "http://localhost:81/actuator/info",
            "templated": false
        },
        "conditions": {
            "href": "http://localhost:81/actuator/conditions",
            "templated": false
        },
        "shutdown": {
            "href": "http://localhost:81/actuator/shutdown",
            "templated": false
        },
        "configprops": {
            "href": "http://localhost:81/actuator/configprops",
            "templated": false
        },
        "configprops-prefix": {
            "href": "http://localhost:81/actuator/configprops/{prefix}",
            "templated": true
        },
        "env": {
            "href": "http://localhost:81/actuator/env",
            "templated": false
        },
        "env-toMatch": {
            "href": "http://localhost:81/actuator/env/{toMatch}",
            "templated": true
        },
        "loggers": {
            "href": "http://localhost:81/actuator/loggers",
            "templated": false
        },
        "loggers-name": {
            "href": "http://localhost:81/actuator/loggers/{name}",
            "templated": true
        },
        "heapdump": {
            "href": "http://localhost:81/actuator/heapdump",
            "templated": false
        },
        "threaddump": {
            "href": "http://localhost:81/actuator/threaddump",
            "templated": false
        },
        "metrics-requiredMetricName": {
            "href": "http://localhost:81/actuator/metrics/{requiredMetricName}",
            "templated": true
        },
        "metrics": {
            "href": "http://localhost:81/actuator/metrics",
            "templated": false
        },
        "scheduledtasks": {
            "href": "http://localhost:81/actuator/scheduledtasks",
            "templated": false
        },
        "mappings": {
            "href": "http://localhost:81/actuator/mappings",
            "templated": false
        }
    }
}

​ 其中每一组数据都有一个请求路径,而在这里请求路径中有之前看到过的health,发送此请求又得到了一组信息

{
    "status": "UP",
    "components": {
        "diskSpace": {
            "status": "UP",
            "details": {
                "total": 297042808832,
                "free": 72284409856,
                "threshold": 10485760,
                "exists": true
            }
        },
        "ping": {
            "status": "UP"
        }
    }
}

​ 当前信息与监控面板中的数据存在着对应关系

在这里插入图片描述

​ 原来监控中显示的信息实际上是通过发送请求后得到json数据,然后展示出来。按照上述操作,可以发送更多的以/actuator开头的链接地址,获取更多的数据,这些数据汇总到一起组成了监控平台显示的所有数据。

​ 到这里我们得到了一个核心信息,监控平台中显示的信息实际上是通过对被监控的应用发送请求得到的。那这些请求谁开发的呢?打开被监控应用的pom文件,其中导入了springboot admin的对应的client,在这个资源中导入了一个名称叫做actuator的包。被监控的应用之所以可以对外提供上述请求路径,就是因为添加了这个包。

在这里插入图片描述

​ 这个actuator是什么呢?这就是本节要讲的核心内容,监控的端点。

​ Actuator,可以称为端点,描述了一组监控信息,SpringBootAdmin提供了多个内置端点,通过访问端点就可以获取对应的监控信息,也可以根据需要自定义端点信息。通过发送请求路劲**/actuator可以访问应用所有端点信息,如果端点中还有明细信息可以发送请求/actuator/端点名称**来获取详细信息。以下列出了所有端点信息说明:

ID描述默认启用
auditevents暴露当前应用程序的审计事件信息。
beans显示应用程序中所有 Spring bean 的完整列表。
caches暴露可用的缓存。
conditions显示在配置和自动配置类上评估的条件以及它们匹配或不匹配的原因。
configprops显示所有 @ConfigurationProperties 的校对清单。
env暴露 Spring ConfigurableEnvironment 中的属性。
flyway显示已应用的 Flyway 数据库迁移。
health显示应用程序健康信息
httptrace显示 HTTP 追踪信息(默认情况下,最后 100 个 HTTP 请求/响应交换)。
info显示应用程序信息。
integrationgraph显示 Spring Integration 图。
loggers显示和修改应用程序中日志记录器的配置。
liquibase显示已应用的 Liquibase 数据库迁移。
metrics显示当前应用程序的指标度量信息。
mappings显示所有 @RequestMapping 路径的整理清单。
scheduledtasks显示应用程序中的调度任务。
sessions允许从 Spring Session 支持的会话存储中检索和删除用户会话。当使用 Spring Session 的响应式 Web 应用程序支持时不可用。
shutdown正常关闭应用程序。
threaddump执行线程 dump。
heapdump返回一个 hprof 堆 dump 文件。
jolokia通过 HTTP 暴露 JMX bean(当 Jolokia 在 classpath 上时,不适用于 WebFlux)。
logfile返回日志文件的内容(如果已设置 logging.file 或 logging.path 属性)。支持使用 HTTP Range 头来检索部分日志文件的内容。
prometheus以可以由 Prometheus 服务器抓取的格式暴露指标。

​ 上述端点每一项代表被监控的指标,如果对外开放则监控平台可以查询到对应的端点信息,如果未开放则无法查询对应的端点信息。通过配置可以设置端点是否对外开放功能。使用enable属性控制端点是否对外开放。其中health端点为默认端点,不能关闭。

management:
  endpoint:
    health:						# 端点名称
      show-details: always
    #    监控端点关闭用如下方式
    info:						# 端点名称
      enabled: false				# 是否开放

​ 为了方便开发者快速配置端点,springboot admin设置了13个较为常用的端点作为默认开放的端点,如果需要控制默认开放的端点的开放状态,可以通过配置设置,如下:

management:
  endpoints:
    enabled-by-default: true	# 是否开启默认端点,默认值true

​ 上述端点开启后,就可以通过端点对应的路径查看对应的信息了。但是此时还不能通过HTTP请求查询此信息,还需要开启通过HTTP请求查询的端点名称,使用“*”可以简化配置成开放所有端点的WEB端HTTP请求权限。

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

​ 整体上来说,对于端点的配置有两组信息,一组是endpoints开头的,对所有端点进行配置,一组是endpoint开头的,对具体端点进行配置。

management:
  endpoint:		# 具体端点的配置
    health:
      show-details: always
      #    监控端点关闭用如下方式
    info:
      enabled: false
  endpoints:	# 全部端点的配置
    web:
      exposure:
        include: "*"
    enabled-by-default: true

总结

  1. 被监控客户端通过添加actuator的坐标可以对外提供被访问的端点功能

  2. 端点功能的开放与关闭可以通过配置进行控制

  3. web端默认无法获取所有端点信息,通过配置开放端点功能

自定义监控指标

​ 端点描述了被监控的信息,除了系统默认的指标,还可以自行添加显示的指标,下面就通过3种不同的端点的指标自定义方式来学习端点信息的二次开发。

INFO端点

​ info端点描述了当前应用的基本信息,可以通过两种形式快速配置info端点的信息

  • 配置形式

    在yml文件中通过设置info节点的信息就可以快速配置端点信息

    info:
      #appName: @project.artifactId@
      #version: @project.version@
      author: hashnode
      company: aaaaa
    

    配置完毕后,对应信息显示在监控平台上

在这里插入图片描述

也可以通过请求端点信息路径获取对应json信息

  • 编程形式

    通过配置的形式只能添加固定的数据,如果需要动态数据还可以通过配置bean的方式为info端点添加信息,此信息与配置信息共存

    @Component
    public class InfoConfig implements InfoContributor {
        @Override
        public void contribute(Info.Builder builder) {
            builder.withDetail("runTime",System.currentTimeMillis());		//添加单个信息
            Map infoMap = new HashMap();		
            infoMap.put("buildTime","2022");
            builder.withDetails(infoMap);									//添加一组信息
        }
    }
    

Health端点

​ health端点描述当前应用的运行健康指标,即应用的运行是否成功。通过编程的形式可以扩展指标信息。

@Component
public class HealthConfig extends AbstractHealthIndicator {
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        boolean condition = true;
        if(condition) {
            builder.status(Status.UP);					//设置运行状态为启动状态
            builder.withDetail("runTime", System.currentTimeMillis());
            Map infoMap = new HashMap();
            infoMap.put("buildTime", "2006");
            builder.withDetails(infoMap);
        }else{
            builder.status(Status.OUT_OF_SERVICE);		//设置运行状态为不在服务状态
            builder.withDetail("上线了吗?","未上线");
        }
    }
}

​ 当任意一个组件状态不为UP时,整体应用对外服务状态为非UP状态。

Metrics端点

​ metrics端点描述了性能指标,除了系统自带的监控性能指标,还可以自定义性能指标。

@Service
public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
    @Autowired
    private BookDao bookDao;

    private Counter counter;

    public BookServiceImpl(MeterRegistry meterRegistry){
        counter = meterRegistry.counter("用户付费操作次数:");
    }

    @Override
    public boolean delete(Integer id) {
        //每次执行删除业务等同于执行了付费业务
        counter.increment();
        return bookDao.deleteById(id) > 0;
    }
}

​ 在性能指标中就出现了自定义的性能指标监控项

在这里插入图片描述

自定义端点

​ 可以根据业务需要自定义端点,方便业务监控

@Component
@Endpoint(id="pay",enableByDefault = true)
public class PayEndpoint {
    @ReadOperation
    public Object getPay(){
        Map payMap = new HashMap();
        payMap.put("level 1","300");
        payMap.put("level 2","291");
        payMap.put("level 3","666");
        return payMap;
    }
}

​ 由于此端点数据spirng boot admin无法预知该如何展示,所以通过界面无法看到此数据,通过HTTP请求路径可以获取到当前端点的信息,但是需要先开启当前端点对外功能,或者设置当前端点为默认开发的端点。

在这里插入图片描述

总结

  1. 端点的指标可以自定义,但是每种不同的指标根据其功能不同,自定义方式不同
  2. info端点通过配置和编程的方式都可以添加端点指标
  3. health端点通过编程的方式添加端点指标,需要注意要为对应指标添加启动状态的逻辑设定
  4. metrics指标通过在业务中添加监控操作设置指标
  5. 可以自定义端点添加更多的指标
  • 9
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
Spring BootSpring Cloud是两个相互补充的框架,可以一起使用来开发分布式系统。 首先,你需要在Spring Boot项目中引入Spring Cloud的依赖。在项目的pom.xml文件中添加以下依赖: ```xml <dependencies> <!-- Spring Boot 核心依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Spring Cloud 相关依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <!-- 其他Spring Cloud组件的依赖 --> <!-- ... --> </dependencies> ``` 接下来,你可以使用Spring Cloud提供的各种组件来实现分布式系统的功能,例如服务注册与发现、负载均衡、断路器等。常用的组件包括Eureka、Ribbon、Feign、Hystrix等。 以服务注册与发现为例,你可以在Spring Boot项目中创建一个Eureka Server,用于服务的注册和发现。在主启动类上添加`@EnableEurekaServer`注解,启用Eureka Server功能。 ```java @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } } ``` 另外,你还可以创建一个普通的Spring Boot服务,并将其注册到Eureka Server上。在这个服务的配置文件中,添加以下配置: ```yaml spring: application: name: your-service-name # 服务名称,用于注册到Eureka Server eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ # Eureka Server的地址 ``` 这样,你就完成了Spring BootSpring Cloud的整合,实现了服务的注册与发现功能。你可以继续探索Spring Cloud的其他组件,根据需求选择合适的组件进行整合

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潮浪之巅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值