Spring Cloud 微服务实践(二)

第 七 章:费 声明式服务消费 Feign

7-1. Feign 是什么

eign 是 Netflix 公司开发的一个声明式的 REST 调用客户端;
Ribbon 负载均衡、Hystrix 服务熔断是我们 Spring Cloud 中进行微服务开发非常基础的组件,在使用的过程中我们也发现它们一般都是同时出现的,而且配置也都非常相似,每次开发都有很多相同的代码,因此 Spring Cloud 基于 Netflix Feign 整合了 Ribbon 和 Hystrix 两个组件,让我们的开发工作变得更加简单,就像 Spring Boot 是对 Spring+SpringMVC 的简化一样,Spring Cloud Feign对 Ribbon 负载均衡、Hystrix 服务熔断进行简化,在其基础上进行了进一步的封装,不仅在配置上大大简化了开发工作,同时还提供了一种声明式的 Web 服
务客户端定义方式

7-2. 使用 Feign 实现消费者

使用 Feign 实现消费者,我们通过下面步骤进行:
第一步:创建普通 Spring Boot 工程
首先我们来创建一个普通的 Spring Boot 工程,取名为:05-springcloud-service-feign;
第二步:添加依赖
要 添 加 的 依 赖 主 要 是 spring-cloud-starter-netflix-eureka-client 和spring-cloud-starter-feign,如下:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.4.5.RELEASE</version>
</dependency>
<!--Spring Cloud 熔断器起步依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.5.RELEASE</version>
</dependency>

第三步:添加注解
在项目入口类上添加@EnableFeignClients 注解表示开启 Spring Cloud Feign的支持功能;
第四步:声明服务
定义一个 HelloService 接口,通过@FeignClient 注解来指定服务名称,进而绑定服务,然后再通过 SpringMVC 中提供的注解来绑定服务提供者提供的接口,如下:

package com.z.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient("01-springcloud-service-provider")
public interface HelloService {


    @RequestMapping("/s/hello")
    public String hello();
}

这相当于绑定了一个名叫 01-springcloud-service-provider (这里01-springcloud-service-provider大小写 01-SPRINGCLOUD-SERVICE-PROVIDER都可以 ) 的服务提供者提供的/service/hello 接口;
我们服务提供者提供的接口如下:

@GetMapping("/service/hello")
public String hello() {
System.out.println(" 服务提供者 1 。。。。。。。");
return "Hello, Spring Cloud ,Provider 1";
}

第五步:使用 Controller 中调用服务
接着来创建一个 Controller 来调用上面的服务,如下:

@RestController
public class FeignController {
	@Autowired
	HelloService helloService;
	@RequestMapping("/web/hello")
	public String hello() {
		return helloService.hello();
	}
}	

第六步:属性配置
在 application.properties 中指定服务注册中心、端口号等信息,如下:


server.port=8082
# 配置服务的名称
spring.application.name=05-springcloud-service-feign
# 配置 eureka 注册中心地址
eureka.client.service-url.defaultZone=http://eureka8761:8761/eureka/,http://eureka8762:8762/eureka/

第七步:测试
依次启动注册中心、服务提供者和 feign 实现服务消费者,然后访问如下地址:
http://localhost:8082/web/hello

7-3. 使用 Feign 实现消费者的测试

负载均衡:
我们知道,Spring Cloud 提供了 Ribbon 来实现负载均衡,使用 Ribbo 直接注入一个 RestTemplate 对象即可,RestTemplate 已经做好了负载均衡的配置;在 Spring Cloud 下,使用 Feign 也是直接可以实现负载均衡的,定义一个注解有@FeignClient 注解的接口,然后使用@RequestMapping 注解到方法上映射远程的 REST 服务,此方法也是做好负责均衡配置的。
问题,默认是轮询,怎么随机?

我们可以在 application.yml 配置文件中来指定,如下:

# feign和ribbon结合,指定策略。feign默认的是轮询的策略,这里的配置可以自定义
01-springcloud-service-provider:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

01-springcloud-service-provider表示作用到哪个微服务,com.netflix.loadbalancer.RandomRule 即前面介绍 Ribbon 时里面的随机策略,当然,我们也可以指定为其他策略,包括我们自己定义的,只要把相应的包路径写到这即可,很方便。重新启动一下服务消费方的程序,访问上面那个测试 url,可以看到三个订单服务是随机调用的,说明配置生效。

服务熔断:
1、在 application.properties 文件开启 hystrix 功能
feign.hystrix.enabled=true
2、指定熔断回调逻辑

package com.z.springcloud.fallback;

