Spring Cloud 学习,整理

菜鸟刚接触Spring Cloud,摸着石头过河。


Eureka:

服务注册和发现中心。

介绍和各种配置可以看:https://www.oschina.net/search?scope=blog&q=Eureka&p=1
Eureka集群:http://www.cnblogs.com/fangfuhai/p/6102486.html

这是各种工具的通用配置:
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Dalston.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>


<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
</dependencies>
添加Eureka依赖:
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka-server</artifactId>
    </dependency>
    //访问Eureka需要用户名,密码
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
</dependencies>

配置属性
server:
  port: 8991
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8991/eureka
    #不当成eureka client,本身eureka server就有client
    register-with-eureka: false
    fetch-registry: false

security:
  basic:
    enabled: true
  user:
    name: user
    password: 123456
Eureka本身也是一个客户端,所以需要剔除
#不当成eureka client,本身eureka server就有client
    register-with-eureka: false
    fetch-registry: false
开启Eureka服务:
@SpringBootApplication
@EnableEurekaServer
public class Discovery {

    public static void main(String[] args){
        SpringApplication.run(Discovery.class,args);
    }
}


将服务注册到Eureka中:
添加依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
//这个用于健康检查
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加属性:
server:
  port: 7904
eureka:
  client:
    healthcheck:
          enabled: true
    service-url:
      defaultZone: http://user:123456@localhost:8991/eureka
  instance:
    prefer-ip-address: true #IP来做Status
    #status-page-url-path: ${management.context-path}/info
    #health-check-url-path: ${management.context-path}/health

@SpringBootApplication
@EnableEurekaClient
public class UserService {
    public static void main(String[] args){
        SpringApplication.run(UserService.class,args);

    }
}
这样就将服务注册到Eureka中
Eureka实现高可用

spring:
  profiles: peer1
eureka:
  instance:
    hostname: peer1
  client:
    serviceUrl:
      defaultZone: http://peer2/eureka/

---
spring:
  profiles: peer2
eureka:
  instance:
    hostname: peer2
  client:
    serviceUrl:
      defaultZone: http://peer1/eureka/
修改本地hosts文件,将127.0.0.1映射到peer1,peer2,

其实这个就是将Eureka服务相互注册,A注册到B中,B注册到A中,第一个启动Eureka会报错,因为会找不到注册中心,但是不用管他


Eureka各个属性详解:https://my.oschina.net/u/2529405/blog/736239


Ribbon:

客户端负载均衡器
具体可以看:https://www.oschina.net/search?scope=blog&q=Ribbon

由于Eureka依赖中已经有Ribbon的jar了,所以不需要添加其他依赖

Ribbon开启负载均衡:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

@Controller
public class MovieController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;
    
    //通过Ribbon访问UserService的接口
    @GetMapping("movie/{id}")
    public User findUserById(@PathVariable Integer id){

        return this.restTemplate.getForObject("http://userviceservice/getUser/"+id,User.class);
    }
    //这个是Robbin的客户端,直接使用
    @GetMapping("load/{id}")
    @ResponseBody
    public String Load(@PathVariable Integer id){
        ServiceInstance userviceservice = loadBalancerClient.choose("userviceservice");
        System.out.println(userviceservice.getServiceId()+"-----"+userviceservice.getHost()+"---------------"+userviceservice.getPort());
    //    ServiceInstance userviceservice2 = loadBalancerClient.choose("userviceservice2");
     //   System.out.println(userviceservice2.getServiceId()+"-----"+userviceservice2.getHost()+"---------------"+userviceservice2.getPort());
        return "1";
    }
}
@RibbonClient(name = "foo", configuration = FooConfiguration.class)
使用注解来开启RibbonClient:name 对方服务,configuration:指定配置,里面可以自定义这些Bean
  • IClientConfig ribbonClientConfig:DefaultClientConfigImpl

  • IRule ribbonRule:ZoneAvoidanceRule

  • IPing ribbonPing:NoOpPing

  • ServerList<Server> ribbonServerList:ConfigurationBasedServerList

  • ServerListFilter<Server> ribbonServerListFilter:ZonePreferenceServerListFilter

  • ILoadBalancer ribbonLoadBalancer:ZoneAwareLoadBalancer

  • ServerListUpdater ribbonServerListUpdater:PollingServerListUpdater

