Feign 如何使用 Okhttp 完成 HTTP URL 请求

背景

最近在使用 Feign 的时候碰到了请求超时的问题,借着这个事儿好好的梳理下与 Feign 相关的客户端,以及客户端的配置,此文可以作为《Feign 如何设置超时时间(connectionTimeout、readTimout)》的补充。

1、Feign 支持的客户端类型

Feign 主要支持 3 种客户端,另外 1 个客户端作为对这 3 种客户端的包装,如下:

  1. Client.Default
    如果没有特别指定使用哪个客户端,则 Feign 将使用这个默认的客户端,该客户端内部使用 JDK 的 HttpURLConnnection 来处理 HTTP URL 的请求
  2. HttpClient(ApacheHttpClient)
    Feign 将会通过 feign.httpclient.ApacheHttpClient(基于 Apache httpclient 开源组件) 来处理 HTTP URL 的请求
  3. OkHttpClient
    Feign 将会通过 feign.okhttp.OkHttpClient(基于 Okhttp3 开源组件)来处理 HTTP URL 的请求
  4. LoadBalancerFeignClient
    该类作为 Client.Default、ApacheHttpClient、OkHttpClient 的包装类而存在,内部使用 Ribbon 负载均衡算法获取 server 服务器,可以理解 Feign 是通过该类的 LoadBalancerFeignClient.execute(Request request, Options options) 方法将请求转发给真正的客户端(Client.Default、ApacheHttpClient、OkHttpClient)来完成对 HTTP URL 请求的处理

2、Feign 的 2 个自动配置类

我是以这 2 个自动配置类作为入口来认识 Feign 各种客户端配置的,下面将分别介绍下:

  • org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration
    在这里插入图片描述
    通过上边的源代码截图,我们可以知道以下几件事情:

    1、通过 @ConditionalOnClass({ILoadBalancer.class, Feign.class}) 注解,我们可以知道该自动配置类执行的条件是:当类路径中存在 com.netflix.loadbalancer.ILoadBalancer 和 feign.Feign 该自动配置类就会执行

    2、通过 @AutoConfigureBefore({FeignAutoConfiguration.class}) 注解,我们可以知道当 FeignRibbonClientAutoConfiguration 和 FeignAutoConfiguration 2 个自动配置类都满足执行条件,那么 FeignRibbonClientAutoConfiguration 将优先于 FeignAutoConfiguration 执行

    3、通过注解 @Import({HttpClientFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class}) 该自动配置类同时又引入进来另外 3 个自动配置类:

    • DefaultFeignLoadBalancedConfiguration.class
      负责加载一个包装了 Client.Default 的 LoadBalancerFeignClient 负载均衡客户端
    • HttpClientFeignLoadBalancedConfiguration.class
      负责加载一个包装了 HttpClient(ApacheHttpClient) 的 LoadBalancerFeignClient 负载均衡客户端
    • OkHttpFeignLoadBalancedConfiguration.class
      负责加载一个包装了 OkHttpClient 的 LoadBalancerFeignClient 负载均衡客户端 【我将会再下边的内容里重点介绍】

    4、通过 @EnableConfigurationProperties({FeignHttpClientProperties.class}) 注解,我们可以知道与该自动配置类匹配的配置信息是 FeignHttpClientProperties.class,也就是项目(例如:annoroad-alpha)中 application.yml 配置文件中的如下内容:

    feign:
      httpclient:
        max-connections: 200 # 连接池连接最大闲置数,缺省值是 200
        time-to-live: 900 # 连接最大闲置时间,单位为秒,缺省值是 900秒(15分钟)
        connection-timeout: 2000 # 连接超时,单位为毫秒,缺省值是 2000毫秒(2秒)
    
  • org.springframework.cloud.openfeign.FeignAutoConfiguration
    可以理解 FeignAutoConfiguration 是一个被阉割了负载均衡能力的 FeignRibbonClientAutoConfiguration ,TA 通过内部类的方式(FeignRibbonClientAutoConfiguration 是通过 @import 注解)实现了对 Client.Default、 HttpClient(ApacheHttpClient)、OkHttpClient 3 种中客户端的支持

3、OkHttpFeignLoadBalancedConfiguration