import com.zph.springcloud.service.HelloService;
import org.springframework.stereotype.Component;

@Component
public class MyFallback implements HelloService {
    @Override
    public String hello() {
        return "远程服务发生异常,调用本地逻辑";
    }
}

3、服务熔断获取异常信息:
为@FeignClient修饰的接口加上fallback方法可以实现远程服务发生异常后进行服务的熔断,但是不能获取到远程服务的异常信息,如果要获取远程服务的异常信息,怎么办?此时可以使用 fallbackFactory:

service:
@FeignClient(name = "01-springcloud-service-provider",fallback = MyFallback.class)
public interface HelloService {


    @RequestMapping("/s/hello")
    public String hello();
}

@Component
public class MyFallback implements HelloService {
    @Override
    public String hello() {
        return "远程服务发生异常,调用本地逻辑";
    }
}

------------------
service:
@FeignClient(name = "01-springcloud-service-provider",/*fallback = MyFallback.class,*/ fallbackFactory = MyFallbackFactory.class)
public interface HelloService {


    @RequestMapping("/s/hello")
    public String hello();
}

----------------------
package com.zph.springcloud.fallback;

import com.zph.springcloud.service.HelloService;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

@Component
public class MyFallbackFactory implements FallbackFactory<HelloService> {
    @Override
    public HelloService create(Throwable throwable) {
        return new HelloService() {
            @Override
            public String hello() {
                return throwable.getMessage()+"hahaa";
            }
        };
    }
}

第 八 章:API 网关 Zuul

8-1. Spring Cloud 的 的 Zuul

通过前面内容的学习,我们已经可以基本搭建出一套简略版的微服务架构了,我们有注册中心 Eureka,可以将服务注册到该注册中心中,我们有 Ribbon 或Feign 可以实现对服务负载均衡地调用,我们有 Hystrix 可以实现服务的熔断,但是我们还缺少什么呢?我们首先来看一个微服务架构图:在这里插入图片描述
在上面的架构图中,我们的服务包括:内部服务 Service A 和内部服务 ServiceB,这两个服务都是集群部署,每个服务部署了 3 个实例,他们都会通过 EurekaServer 注册中心注册与订阅服务,而 Open Service 是一个对外的服务,也是集群部署,外部调用方通过负载均衡设备调用 Open Service 服务,比如负载均衡使用 Nginx,这样的实现是否合理,或者是否有更好的实现方式呢?接下来我们主要围绕该问题展开讨论。
1、如果我们的微服务中有很多个独立服务都要对外提供服务,那么我们要如何去管理这些接口?特别是当项目非常庞大的情况下要如何管理?
2、在微服务中,一个独立的系统被拆分成了很多个独立的服务,为了确保安全,权限管理也是一个不可回避的问题,如果在每一个服务上都添加上相同的权限验证代码来确保系统不被非法访问,那么工作量也就太大了,而且维护也非常不方便。
为了解决上述问题,微服务架构中提出了 API 网关的概念,它就像一个安检站一样,所有外部的请求都需要经过它的调度与过滤, 然后 API 网关来实现请求路由、负载均衡、权限验证等功能;那么 Spring Cloud 这个一站式的微服务开发框架基于 Netflix Zuul 实现了Spring Cloud Zuul,采用 Spring Cloud Zuul 即可实现一套 API 网关服务。

8-2. 使用 Zuul 构建 API

1、创建一个普通的 Spring Boot 工程名为 06-springcloud-api-gateway,然后添加相关依赖,这里我们主要添加两个依赖 zuul 和 eureka 依赖:

<!-- 添加 spring cloud 的 zuul 的起步依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- 添加 spring cloud 的 eureka 的客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2、在入口类上添加@EnableZuulProxy 注解,开启 Zuul 的 API 网关服务功能:
@EnableZuulProxy // 开启 Zuul 的 API 网关服务功能

3、在 application.properties 文件中配置路由规则:

# 配置服务内嵌的 Tomcat 端口
server.port=8080
# 配置服务的名称
spring.application.name=06-springcloud-api-gateway
# 配置路由规则
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign
# 配置 API 网关到注册中心上, API 网关也将作为一个服务注册到 eureka-server 上
eureka.client.service-url.defaultZone=http://eureka8761:8761/eureka/,http://eureka8762:8762/eureka/