也可以使用application.yml的属性配置Ribbon。
users:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
users指服务名称。

只使用Ribbon,不使用Eureka:
ribbon:
  eureka:
   enabled: false
stores:
  ribbon:
    listOfServers: example.com,google.com

stores是只服务名,listOfServers:指定的服务路径,由于没有开启Eureka,所以需要添加这个

Ribbon使用缓存
ribbon:
  eager-load:
    enabled: true
    clients: client1, client2, client3

Feign:

  声明式Rest客户端。】

具体问题可看:https://www.oschina.net/search?scope=blog&q=Feign
Spring Cloud集成Ribbon和Eureka以在使用Feign时提供负载均衡的http客户端

添加Feign依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
</dependencies>
开启Feign
@SpringBootApplication
@EnableFeignClients
@EnableEurekaClient
public class Feign {

    public static void main(String[] args){
        SpringApplication.run(Feign.class,args);
    }
}

@FeignClient(name = "userservice",configuration = FeignConfig.class)
public interface UserFeignClient {

    @RequestMapping(value = "/getuser/{id}", method = RequestMethod.GET)
    public User getUser(@PathVariable("id") Integer id);
}
Feign的配置 name是服务名,configuration指定Feign配置 。Feign的默认配置是

Spring Cloud Netflix默认为feign(BeanType beanName:ClassName)提供以下bean:

  • Decoder feignDecoder:ResponseEntityDecoder(其中包含SpringDecoder

  • Encoder feignEncoder:SpringEncoder

  • Logger feignLogger:Slf4jLogger

  • Contract feignContract:SpringMvcContract //通过MVC的方式去调用其他服务的接口

  • Feign.Builder feignBuilder:HystrixFeign.Builder

  • Client feignClient:如果Ribbon启用,则为LoadBalancerFeignClient,否则将使用默认的feign客户端。

configuration:可以配置这些默认值。改用其他的配置

可以通过将 feign.okhttp.enabled feign.httpclient.enabled 设置为 true ,并将它们放在类路径上来使用OkHttpClient和ApacheHttpClient feign客户端。 // 设置用什么HttpClient访问

注意:如果你配置了configuration,你不能将这个类放在Application的包以及他的子包下,不然会产生干扰。

@FeignClient(name = "${feign.name}", url = "${feign.url}")
Feign也可以这么配置,如果使用 URL属性,则一定需要配上name属性

feign.hystrix.enabled=true,//Feign开启断路器

@Configuration
public class FooConfiguration {
    @Bean
	@Scope("prototype")
	public Feign.Builder feignBuilder() {
		return Feign.builder();
	}
} //禁用断路器,,即修改Feign的默认配置,
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    Hello iFailSometimes();
}

static class HystrixClientFallback implements HystrixClient {
    @Override
    public Hello iFailSometimes() {
        return new Hello("fallback");
    }
} // Feign中使用断路器,使用fallback属性。
@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {
	@RequestMapping(method = RequestMethod.GET, value = "/hello")
	Hello iFailSometimes();
}

@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
	@Override
	public HystrixClient create(Throwable cause) {
		return new HystrixClientWithFallBackFactory() {
			@Override
			public Hello iFailSometimes() {
				return new Hello("fallback; reason was: " + cause.getMessage());
			}
		};
	}
} //使用断路器,使用fallbackFactory属性,可以在Throwable cause中的到回退原因

Primary属性,应该和Spring里面的Primary属性差不多,用于同一个接口的不同实现类

feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
feign.compression.response.enabled=true   // Feign请求压缩

Zuul:

简介:http://lxlong.iteye.com/blog/2267985, https://www.oschina.net/search?scope=blog&q=Zuul
路由器

