SpringBoot项目中使用feign调用远程http接口(超详细文档)

参考文档:
Feign远程调用原理

在实际生产需要中,经常会遇到调用远程http接口的场景.
举例: 比如我的Springboot项目会调用另一个Springboot项目的接口, 或者调用一些第三方服务的Restful api.
采用常规的方案,需要配置请求head、body,然后才能发起请求。获得响应体后,还需解析等操作,十分繁琐。

Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。
Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。

Feign的工作原理,可以参考下面这篇文档:
Feign远程调用原理

本篇文章就以实际案例来说明一下:SpringBoot项目中是如何使用feign调用远程http接口的.
文章中对feign调用做了一个封装. 基于封装类, 可以通过最小化的代码开发实现各种场景的接口调用

码字不易,转载请标注出处!

一. 创建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调用的一些封装

主要包括:

  1. 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调用远程接口的一个基础项目的实例代码,项目最终架构如下图所示:

在这里插入图片描述

怎么样,你学会了没?赶紧动手体验一番吧

### 回答1: Spring Boot Feign是一种用于远程调用的工具,它可以帮助开发者轻松地实现微服务之间的调用。通过Feign,开发者可以定义一个接口,然后在该接口上添加注解来指定调用的服务和方法。Feign会自动生成一个代理对象,开发者可以直接调用该代理对象的方法来实现远程调用Feign还提供了负载均衡和断路器等功能,可以帮助开发者更好地管理微服务之间的调用。 ### 回答2: SpringBoot Feign是一个基于HTTP请求的RESTful API客户端,它使得我们可以轻松地调用远程HTTP服务,而无需编写复杂的代码和配置。它通过在接口层面声明HTTP请求的方式,将HTTP请求以类和方法的形式组织,并且支持负载均衡、服务发现和请求过滤功能。 在使用SpringBoot Feign进行远程调用时,需要依赖Feign的starter和Eureka的starter。在启动类上使用@EnableFeignClients注解启用Feign客户端,并在需要使用接口上添加@FeignClient注解,同时可以在注解指定要访问的远程服务的名称,即Spring Cloud注册到Eureka注册心的服务名。 Feign客户端通过动态代理将接口的请求映射到目标URI,并发送对应的HTTP请求,接收响应并返回。请求的参数和返回的内容均可以使用注解进行控制。此外,Feign还支持多种请求方式(GET、POST、DELETE、PUT等),以及请求头、请求体的自定义。 对于负载均衡,Feign默认集成了Ribbon实现客户端负载均衡。通过在注解指定服务名,Feign会自动调用Ribbon进行负载均衡处理,以便访问多个提供同一服务的实例机器。对于服务发现,Feign同样与Eureka无缝集成,可以通过注解指定服务名来自动完成服务发现操作。 在调用远程服务的过程Feign还支持请求的重试机制和请求过滤器,可以通过实现RequestInterceptor接口来定义请求拦截器,实现Fallback接口来定义请求失败后的处理方法。 总之,SpringBoot Feign使得我们可以以非常简单和便捷的方式调用远程HTTP服务,并且在实现服务注册、发现、负载均衡等方面无需太多配置和复杂的代码,极大地提高了系统的性能和扩展性。 ### 回答3: Spring Boot Feign是一个基于HTTP的RESTful API客户端,可以在Spring应用使用。 它可以帮助开发人员从应用程序轻松地调用远程服务。 Spring Boot Feign的主要功能包括: 1. 通过在接口使用注解,使用简单易用的声明式RESTAPI调用。 2. 提供了非常简单的编程方式,而无需手动处理HTTP请求。 3. 支持Apache HttpClient和OkHttp等多个HTTP客户端。 4. 可以基于已有的Ribbon负载均衡器,实现远程服务的自动负载均衡。 5. 支持请求和响应的压缩。 6. 支持异步调用使用Spring Boot Feign进行远程调用步骤如下: 1. 添加Spring Boot Feign依赖。 2. 定义Feign客户端接口,该接口包含声明要访问的远程服务的所有方法。 3. 添加Feign客户端接口的注释,以标识所需的远程服务。 4. 创建Feign客户端。 5. 使用Feign客户端来调用定义的方法,以访问远程服务。 作为一个开发人员,需要对Feign客户端接口进行声明,以定义将要访问的远程服务。通过使用注释,我们可以声明需要访问哪个服务和使用哪个URL。Spring Boot Feign可以自动处理HTTP请求,从而允许简单和干净的代码,而无需手动处理HTTP头/正文。使用Feign客户端接口定义的方法,可以像本地方法一样使用,但实际上它们将向远程服务器发出请求。 使用Spring Boot Feign进行远程调用的好处是: 1. 代码简单,易于理解。 2. 降低了与远程服务交互的复杂性。 3. 可以节省调用远程服务的成本和时间。 4. 帮助开发人员更快地进行开发和测试。 总之,Spring Boot Feign是一种非常强大的框架,可以帮助开发人员更轻松地进行RESTful API的开发和调用
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值