文章目录
- 前言
- 正文
- 一、项目环境
- 二、项目结构
- 2.1 包的含义
- 2.2 代理的场景
 
- 三、完整代码示例
- 3.1 定义FeignClient
- 3.2 定义拦截器
- 3.3 配置类
- 3.4 okhttp配置
- 3.5 响应体
- 3.5.1 天行基础响应
- 3.5.2 热点新闻响应
 
- 3.6 代理类
- 3.6.1 代理工厂
- 3.6.2 代理客户端
- 3.6.3 FeignClient的建造器
 
 
- 四、调用&测试
- 4.1 配置信息
- 4.2 测试代码
- 4.3 调用结果
 
 
- 附录
前言
一般来说我们的项目中难免会涉及到调用三方接口的场景。
 以前我们可能用 RestTemplate,或者再用OkHttp优化一下。
但是,在读了本文之后,你将发现使用OpenFeign的 FeignClient来调用三方接口,也是纵享丝滑的。
注意,本文旨在使用FeignClient调用三方接口,不讨论其他情况。比如高版本JDK自带的工具类,或者响应式API。
本文使用FeignClient来调用天行API接口。(https://www.tianapi.com/)
 在天行官网注册账号后,可以申请自己想要调用的API接口。拿到key之后即可调用。每天都有免费的使用次数。
正文
一、项目环境
项目使用 Java 17、SpringBoot 3.3.4 、SpringCloud 2023.0.3
<properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <spring-boot.version>3.3.4</spring-boot.version>
        <spring-cloud.version>2023.0.3</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.32</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-okhttp -->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-loadbalancer -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
    </dependencies>
<dependencyManagement>
      <dependencies>
      <!-- springboot依赖 -->
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-dependencies</artifactId>
              <version>${spring-boot.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${spring-cloud.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
   </dependencies>
 </dependencyManagement>
二、项目结构

2.1 包的含义
- com.pine.client.core.tianxing: 定义FeignClient接口;
- com.pine.client.interceptor:拦截器
- com.pine.client.config:配置类
- com.pine.client.proxy:代理相关内容
- com.pine.client.beans:请求体+响应体
2.2 代理的场景

 假设你现在有内网和外网两种网络环境,应用部署在内网,现在你的应用需要访问外部三方接口,需要开白名单;
但是,一般而言,不会直接给你的应用开启白名单,会统一经过一个代理机进行跳转,也就是给内网中的代理机开启白名单,而你的应用使用它作为代理去访问三方接口。
三、完整代码示例
我这里接入三方接口的是:https://www.tianapi.com/apiview/68
温馨提示:天行API的接口文档不可尽信,有的响应体结构对应不上,建议在开发时,可以先用JsonNode接收,然后看实际的响应结构是什么,再定义对象去接收。
3.1 定义FeignClient
package com.pine.client.core.tianxing;
import com.pine.client.beans.TianXingNetHotResponse;
import com.pine.client.config.TianXingRequestConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
/**
 * 天行feign client
 *
 * @author pine manage
 * @since 2024-11-22
 */
@FeignClient(name = "tianXing", url = "${rpc.tianxing.url}", configuration = TianXingRequestConfiguration.class)
public interface TianXingFeignClient {
    @PostMapping(value = "/nethot/index")
    TianXingNetHotResponse netHot();
}
3.2 定义拦截器
package com.pine.client.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
 * 天行 feign请求拦截器
 *
 * @author pine manage
 * @since 2024-11-22
 */
public class TianXingRequestInterceptor implements RequestInterceptor {
    private final String key;
    public TianXingRequestInterceptor(String key) {
        this.key = key;
    }
    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 请求头增加参数 content-type
        requestTemplate.header("Content-Type", "application/x-www-form-urlencoded");
        // 请求参数增加key
        requestTemplate.query("key", key);
    }
}
3.3 配置类
package com.pine.client.config;
import com.pine.client.interceptor.TianXingRequestInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * 天行接口请求配置
 *
 * @author pine manage
 * @since 2024-11-22
 */
@Configuration
public class TianXingRequestConfiguration {
    @Value("${rpc.tianxing.key}")
    private String tianXingKey;
    @Bean
    public TianXingRequestInterceptor tianXingRequestInterceptor() {
        return new TianXingRequestInterceptor(tianXingKey);
    }
}
3.4 okhttp配置
package com.pine.client.config;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import java.io.IOException;
/**
 * okhttp配置
 *
 * @author pine manage
 * @since 2024-11-22
 */
@Slf4j
@Configuration
public class OkHttpConfig {
    @Bean
    public okhttp3.OkHttpClient.Builder okHttpClientBuilder() {
        return new okhttp3.OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor());
    }
    /**
     * okhttp3 请求日志拦截器
     */
    static class LoggingInterceptor implements Interceptor {
        
        @NonNull
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            long start = System.nanoTime();
            log.info(String.format("Sending request %s on %s%n%s",
                    request.url(), chain.connection(), request.headers()));
            Response response = chain.proceed(request);
            long end = System.nanoTime();
            log.info(String.format("Received response for %s in %.1fms%n%s",
                    response.request().url(), (end - start) / 1e6d, response.headers()));
            return response;
        }
    }
}
3.5 响应体
3.5.1 天行基础响应
package com.pine.client.beans;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
 * 天行基础响应类
 *
 * @author pine manage
 * @since 2024-11-22
 */