因为本篇的主要讲的是 Feign 中的 Okhttp,所以这里我将以 OkHttpFeignLoadBalancedConfiguration 作为入口进行说明,首先先看下源代码截图:
在这里插入图片描述
通过上边的源代码截图,我们可以知道以下几件事情:

1、这里的有两个 OkHttpClient,一个是 Feign 对 okhttp3.OkHttpClient 的包装类(feign.okhttp.OkHttpClient),另外一个是原始的 okhttp3 的 OkHttpClient
2、通过 @ConditionalOnClass({OkHttpClient.class})、@ConditionalOnProperty({“feign.okhttp.enabled”}) 这两个注解,我们可以知道当类路径存在 feign.okhttp.OkHttpClient(换句话说就是 pom.xml 引入了 feign-okhttp 的 Jar 包),且项目中 application.yml 文件中的 feign.okhttp.enabled=true(例如:annoroad-alpha ),则该自动配置类执行
3、通过内部类 OkHttpFeignConfiguration 对 okhttp3.OkHttpClient 进行了初始化、加载
4、与 okhttp3.OkHttpClient 连接池(ConnectionPool)相关的参数最大空闲连接数、允许连接最大空闲时间,分别是通过 FeignHttpClientProperties 里的 maxConnections、timeToLive 参数来初始化的,这里的 maxConnections、timeToLive 分别等同于 application.yml 文件中的 feign.httpclient.max-conections、feign.httpclient.time-to-live
5、与 okhttp3.OkHttpClient 连接超时相关的设置,是通过 FeignHttpClientProperties 里的 connectionTimeout 参数来初始化的,这里的 connectionTimeout 等同于 application.yml 文件中的 feign.httpclient.connection-timeout