添加依赖:
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
</dependencies>
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args){
        SpringApplication.run(ZuulApplication.class,args);
    }
}  //开启zuul

Zuul的application.yml配置
server:
  port: 7777
  #context-path: /demo 这个配置是等于tomcat中的项目路径,http://localhost:8080/demo
eureka:
  client:
    service-url:
      defaultZone: http://user:123456@localhost:8991/eureka
  instance:
    prefer-ip-address: true
    #IP来做Status
    #status-page-url-path: ${management.context-path}/info
    #health-check-url-path: ${management.context-path}/health
    #instance-id:
    #home-page-url-path: /demo 如果配了context-path,这样配置可以让eureka直接访问/hystrix.stream,而不用/demo/hystrix.stream

spring:
  application:
    name: microservice-getaway-zuul
zuul:
  #prefix: /getuser
  #strip-prefix: false  这个可以用在zuul代理的服务有content-path,这样就可以不用在写content-path了,全局配置
logging:
  level:
    com.netflix: debug
  #ignoredServices: '*'
  #routes:
    #abc:   #更细粒度,这里可以随便写,只要唯一
      #path: /user/**
      #serviceId: user-service-hystrix
    #userservice: /myusers/** userservice映射为myusers,可以通过/myusers访问


    #zuul:
      #routes:
        #users:
          #path: /myusers/**
        #legacy:
          #path: /**  users/myusers/**这么访问,其他的用/**访问
        #sensitiveHeaders 不让http请求的带有的Cookie,Set-Cookie,Authorization等属性传到后端
Zuul不能发现客户端,所以需要Eureka来提供。Zuul还是有点不懂,还需要看。

通过Zuul来上传大文件需要在URL之前加上/Zuul,接着:
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000   //配置超时时间
Zuul支持Hystrix的回退(即通过Zuul访问服务,如果服务挂了,可以做后续处理)
@Component
public class MyFallbackProvider implements ZuulFallbackProvider {
    @Override
    public String getRoute() {
        return "userservice";  //远程的服务,如果需要所有远程服务都支持回退,则这边应 return “*” 或者return null
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK; //状态码
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200; //状态码
            }

            @Override
            public String getStatusText() throws IOException {
                return "OK";  //状态
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("fallback".getBytes());
            }  //返回的内容

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}
Zuul过滤器:
@EnableZuulProxy@EnableZuulServer

Spring Cloud Netflix根据使用何种注释来启用Zuul安装多个过滤器。@EnableZuulProxy@EnableZuulServer的超集。换句话说,@EnableZuulProxy包含@EnableZuulServer安装的所有过滤器。“代理”中的其他过滤器启用路由功能。如果你想要一个“空白”Zuul,你应该使用@EnableZuulServer

@EnableZuulServer过滤器

创建从Spring Boot配置文件加载路由定义的SimpleRouteLocator

安装了以下过滤器(正常Spring豆类):

前置过滤器

  • ServletDetectionFilter:检测请求是否通过Spring调度程序。使用键FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY设置布尔值。

  • FormBodyWrapperFilter:解析表单数据,并对下游请求进行重新编码。

  • DebugFilter:如果设置debug请求参数,则此过滤器将RequestContext.setDebugRouting()RequestContext.setDebugRequest()设置为true。

路由过滤器

  • SendForwardFilter:此过滤器使用Servlet RequestDispatcher转发请求。转发位置存储在RequestContext属性FilterConstants.FORWARD_TO_KEY中。这对于转发到当前应用程序中的端点很有用。

过滤器:

  • SendResponseFilter:将代理请求的响应写入当前响应。

错误过滤器:

  • SendErrorFilter:如果RequestContext.getThrowable()不为null,则转发到/错误(默认情况下)。可以通过设置error.path属性来更改默认转发路径(/error)。

@EnableZuulProxy过滤器

创建从DiscoveryClient(如Eureka)以及属性加载路由定义的DiscoveryClientRouteLocator每个serviceIdDiscoveryClient创建路由。随着新服务的添加,路由将被刷新。