@Data
public class TianXingBaseResponse implements Serializable {
    @Serial
    private static final long serialVersionUID = 4154999614348985895L;
    private Long code;
    private String msg;
}
3.5.2 热点新闻响应
package com.pine.client.beans;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
 * 天行热点新闻查询响应
 *
 * @author pine manage
 * @since 2024-11-22
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class TianXingNetHotResponse extends TianXingBaseResponse implements Serializable {
    @Serial
    private static final long serialVersionUID = 52499588383169858L;
    private List<Body> newslist;
    private String tip;
    @Data
    public static class Body implements Serializable {
        @Serial
        private static final long serialVersionUID = -1264805102673130063L;
        private String brief;
        private String index;
        private String keyword;
        private String trend;
    }
}
3.6 代理类
注意:代理的使用,我这里没做测试,如果你的应用场景涉及到了,建议先测试下。在使用代理的时候,@FeignClient 注解中的 url参数就要去掉,使用代理的proxiedUrl传入。
3.6.1 代理工厂
package com.pine.client.proxy;
import java.net.InetSocketAddress;
import java.net.Proxy;
/**
 * 代理工厂
 *
 * @author pine manage
 * @since 2024-11-22
 */
public class ProxyFactory {
    public static Proxy newHttpProxy(String hostname, int port) {
        return new Proxy(Proxy.Type.HTTP,
                new InetSocketAddress(hostname, port));
    }
    public static Proxy newDirectProxy(String hostname, int port) {
        return new Proxy(Proxy.Type.DIRECT,
                new InetSocketAddress(hostname, port));
    }
    public static Proxy newSocksProxy(String hostname, int port) {
        return new Proxy(Proxy.Type.SOCKS,
                new InetSocketAddress(hostname, port));
    }
}
3.6.2 代理客户端
package com.pine.client.proxy;
import feign.Client;
import feign.Request;
import feign.Response;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.Proxy;
/**
 * 代理客户端
 *
 * @author pine manage
 * @since 2024-11-22
 */