以上配置,我们的路由规则就是匹配所有符合/api-wkcto/**的请求,只要路径中带有/api-wkcto/都将被转发到 05-springcloud-service-feign 服务上,至于05-springcloud-service-feign 服务的地址到底是什么则由 eureka-server 注册中心去分析,我们只需要写上服务名即可。
以我们目前搭建的项目为例,请求 http://localhost:8080/api-wkcto/web/hello 接口
则相当于请求 http://localhost:8082/web/hello( 05-springcloud-service-feign 服务的地址为 http://localhost:8082/web/hello ),
路由规则中配置的 api-wkcto 是路由的名字,可以任意定义,但是一组 path 和serviceId 映射关系的路由名要相同。
如果以上测试成功,则表示们的 API 网关服务已经构建成功了,我们发送的符合路由规则的请求将自动被转发到相应的服务上去处理。

8-3. 使用 Zuul 进行请求过滤

我们知道 Spring cloud Zuul 就像一个安检站,所有请求都会经过这个安检站,所以我们可以在该安检站内实现对请求的过滤,下面我们以一个权限验证案例说这一点:
1、我们定义一个过滤器类并继承自 ZuulFilter,并将该 Filter 作为一个 Bean:

@Component
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
if (token == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.addZuulResponseHeader("content-type","text/html;charset=utf-8");
ctx.setResponseBody(" 非法访问");
}
return null;
}
}

8-4. Zuul 的路由规则

(1) 在前面的例子中:

# 配置路由规则
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign

当访问地址符合/api-wkcto/**规则的时候,会被自动定位到05-springcloud-service-feign 服务上,不过两行代码有点麻烦,还可以简化为:

zuul.routes.05-springcloud-service-feign=/api-wkcto/**

zuul.routes 后面跟着的是服务名,服务名后面跟着的是路径规则,这种配置方式更简单。
(2) 如果映射规则我们什么都不写,系统也给我们提供了一套默认的配置规则默认的配置规则如下:

# 默认的规则
zuul.routes.05-springcloud-service-feign.path=/05-springcloud-service-feign/**
zuul.routes.05-springcloud-service-feign.serviceId=05-springcloud-service-feign

(3) 默认情况下,Eureka 上所有注册的服务都会被 Zuul 创建映射关系来进行路由。但是对于我这里的例子来说,我希望:05-springcloud-service-feign 提供服务;而 01-springcloud-service-provider 作为服务提供者只对服务消费者提供服务,不对外提供服务。
如果使用默认的路由规则,则 Zuul 也会自动为01-springcloud-service-provider 创建映射规则,这个时候我们可以采用如下方式来让 Zuul 跳过 01-springcloud-service-provider 服务,不为其创建路由规则:

# 忽略掉服务提供者的默认规则
zuul.ignored-services=01-springcloud-service-provider

不给某个服务设置映射规则,这个配置我们可以进一步细化,比如说我不想给/hello 接口路由,那我们可以按如下方式配置:

# 忽略掉某一些接口路径
zuul.ignored-patterns=/**/hello/**

此外,我们也可以统一的为路由规则增加前缀,设置方式如下:

# 配置网关路由的前缀
zuul.prefix=/myapi

此时我们的访问路径就变成了 http://localhost:8080/myapi/web/hello

(4) 路由规则通配符的含义:

通配符含义举例说明
匹配任意单个字符/05-springcloud-service-feign/?匹配/05-springcloud-service-feign/ a,/05-springcloud-service-feign/ b,/05-springcloud-service-feign/ c 等
*匹配任意数量的字符/05-springcloud-service-feign/ *匹配/05-springcloud-service-feign/ aaa,/05-springcloud-service-feign/ bbb,/05-springcloud-service-feign/ ccc 等,无法匹配/05-springcloud-service-feign/ a/b/c
**匹配任意数量的字符/05-springcloud-service-feign/**匹配/05-springcloud-service-feign/ aaa,/05-springcloud-service-feign/ bbb,/05-springcloud-service-feign/ ccc 等,也可以匹配/05-springcloud-service-feign/ a/b/c
  1. 一般情况下 API 网关只是作为各个微服务的统一入口,但是有时候我们可能也需要在 API 网关服务上做一些特殊的业务逻辑处理,那么我们可以让请求到达 API 网关后,再转发给自己本身,由 API 网关自己来处理,那么我们可以进行如下的操作:
    在 06-springcloud-api-gateway 项目中新建如下 Controller:
@RestController
public class GateWayController {
	@RequestMapping("/api/local")
	public String hello() {
		return "exec the api gateway.";
	}
}

然后在 application.properties 文件中配置:

zuul.routes.gateway.path=/gateway/**
zuul.routes.gateway.url=forward:/api/local

引入springboot热部署插件:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
</dependency>

8-5. Zuul 的 的 异常处理

Spring Cloud Zuul 对异常的处理是非常方便的,但是由于 Spring Cloud 处于迅速发展中,各个版本之间有所差异,本案例是以 Finchley.RELEASE 版本为例,来说明 Spring Cloud Zuul 中的异常处理问题。
首先我们来看一张官方给出的 Zuul 请求的生命周期图:
在这里插入图片描述
1.正常情况下所有的请求都是按照 pre、route、post 的顺序来执行,然后由 post返回 response
2.在 pre 阶段,如果有自定义的过滤器则执行自定义的过滤器
3.pre、routing、post 的任意一个阶段如果抛异常了,则执行 error 过滤器
我们可以有两种方式统一处理异常:
1、禁用 zuul 默认的异常处理 SendErrorFilter 过滤器,然后自定义我们自己的Errorfilter 过滤器

配置Errorfilter 过滤器:

zuul.SendErrorFilter.error.disable=true
package com.z.springcloud.controller;

import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ErrorHandlerController implements ErrorController {
    /**
     *  出异常后进入该方法,交由下面的方法处理
     */
    @Override
    public String getErrorPath() {
        //相当于请求到/error
        return "/error";
    }
    @RequestMapping("/error")
    public Object error(){
        System.out.println(11111);
        RequestContext ctx = RequestContext.getCurrentContext();
        ZuulException exception = (ZuulException)ctx.getThrowable();
        return exception.nStatusCode + "--" + exception.getMessage();
    }
}


