文章目录
- 前言
- 正文
- 一、项目环境
- 二、项目结构
- 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系列文章目录(总纲篇)