public class ProxyClient extends Client.Proxied {
    public ProxyClient(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier, Proxy proxy) {
        super(sslContextFactory, hostnameVerifier, proxy);
    }
    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        return super.execute(request, options);
    }
}
3.6.3 FeignClient的建造器
package com.pine.client.proxy;
import feign.Feign;
import feign.Request;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.springframework.cloud.openfeign.DefaultFeignLoggerFactory;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.net.Proxy;
import java.time.Duration;
import java.util.Optional;
/**
 * FeignClient建造器
 *
 * @author pine manage
 * @since 2024-11-22
 */
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class FeignClientBuilder {
    private static final Long DEFAULT_CONNECT_TIMEOUT = 5L;
    private static final Long DEFAULT_READ_TIMEOUT = 10L;
    private static final boolean DEFAULT_FOLLOW_REDIRECTS = true;
    @Nullable
    private Encoder encoder;
    @Nullable
    private Decoder decoder;
    @NonNull
    private Proxy proxy;
    @Nullable
    private SSLSocketFactory sslContextFactory;
    @Nullable
    private HostnameVerifier hostnameVerifier;
    @Nullable
    private Retryer retryer;
    @Nullable
    private Long connectTimeout;
    @Nullable
    private Long readTimeout;
    @Nullable
    private Boolean followRedirects;
    public <T> T build(Class<T> clazz, String proxiedUrl) {
        this.connectTimeout = Optional.ofNullable(this.connectTimeout).orElse(DEFAULT_CONNECT_TIMEOUT);
        this.readTimeout = Optional.ofNullable(this.readTimeout).orElse(DEFAULT_READ_TIMEOUT);
        this.followRedirects = Optional.ofNullable(this.followRedirects).orElse(DEFAULT_FOLLOW_REDIRECTS);
        this.retryer = Optional.ofNullable(retryer).orElse(Retryer.NEVER_RETRY);
        this.encoder = Optional.ofNullable(encoder).orElse(new Encoder.Default());
        this.decoder = Optional.ofNullable(decoder).orElse(new Decoder.Default());
        Request.Options options = new Request.Options(Duration.ofSeconds(connectTimeout), Duration.ofSeconds(readTimeout), followRedirects);
        ProxyClient proxyClient = new ProxyClient(sslContextFactory, hostnameVerifier, proxy);
        return Feign.builder().client(proxyClient)
                .retryer(this.retryer)
                .options(options)
                .encoder(encoder)
                .decoder(decoder)
                .logger(new DefaultFeignLoggerFactory(null).create(clazz))
                .contract(new SpringMvcContract())
                .target(clazz, proxiedUrl);
    }
//    public static void main(String[] args) {
//        com.pine.client.core.tianxing.TianXingFeignClient tianXingFeignClient = new FeignClientBuilder()
//                .setProxy(ProxyFactory.newHttpProxy("代理地址", 1003))
//                .build(com.pine.client.core.tianxing.TianXingFeignClient.class, "localhost");
//    }
}
四、调用&测试
4.1 配置信息
spring:
  cloud:
    openfeign:
      # 启用okhttp配置
      okhttp:
        enabled: true
    loadbalancer:
      # 关闭负载重试
      retry:
        enabled: false
rpc:
  tianxing:
    key: 你自己申请的天行key
    url: http://api.tianapi.com
4.2 测试代码
在controller中添加代码:
@Resource
private TianXingFeignClient tianXingFeignClient;
@PostMapping("/test")
public ResultVo<TianXingNetHotResponse> test() {
    TianXingNetHotResponse netHotResponse = tianXingFeignClient.netHot();
    return ResultVo.success(netHotResponse);
}
4.3 调用结果
控制台输出了okhttp的拦截内容:
2024-11-22 16:04:35.177  INFO  82746 --- [http-nio-8080-exec-1] com.pine.client.config.OkHttpConfig$LoggingInterceptor.intercept(OkHttpConfig.java:39) : [logId=default] Sending request http://api.tianapi.com/nethot/index?key=你自己申请的天行key on null
Accept: */*
2024-11-22 16:04:35.477  INFO  82746 --- [http-nio-8080-exec-1] com.pine.client.config.OkHttpConfig$LoggingInterceptor.intercept(OkHttpConfig.java:43) : [logId=default] Received response for http://api.tianapi.com/nethot/index?key=你自己申请的天行key in 298.1ms
Server: nginx
Date: Fri, 22 Nov 2024 08:04:35 GMT
Content-Type: application/json;charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: *
Access-Control-Max-Age: 604800
Strict-Transport-Security: max-age=31536000
接口响应结果:省略(自行调用即可)
附录
附1:本系列文章链接
 SpringCloud系列文章目录(总纲篇)


