除了上述过滤器之外,还安装了以下过滤器(正常Spring豆类):

前置过滤器

  • PreDecorationFilter:此过滤器根据提供的RouteLocator确定在哪里和如何路由。它还为下游请求设置各种与代理相关的头。

路由过滤器

  • RibbonRoutingFilter:此过滤器使用Ribbon,Hystrix和可插拔HTTP客户端发送请求。服务ID位于RequestContext属性FilterConstants.SERVICE_ID_KEY中。此过滤器可以使用不同的HTTP客户端。他们是:

    • Apache HttpClient这是默认的客户端。

    • Squareup OkHttpClient v3。通过在类路径上设置com.squareup.okhttp3:okhttp库并设置ribbon.okhttp.enabled=true来启用此功能。

    • Netflix Ribbon HTTP客户端。这可以通过设置ribbon.restclient.enabled=true来启用。这个客户端有限制,比如它不支持PATCH方法,还有内置的重试。

  • SimpleHostRoutingFilter:此过滤器通过Apache HttpClient发送请求到预定的URL。URL位于RequestContext.getRouteHost()

自定义Filter:
public class MyFliter extends ZuulFilter {
    //filter:4,pre,post,route,error
    @Override
    public String filterType() {
        return "pre";  //在访问服务之前
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    //是否开启Fliter
    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); //注意这个类RequestContext

        String remoteHost = request.getRemoteHost();
        int remotePort = request.getRemotePort();
        String remoteAddr = request.getRemoteAddr();
        System.out.println(remoteAddr+"---------"+remoteHost+"---------"+remotePort);
        return null;
    }
}
Zuul内部使用Ribbon调用远程URL,并且Ribbon客户端默认在第一次调用时由Spring Cloud加载。 可以使用以下配置更改Zuul的此行为,并将导致在应用程序启动时,子Ribbon相关的应用程序上下文正在加载。


Hystrix:

断路器:当远程服务访问不了的时候会触发。
看:https://www.oschina.net/search?scope=blog&q=Hystrix
添加依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableCircuitBreaker    //开启断路器
@EnableHystrixDashboard  //仪表盘
public class MovieServiceHystrix {
    public static void main(String[] args){
        SpringApplication.run(MovieServiceHystrix.class,args);

    }
}
@HystrixCommand(fallbackMethod = "stubMyService",
    commandProperties = {
      @HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE") //指定策略
    }
) //这个属性应该是用在Ribbon中的,远程服务访问不了时候执行的方法
hystrix查看health和hyxtrix.stream需要
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
如果用Ribbon来处理Hystrix命令,则需要将Hystrix的超时时间长与Ribbon的时间
 (1)使用Hystrix仪表盘:
  添加依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
访问/hystrix.stream即可看到数据。
 (2)使用Turbine:
   Turbine是将所有相关/hystrix.stream端点聚合到Hystrix仪表板中使用的/turbine.stream的应用程序
 添加依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>
application.yml配置






spring:
  application:
    name: microservice-turbine

turbine:
  aggregator:
    clusterConfig: MOVIE-SERVICE-HYSTRIX
  appConfig: movie-service-hystrix
  #这是监控多个服务的配置
   #aggregator:
      #clusterConfig: default
   #appConfig: customers,stores
    #clusterNameExpression: "'default'"

#turbine.instanceUrlSuffix.MOVIE-SERVICE-HYSTRIX: /demo/hystrix.stream   配置完context-path之后,配置这个就可以直接/hystrix.stream
http://my.turbine.sever:8080/turbine.stream?cluster=<CLUSTERNAME> ;使用Turbine地址加配置的clusterConfig去访问
@SpringBootApplication
@EnableTurbine  //开启Turbine
public class TurbineApplication {
    public static void main(String[] args){

        SpringApplication.run(TurbineApplication.class,args);
    }
}
Turbine基本就是用来查看访问远程服务的次数,频率什么的。