4、坑

  1. readTimeout

    如果你再细心点,可能会发现一个问题:

    Q1:readTimeout 去哪里了呢?!

    我们再回头看下 OkHttpFeignLoadBalancedConfiguration 类:

    class OkHttpFeignLoadBalancedConfiguration {
        @Bean
        @ConditionalOnMissingBean({Client.class})
        public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
            OkHttpClient delegate = new OkHttpClient(okHttpClient);
            return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
        }
    }
    

    实际上处理 HTTP URL 请求的是 feignClient(…) 方法中的 feign.okhttp.OkHttpClient.execute(…) 方法,源码如下:

    public final class OkHttpClient implements Client {
    
        public Response execute(feign.Request input, Options options) throws IOException {
            okhttp3.OkHttpClient requestScoped;
            if (this.delegate.connectTimeoutMillis() == options.connectTimeoutMillis() && this.delegate.readTimeoutMillis() == options.readTimeoutMillis()) {
                requestScoped = this.delegate;
            } else {
                requestScoped = this.delegate.newBuilder().connectTimeout((long)options.connectTimeoutMillis(), TimeUnit.MILLISECONDS).readTimeout((long)options.readTimeoutMillis(), TimeUnit.MILLISECONDS).followRedirects(options.isFollowRedirects()).build();
            }
    
            Request request = toOkHttpRequest(input);
            okhttp3.Response response = requestScoped.newCall(request).execute();
            return toFeignResponse(response, input).toBuilder().request(input).build();
        }
        
    }	
    

    通过上边的代码片段我们可以发现,okhttp3.OkHttpClient 在执行真正的请求之前,会先拿 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 与已初始化的 okhttp3.OkHttpClient 中的 connectTimeout、readTimeout 进行对比,如果完全一致就不用说了。如果不一致的话,则会以 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 的值重新设置 okhttp3.OkHttpClient 中的 connectTimeout、readTimeout。这下我们就知道了,如果有需要自己指定一个 readTimeout,那么就要对 feign.Request.Options 中的 readTimeoutMillis 下手了,那问题又来了:

    Q2:feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 是通过什么来设置的呢?!

    实际上 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 来源于 org.springframework.cloud.openfeign.FeignClientProperties,如下图:

    在这里插入图片描述

    通过上边代码片段中的 @ConfigurationProperties(“feign.client”) 注解,我们可以知道 FeignClientProperties 对应项目中 application.yml 文件中的 feign.client 配置,如下:

    feign:
      client:
        config:
          default: # 服务名,填写 default 为所有服务,或者指定某服务,例如:annoroad-beta
            connectTimeout: 10000 # 连接超时,10秒
            readTimeout: 20000 # 读取超时,20秒
    

    如果想要该设置生效(将指定的值赋值到 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis ),还必须满足一条:

    Q3:connectTimeout 和 readTimeout 必须同时配置!!!!!!!!!!

    至于为啥,看了下边的源代码就会明白了:

    class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
           
        protected void configureUsingProperties(
        	FeignClientProperties.FeignClientConfiguration config, 
        	Feign.Builder builder) {
            
            if (config == null) {
                return;
            }
            if (config.getLoggerLevel() != null) {
                builder.logLevel(config.getLoggerLevel());
            }
            // ========= 此处,必须俩值都不为 null 才会替换新 options ========
            if (config.getConnectTimeout() != null && 
            	config.getReadTimeout() != null) {
                builder.options(
                new Request.Options(
                	config.getConnectTimeout(), config.getReadTimeout()));
            } 
    

    巴拉巴拉说了这么多,简单的来说就是我们可以通过 同时(必须是同时!!!再次强调!!) 设置 application.yml 配置文件中的 feign.client.default.config.connectTimeout、feign.client.default.config.readTimeout 来使自己指定的 readTimeout 生效。

    Ok,再向下深挖,又引发出新的问题:

    Q4:feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis
    到底是在什么时候被赋值为 FeignClientProperties 的 connectionTimout、readTimeout 的呢?!

    先不着急针对这个问题做解答,跟着我的思路从头来过:

    1、首先是 Application 类中的 @EnableFeignClients 注解
    在这里插入图片描述
    2、让我进入到 @EnableFeignClients 注解 内部看看,如下图:
    在这里插入图片描述
    3、FeignClientsRegistrar 是个什么鬼,跟进源代码看看:
    在这里插入图片描述
    TA实现了接口 ImportBeanDefinitionRegistrar,实现了这个接口又有啥用呢?下面插播下 ImportBeanDefinitionRegistrar 接口的简介:

    1、 ImportBeanDefinitionRegistrar 类只能通过其他类 @Import 的方式来加载,通常是启动类或配置类。
    2、使用@ Import,如果括号中的类是 ImportBeanDefinitionRegistrar 的实现类,则会调用接口方法(ImportBeanDefinitionRegistrar#registerBeanDefinitions),将其中要注册的类注册成 Bean。
    3、实现该接口的类拥有注册 Bean 的能力。

    OK,原来是要自定义注册 Bean 的过程啊,那要注册什么 Bean 呢,让我们来看下 FeignClientsRegistrar 的源代码:

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    
    ...

    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        this.registerDefaultConfiguration(metadata, registry);
        
        // !!! 注册所有带 @FeignClient 注解的 Bean
        this.registerFeignClients(metadata, registry);
    }

    public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    	
    	...
        
        while(var17.hasNext()) {
        	
        	...
            
            while(var21.hasNext()) {
                BeanDefinition candidateComponent = (BeanDefinition)var21.next();
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                	
                	...
                    
                    // !!! 注册当前带 @FeignClient 注解的 Bean
                    this.registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }

    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        BeanDefinitionBuilder definition = 
        	BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

        ...

        BeanDefinitionHolder holder = new BeanDefinitionHolder(
        	beanDefinition, className, new String[]{alias});
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

   ...
}

我们将主要注意力放在 FeignClientFactoryBean 身上,顾名思义 TA 就是 FeignClient 的工厂类(用来创建 FeignClient),而 TA 实现了 FactoryBean 接口,下面再插播一下 FactoryBean 的简介:

1、当配置文件中 的 class 属性配置的实现类是 FactoryBean 时,通过 getBean() 方法返回的不是 FactoryBean 本身,而是 FactoryBean#getObject() 方法所返回的对象,相当于 FactoryBean#getObject() 代理了 getBean() 方法
2、我们可以通过实现 FactoryBean 接口,自定义 Bean 的生成

下面继续看 FeignClientFactoryBean 的源代码:

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    public Object getObject() throws Exception {
        return this.getTarget();
    }

    <T> T getTarget() {
        FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class);
        // !!! 生成一个 feign.Feign.Builder
        Builder builder = this.feign(context);

        ...
    }

    protected Builder feign(FeignContext context) {
        FeignLoggerFactory loggerFactory = 
        	(FeignLoggerFactory)this.get(context, FeignLoggerFactory.class);
        Logger logger = loggerFactory.create(this.type);
        Builder builder = ((Builder)this.get(context, Builder.class))
        	.logger(logger)
        	.encoder((Encoder)this.get(context, Encoder.class))
        	.decoder((Decoder)this.get(context, Decoder.class))
        	.contract((Contract)this.get(context, Contract.class));
        // !!! Feign 配置信息
        this.configureFeign(context, builder);
        return builder;
    }

    protected void configureFeign(FeignContext context, Builder builder) {
    	// !!! 原来是这个使用获取的 FeignClientProperties
        FeignClientProperties properties = 
        	(FeignClientProperties)this.applicationContext.getBean(FeignClientProperties.class);
        if (properties != null) {
            if (properties.isDefaultToProperties()) {
                this.configureUsingConfiguration(context, builder);
                this.configureUsingProperties(
                	(FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()), 
                	builder);
                this.configureUsingProperties(
                	(FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);
            } else {
                this.configureUsingProperties(
                	(FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()), 
                	builder);
                this.configureUsingProperties(
                	(FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);
                this.configureUsingConfiguration(context, builder);
            }
        } else {
            this.configureUsingConfiguration(context, builder);
        }
    }  

    protected void configureUsingConfiguration(FeignContext context, Builder builder) {
    	...
    	
        Options options = (Options)this.getOptional(context, Options.class);
        if (options != null) {
            builder.options(options);
        }

        ...
    }

    protected void configureUsingProperties(FeignClientConfiguration config, Builder builder) {
        if (config != null) {

        	...

            if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
                builder.options(new Options(config.getConnectTimeout(), config.getReadTimeout()));
            }
            ...
        }
    }
}	