在AuthFilter类中自定义一个10/0的错误。

2、自定义全局 error 错误页面
注释掉1的配置和类。

@RestController
public class ErrorHandlerController implements ErrorController {
/**
*  出异常后进入该方法,交由下面的方法处理
*/
@Override
public String getErrorPath() {
//相当于请求到/error
return "/error";
}
@RequestMapping("/error")
public Object error(){
RequestContext ctx = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException)ctx.getThrowable();
return exception.nStatusCode + "--" + exception.getMessage();
}
}

第 九 章:Spring Cloud Config

9-1. Spring Cloud Config 是什么

在分布式系统中,尤其是当我们的分布式项目越来越多,每个项目都有自己的配置文件,对配置文件的统一管理就成了一种需要,而 Spring Cloud Config 就提供了对各个分布式项目配置文件的统一管理支持。Spring Cloud Config 也叫分布式配置中心,市面上开源的分布式配置中心有很多,比如国内的,360 的QConf、淘宝的 diamond、百度的 disconf 都是解决分布式系统配置管理问题,国外也有很多开源的配置中心 Apache 的 Apache Commons Configuration、owner、cfg4j 等等;
Spring Cloud Config 是一个解决分布式系统的配置管理方案。它包含 Client和 Server 两个部分,Server 提供配置文件的存储、以接口的形式将配置文件的内容提供出去,Client 通过接口获取数据、并依据此数据初始化自己的应用。Spring cloud 使用 git 或 svn 存放配置文件,默认情况下使用 git。

9-2. 构建 Springcloud config 配置中心

构建一个 spring cloud config 配置中心按照如下方式进行:
1、创建一个普通的 Spring Boot 项目
2、在 pom.xml 文件中添加如下依赖:

org.springframework.cloud spring-cloud-config-server 3、在入口类,也就是 main 方法的类上添加注解 @EnableConfigServer 4、在 application.properties 中配置一下 git 仓库信息,此处我们使用 GitHub( 也 可 以 使 用 码 云 gitee ) , 首 先 在 我 的 Github 上 创 建 一 个 名 为spring-cloud-config 的项目,创建之后,再做如下配置: ```css server.port=3721 spring.application.name=07-springcloud-config-server spring.cloud.config.server.git.uri=https://github.com/myspring/sprin g-cloud-config.git spring.cloud.config.server.git.search-paths=config-center spring.cloud.config.server.git.username=xxxx@163.com spring.cloud.config.server.git.password=xxxx123456 ```

其中:
1.uri 表示配置中心所在仓库的位置
2.search-paths 表示仓库下的子目录
3.username 表示你的 GitHub 用户名
4.password 表示你的 GitHub 密码
至此我们的配置中心服务端就创建好了。

9-3. 构建 Springcloud config配置中心仓库

接下来我们需要在 github 上设置好配置中心,首先在本地创建一个文件夹叫wkcto,然后在里面创建一个文件夹叫 config-center,然后在 config-center中创建四个配置文件,如下:
pplication.properties
application-dev.properties
application-test.properties
application-online.properties

