参考文档:
Feign远程调用原理
在实际生产需要中,经常会遇到调用远程http接口的场景.
举例: 比如我的Springboot项目会调用另一个Springboot项目的接口, 或者调用一些第三方服务的Restful api.
采用常规的方案,需要配置请求head、body,然后才能发起请求。获得响应体后,还需解析等操作,十分繁琐。
Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。
Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。
Feign的工作原理,可以参考下面这篇文档:
Feign远程调用原理
本篇文章就以实际案例来说明一下:SpringBoot项目中是如何使用feign调用远程http接口的.
文章中对feign调用做了一个封装. 基于封装类, 可以通过最小化的代码开发实现各种场景的接口调用
码字不易,转载请标注出处!
文章目录
- 一. 创建springboot项目
- 二. springboot项目中引入feign
- 2.1 对于feign调用的一些封装
- 2.1.1 封装抽象类用于配置基本http请求.
- 2.2 正式进行feign调用
- 2.2.1 具有替换目标URL请求功能的实现类
- 2.2.2 编写Feign调用的配置类
- 2.2.3 开发带有FeignClient注解的类
- 2.3 接口调用测试
- 2.3.1 postman调用get接口测试
- 2.3.2 postman调用post接口测试
- 2.4 最终项目结构
一. 创建springboot项目
pom.xml导入基本依赖:
这里说明下: 引入fastjson是因为接口的返回数据通常是json格式.
引入lombok是为了少些几行代码.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.4.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>HdrHistogram</artifactId>
<groupId>org.hdrhistogram</groupId>
</exclusion>
<exclusion>
<artifactId>bcprov-jdk15on</artifactId>
<groupId>org.bouncycastle</groupId>
</exclusion>
<exclusion>
<artifactId>jsr305</artifactId>
<groupId>com.google.code.findbugs</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>9.7.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>9.7.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>9.7.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
<version>9.7.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
application.yml
配置
spring:
application:
name: feign
server:
port: 8084
feign:
url: http://localhost:8083 #远程调用接口的url
启动类代码
, 加入EnableFeignClients开启Feign注解,使Feign的bean可以被注入
@SpringBootApplication
@EnableFeignClients
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
controller层测试代码:
@RestController
@RequestMapping("/feign")
public class FeignController {
@GetMapping
public String feignTest() {
return "Hello feign";
}
}
启动项目进行测试
可以看到最基本的springboot框架已经搭建完成! 可以开始进行下一步操作
此时的项目结构如下图,非常简洁:
二. springboot项目中引入feign
2.1 对于feign调用的一些封装
主要包括:
- AbstractClient类对Feign的Client类进行进一步封装,设置并获取HttpURLConnection连接, 它的子类只需实现具体替换目标URL请求的功能即可
至于为什么需要一个具体的实现类来替换目标URL,原因如下:
在FeignClient注解里,需要指定远程url.
在实际项目里, 远程url的路径可能不唯一(比如说调用多个微服务的接口)
并且,url信息不会写死在代码里.通常是通过配置文件进行配置或者保存在数据库中.
这时就需要对url进行转换,保证最终Feign调用时能够访问到正确的链接
@FeignClient(value = "feign", url = "{url}", configuration = FeignConfiguration.class)
2.1.1 封装抽象类用于配置基本http请求.
代码如下:
package com.pers.xmr.http.client;
import feign.Client;
import feign.Request;
import feign.Response;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import static feign.Util.*;
import static feign.Util.CONTENT_LENGTH;
import static java.lang.String.format;
/**
* @author xmr
* @date 2022/4/16 11:01
* @description
*/
public abstract class AbstractClient implements Client {
private final SSLSocketFactory sslContextFactory;
private final HostnameVerifier hostnameVerifier;
public AbstractClient() {
this.sslContextFactory = null;
this.hostnameVerifier = null;
}
public AbstractClient(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
this.sslContextFactory = sslContextFactory;
this.hostnameVerifier = hostnameVerifier;
}
/**
* 功能:设置并获取HttpURLConnection连接
*
* @param request HTTP 请求
* @param options HTTP 请求可选项参数
* @return HttpURLConnection 连接
* @throws IOException 异常对象
*/
HttpURLConnection convertAndSend(Request request, Request.Options options) throws IOException {
final HttpURLConnection
connection = convertAndGetNewHttpURLConnection(request);
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
if (sslContextFactory != null) {
sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
connection.setInstanceFollowRedirects(options.isFollowRedirects());
connection.setRequestMethod(request.method());
Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
boolean
gzipEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
boolean
deflateEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);
boolean hasAcceptHeader = false;
Integer contentLength = null;
for (String field : request.headers().keySet()) {
if ("Accept".equalsIgnoreCase(field)) {
hasAcceptHeader = true;
}
for (String value : request.headers().get(field)) {
if (field.equals(CONTENT_LENGTH)) {
if (!gzipEncodedRequest && !deflateEncodedRequest) {
contentLength = Integer.valueOf(value);
connection.addRequestProperty(field, value);
}
} else {
connection.addRequestProperty(field, value);
}
}
}
// Some servers choke on the default accept string.
if (!hasAcceptHeader) {
connection.addRequestProperty("Accept", "*/*");
}
if (request.body() != null) {
if (contentLength != null) {
connection.setFixedLengthStreamingMode(contentLength);
} else {
connection.setChunkedStreamingMode(8196);
}
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
if (gzipEncodedRequest) {
out = new GZIPOutputStream(out);
} else if (deflateEncodedRequest) {
out = new DeflaterOutputStream(out);
}
try {
out.write(request.body());
} finally {
try {
out.close();
} catch (IOException e) { // NOPMD
System.out.println("Error happened. " + e.getMessage());
}
}
}
return connection;
}
/**
* 功能:转换并获取HTTP响应消息体
*
* @param connection HTTP 连接
* @return 响应消息体
* @throws IOException 异常对象
*/
Response convertResponse(HttpURLConnection connection) throws IOException {
int status = connection.getResponseCode();
String reason = connection.getResponseMessage();
if (status < 0) {
throw new IOException(format("Invalid status(%s) executing %s %s", status,
connection.getRequestMethod(), connection.getURL()));
}
Map<String, Collection<String>> headers = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// response message
if (field.getKey() != null) {
headers.put(field.getKey(), field.getValue());
}
}
Integer length = connection.getContentLength();
if (length == -1) {
length = null;
}
InputStream stream;
if (status >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
return Response.builder()
.status(status)
.reason(reason)
.headers(headers)
.body(stream, length)
.build();
}
/**
* 功能: 拦截原始HTTP请求,替换为目标HTTP请求后获取目标HTTP请求的URL连接
* 具体替换目标URL请求交由实现类完成
*
* @param request HTTP 请求
* @return HTTPURLConnection 连接
* @throws IOException 异常对象
*/
abstract HttpURLConnection convertAndGetNewHttpURLConnection(Request request) throws IOException;
}
2.2 正式进行feign调用
2.2.1 具有替换目标URL请求功能的实现类
这里加上Component注解是为了能够注入配置文件里面的配置
package com.pers.xmr.http.client;
import feign.Request;
import feign.Response;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author xmr
* @date 2022/4/16 11:28
* @description
*/
@Component
public class FeignClient extends AbstractClient {
private final static Pattern urlPattern = Pattern.compile("://\\{feignUrl}/([A-Za-z0-9_-]*)");
@Value(value = "${feign.url:}")
private String feignUrl;
@Override
public Response execute(Request request, Request.Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection).toBuilder().request(request).build();
}
@Override
HttpURLConnection convertAndGetNewHttpURLConnection(Request request) throws IOException {
String sourceUrl = request.url();
Matcher matcher = urlPattern.matcher(sourceUrl);
String targetUrl = sourceUrl;
boolean isFind = matcher.find();
if(isFind) {
String regex = "http://\\{feignUrl}";
targetUrl = sourceUrl.replaceAll(regex, feignUrl);
}
return (HttpURLConnection) new URL(targetUrl).openConnection();
}
}
2.2.2 编写Feign调用的配置类
package com.pers.xmr.http.configuration;
import com.pers.xmr.http.client.FeignClient;
import feign.Client;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author xmr
* @date 2022/4/16 11:40
* @description
*/
@Configuration
public class FeignConfiguration {
@Bean
public Client feignClient() {
return new FeignClient();
}
}
2.2.3 开发带有FeignClient注解的类
通过这里调用远端接口.
简单说明下我的两个远程接口.
一个Get请求接口,没做什么事,返回一个Json串.
一个Post请求接口,请求体是GitParamDO的对象从github拉取代码,拉取成功之后返回包含代码拉取的路径信息的json串
创建post接口需要的实体类(这里仅仅是通过该实体类说明该如何通过feign调用post接口而已)
package com.pers.xmr.http.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author xmr
* @date 2022/4/16 12:00
* @description
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GitParamDO {
private String repostoryUrl; // 镜像仓库路径
private String branch; // 代码分支
private String tag; // 代码标签名称
private String commitId; // 代码提交的commitId
}
创建FeignClient
注意事项:
本案例的post请求需要添加注解:
@Headers({"content-type:application/json"})
因为本例中post是以json形式传递请求体的
配置文件中配置的feign.url
+ @PostMapping和@GetMapping对应的value值
为远程接口实际的http地址,注意这里不要映射错误的地址
package com.pers.xmr.http.client;
import com.alibaba.fastjson.JSONObject;
import com.pers.xmr.http.configuration.FeignConfiguration;
import com.pers.xmr.http.model.GitParamDO;
import feign.Headers;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @author xmr
* @date 2022/4/16 12:06
* @description
*/
@FeignClient(value = "remote", url = "{feignUrl}", configuration = FeignConfiguration.class)
public interface RemoteClient {
/**
* 获取git下载路径
* @return git代码下载的本地路径
*/
@PostMapping(value = "/image/git")
@Headers({"content-type:application/json"})
JSONObject gitClone(@RequestBody GitParamDO GitParamDO
);
/**
* 获取git下载路径
* @return git代码下载的本地路径
*/
@GetMapping(value = "/image/git")
JSONObject getTest();
}
编写service层代码
package com.pers.xmr.serivce;
import com.alibaba.fastjson.JSONObject;
import com.pers.xmr.http.client.RemoteClient;
import com.pers.xmr.http.model.GitParamDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author xmr
* @date 2022/4/16 12:23
* @description
*/
@Service
public class FeignService {
@Autowired
RemoteClient remoteClient;
public JSONObject getTest() {
return remoteClient.getTest();
}
public JSONObject postTest(GitParamDO gitParamDO) {
return remoteClient.gitClone(gitParamDO);
}
}
在Controller层增加对以上两个接口的调用
package com.pers.xmr.controller;
import com.alibaba.fastjson.JSONObject;
import com.pers.xmr.http.client.RemoteClient;
import com.pers.xmr.http.model.GitParamDO;
import com.pers.xmr.serivce.FeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author xmr
* @date 2022/4/16 10:45
* @description
*/
@RestController
@RequestMapping("/feign")
public class FeignController {
@Autowired
FeignService feignService;
@GetMapping
public JSONObject getTest() {
return feignService.getTest();
}
@PostMapping
public JSONObject postTest(GitParamDO gitParamDO) {
return feignService.postTest(gitParamDO);
}
}
2.3 接口调用测试
2.3.1 postman调用get接口测试
返回结果符合预期
2.3.2 postman调用post接口测试
可以看到,接口调用成功
2.4 最终项目结构
以上就是一个可以通过feign调用远程接口的一个基础项目的实例代码,项目最终架构如下图所示:
怎么样,你学会了没?赶紧动手体验一番吧