通过上边代码中的最后两个方法 configureUsingConfiguration、configureUsingProperties,我们可以看到原来是在这里 FeignClientProperties 与 feign.Request.Options 终于有了交集,而后又将 feign.Request.Options 作为属性值赋值到了 feign.Feign.Builder 中。

OK,至此也回答了上边的问题(Q4)…

稍等下,还没玩完 (哈哈哈 :>),既然我们提到 feign.Feign.Builder ,那就顺道再聊聊 TA 是如何创建 feign.Feign 的吧,来看源代码吧:

public abstract class Feign {

	...

    public static class Builder {

    	...

        public Feign build() {
            Factory synchronousMethodHandlerFactory = new Factory(
            	this.client, this.retryer, this.requestInterceptors, 
            	this.logger, this.logLevel, this.decode404, 
            	this.closeAfterDecode, this.propagationPolicy);
            ParseHandlersByName handlersByName = new ParseHandlersByName(
            	this.contract, this.options, this.encoder, 
            	this.decoder, this.queryMapEncoder, this.errorDecoder, 
            	synchronousMethodHandlerFactory);
            return new ReflectiveFeign(
            	handlersByName, this.invocationHandlerFactory, this.queryMapEncoder);
        }
    }
}

ReflectiveFeign.ParseHandlersByName 类在 ReflectiveFeign.ParseHandlersByName#apply 方法中创建了 feign.SynchronousMethodHandler,通过 feign.SynchronousMethodHandler 的 feign.SynchronousMethodHandler#invoke、feign.SynchronousMethodHandler#executeAndDecode 方法将 feign.Request.Options 作为参数传给 feign.Client 接口的具体实现类(例如:feign.okhttp.OkHttpClient)的 feign.Client#execute 方法,然后就是上文提到过在请求 okhttp3.OkHttpClient#execute 之前完成 okhttp3.OkHttpClient(connectTimeout、readTimeout) 与 入参 feign.Request.Options(connectTimeoutMillis、readTimeoutMillis)的对比,然后根据结果判断是否需要重置 okhttp3.OkHttpClient 的 connectTimeout、readTimeout。

  1. connectTimeout

    还记得之前有说过我们可以通过设置项目中(例如:annoroad-alpha) application.yml 文件中的 feign.httpclient.connection-timeout 来设置 okhttp3.OkHttpClient 的 connectionTimeout 吗?!这句话充其量只能说对了 10%,实际上这个配置只对 okhttp3.OkHttpClient 初始化阶段有影响,当我们真正要处理 HTTP URL 请求的时候,最主要的还是依据 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis(如果初始化的 okhttp3.OkHttpClient 实例中的 connectTimeoutMillis、readTimeoutMillis 与 feign.Request.Options 中的不一致,那么 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 将会覆盖 okhttp3.OkHttpClient 实例中的 connectTimeoutMillis、readTimeoutMillis)