在四个文件里面分别写上要测试的内容:
url=http://www.wkcto.com
url=http://dev.wkcto.com
url=http://test.wkcto.com
url=http://online.wkcto.com
然后回到 wkcto 目录下,依次执行如下命令将本地文件同步到 Github 仓库中:
1、添加提交人的账号信息,git 需要知道提交人的信息作为标识;
git config --global user.name ‘junge’
git config --global user.email ‘junge@163.com’
2、将该目录变为 git 可以管理的目录;
git init
2、将文件添加到暂存区;
git add config-center/
3、把文件提交到本地仓库;
git commit -m ‘add config-center’
4、添加远程主机;
git remote add origin https://github.com/hnylj/spring-cloud-config.git
5、将本地的 master 分支推送到 origin 主机;
git push -u origin master

至此,我们的配置文件就上传到 GitHub 上了。此时启动我们的配置中心,通过/{application}/{profile}/{label}就能访问到我们的配置文件了;
其中:
{application} 表示配置文件的名字,对应的配置文件即 application,
{profile} 表示环境,有 dev、test、online 及默认,
{label} 表示分支,默认我们放在 master 分支上,
通过浏览器上访问 http://localhost:3721/application/dev/master返回的 JSON 格式的数据:
name 表示配置文件名 application 部分,
profiles 表示环境部分,
label 表示分支,
version 表示 GitHub 上提交时产生的版本号,
同时当我们访问成功后,在控制台会打印了相关的日志信息;当访问成功后配置中心会通过 git clone 命令将远程配置文件在本地也保存一
份,以确保在 git 仓库故障时我们的应用还可以继续正常使用。

9-4. 构建 Springcloud config

前面已经搭建好了配置中心的服务端,接下来我们来看看如何在客户端应用中使用。
1、创建一个普通的 Spring Boot 工程 08-springcloud-config-client,并添加如下依赖:

org.springframework.cloud spring-cloud-starter-config 2、创建 bootstrap.properties 文件,用于获取配置信息,文件内容如下:

server.port=3722
spring.application.name=application
spring.cloud.config.profile=dev
spring.cloud.config.label=master
spring.cloud.config.uri=http://localhost:3721/

其中:
name 对应配置文件中的 application 部分,
profile 对应了 profile 部分,
label 对应了 label 部分,
uri 表示配置中心的地址。
3、创建一个 Controller 进行测试:


package com.zph.springcloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope
public class ConfigController {
    @Value("${url}")
    private String url;

    @Autowired
    private Environment env;
    @RequestMapping("/cloud/url")
    public String url () {
        return this.url;
    }
    @RequestMapping("/cloud/url2")
    public String url2 () {
        return env.getProperty("url");
    }
}

我们可以直接使用@Value 注解注入配置的属性值,也可以通过 Environment对象来获取配置的属性值。

9-5. Springcloud config 配置中心客户端测试

通过客户端应用测试是否能够获取到配置中心配置的数据;

9-6. Springcloud config 的工作原理

Spring cloud Config Server 的工作过程如下图所示:
在这里插入图片描述
1、首先需要一个远程 Git 仓库,平时测试可以使用 GitHub,在实际生产环境中,需要自己搭建一个 Git 服务器,远程 Git 仓库的主要作用是用来保存我们的配置文件;
2、除了远程 Git 仓库之外,我们还需要一个本地 Git 仓库,每当 Config Server访问远程 Git 仓库时,都会克隆一份到本地,这样当远程仓库无法连接时,就直接使用本地存储的配置信息;
3、微服务 A、微服务 B 则是我们的具体应用,这些应用在启动的时会从 ConfigServer 中获取相应的配置信息;
4.当微服务 A、微服务 B 尝试从 Config Server 中加载配置信息的时候,ConfigServer 会先通过 git clone 命令克隆一份配置文件保存到本地;
5、由于配置文件是存储在 Git 仓库中,所以配置文件天然具有版本管理功能;

9-7. Springcloud config 的 安全保护

生产环境中我们的配置中心肯定是不能随随便便被人访问的,我们可以加上适当的保护机制,由于微服务是构建在 Spring Boot 之上,所以整合 Spring Security是最方便的方式。
1、在 springcloud config server 项目添加依赖:

org.springframework.boot
spring-boot-starter-security

2、在 springcloud config server 项目的 application.properties 中配置用户名密码:
spring.security.user.name=wkcto
spring.security.user.password=123456
3、在 springcloud config client 上 bootstrap.properties 配置用户名和密码:
spring.cloud.config.username=wkcto
spring.cloud.config.password=123456
4、最后测试验证;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值