Spring Cloud Config :

Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持。 使用Config Server,您可以在所有环境中管理应用程序的外部属性。 客户端和服务器上的概念映射与Spring EnvironmentPropertySource抽象相同,因此它们与Spring应用程序非常契合,但可以与任何以任何语言运行的应用程序一起使用。 随着应用程序通过从开发人员到测试和生产的部署流程,您可以管理这些环境之间的配置,并确定应用程序具有迁移时需要运行的一切。 服务器存储后端的默认实现使用git,因此它轻松支持标签版本的配置环境,以及可以访问用于管理内容的各种工具。(主要还是用于从Git上或者其他地方拉取配置文件)
介绍:https://www.oschina.net/search?scope=blog&q=Spring+Cloud+Config

用下列形式的方式进行访问:
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

添加依赖:
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>  //服务端
    </dependency>
</dependencies>
application.yml配置:
  cloud:
    config:
      server:
        git:
          #uri: https://git.oschina.net/8277/{application} //通配符
          uri: https://git.oschina.net/8277/spring-cloud-server-test  //远程库
#这个client会读取git上的application.properties的文件,比如端口号,导致本地的被覆盖掉了


  #cloud:
    #config:
      #server:
        #git:
          #uri: https://git.oschina.net/8277/spring-cloud-server-test //默认,如果访问不到就访问这个
          #repos:
            #simple: https://git.oschina.net/8277/simple
            #special:
              #pattern: special*/dev*,*special*/dev*  //匹配,满足这些才能被访问
              #uri: https://git.oschina.net/8277/special
开启Server:
@SpringBootApplication
@EnableConfigServer
public class ConfigServer {
    public static void main(String[] args){

        SpringApplication.run(ConfigServer.class,args);
    }
}
客户端:
添加依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
application.yml配置,由于Spring Cloud有限扫描bootstrap.yml,所以我们需要新建一个bootstrap.yml,
配置:
spring:
  cloud:
    config:
      uri: http://localhost:7913   //服务端地址
      label: master  //这个是分支
      profile: default //视频说应该这个配,将访问项目名配成application.name,profile配成-后面的东西,我不是很理解
  application:
    name: simple   #我怎么才能让client可以访问多个server
server:
  port: 7914
对了 这里还有一个坑,如果服务端的配置文件中配置了端口,会将客户端配置的端口号覆盖。
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/spring-cloud-samples/config-repo
          searchPaths: foo,bar*

在此示例中,服务器搜索顶级和“foo /”子目录以及名称以“bar”开头的任何子目录中的配置文件。

认证:
服务端:
dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
</dependencies>
  cloud:
    config:
      server:
        git:
          uri: https://git.oschina.net/8277/spring-cloud-server-test
security:
  user:
    password: 123456
    name: user
  basic:
    enabled: true
客户端:
spring:
  cloud:
    config:
      uri: http://user:123456@localhost:7917
      label: master
      profile: dev
      #username: user
      #password: 123456 属性优先级比URL  application:
    name: foobar   #我怎么才能让client可以访问多个server
server:
  port: 7918
加密解密:
需要Server.jks,具体生成可以看文档,需要CURL工具。
服务端:
encrypt:
  key: abc 对称加密 这个密钥好像和文件也有点关系,如果密钥是abc,那么文件也要以abc开头
将server.jks放在classpath下

在远程的文件中放的是加密后的信息,用client访问就可以得到解密后的信息
properties: profile = {cipher}d82961bcd0b29c3289e032f7811077d3ec711df0802c9529843b1492cd9f6b2b
yml:profile:‘{cipher}d82961bcd0b29c3289e032f7811077d3ec711df0802c9529843b1492cd9f6b2b’

非对称加密:
encrypt:
  keyStore:
    location: classpath:/server.jks
    password: letmein  //这些属性都是使用curl的时候自己写的
    alias: mytestkey
    secret: changeme

用 远程文件更改后, 用BUS刷新项目中获取的值。用的是RabbitMQ,具体操作 我蒙蔽了 实在有点不懂

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值