5、如何在 springboot 项目中应用 Okhttp

  1. 引入相关 starter

    在 pom.xml 文件中加入 spring-cloud-starter-netflix-ribbon、feign-okhttp 依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <version>2.1.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-okhttp</artifactId>
        <version>10.7.4</version>
    </dependency>
    

    (注:这里我引用的是 spring-cloud-starter-netflix-eureka-client 而非直接引用 spring-cloud-starter-netflix-ribbon,之所以也可以是因为 spring-cloud-starter-netflix-eureka-client 本身依赖 spring-cloud-starter-netflix-eureka-client)

  2. 配置

    在 application.yml 文件中增加如下配置:

    feign:
      client:
        config:
          default: # 服务名,填写 default 为所有服务,或者指定某服务,例如:annoroad-beta
            connectTimeout: 10000 # 连接超时,10秒
            readTimeout: 20000 # 读取超时,20秒
      httpclient:
        enabled: false # 关闭 ApacheHttpClient
        max-connections: 50 # 连接池连接最大连接数(缺省值 200)
        time-to-live: 600 # 连接最大闲置时间,单位为秒,600秒==10分钟(缺省值为 900秒==15分钟)
      okhttp:
        enabled: true # 开启 okhttp
    
  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
注:下文中的 *** 代表文件名中的组件名称。 # 包含: 中文-英文对照文档:【***-javadoc-API文档-中文(简体)-英语-对照版.zip】 jar包下载地址:【***.jar下载地址(官方地址+国内镜像地址).txt】 Maven依赖:【***.jar Maven依赖信息(可用于项目pom.xml).txt】 Gradle依赖:【***.jar Gradle依赖信息(可用于项目build.gradle).txt】 源代码下载地址:【***-sources.jar下载地址(官方地址+国内镜像地址).txt】 # 本文件关键字: 中文-英文对照文档,中英对照文档,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压 【***.jar中文文档.zip】,再解压其中的 【***-javadoc-API文档-中文(简体)版.zip】,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·本文档为双语同时展示,一行原文、一行译文,可逐行对照,避免了原文/译文来回切换的麻烦; ·有原文可参照,不再担心翻译偏差误导; ·边学技术、边学英语。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
Feign-OkHttp是一个依赖库,用于在Spring Boot项目中集成OkHttp。你可以通过在pom.xml文件中添加以下依赖来使用Feign-OkHttp: ```xml <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency> ``` 在项目中引入Feign-OkHttp后,它会自动配置OkHttpClient并进行初始化。通过在application.yml文件中设置feign.okhttp.enabled=true来启用Feign-OkHttp自动配置。 在Feign-OkHttp中,连接池相关的参数如最大空闲连接数和连接最大空闲时间可以在application.yml文件中的feign.httpclient.max-connections和feign.httpclient.time-to-live属性中进行配置。 另外,连接超时的设置是通过application.yml文件中的feign.httpclient.connection-timeout属性进行配置。 需要注意的是,feign.httpclient.connection-timeout只对OkHttpClient的初始化阶段起作用。实际处理HTTP URL请求时,主要依据feign.Request.Options中的connectTimeoutMillis和readTimeoutMillis参数。如果feign.Request.Options中的connectTimeoutMillis和readTimeoutMillis与OkHttpClient实例中的不一致,则以feign.Request.Options中的参数为准。 这就是如何在Spring Boot项目中使用Feign-OkHttp集成OkHttp的方法。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Spring Cloud Open Feign系列【4】集成OkHttp及连接池配置详解](https://blog.csdn.net/qq_43437874/article/details/122169675)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Feign 如何使用 Okhttp 完成 HTTP URL 请求](https://blog.csdn.net/yangchao1125/article/details/104492547)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

cab5

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

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

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

打赏作者

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

抵扣说明:

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

余额充值