参考文档:
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";
    }
}

启动项目进行测试

spring boot查询接口开发 spring boot 接口调用_feign


可以看到最基本的springboot框架已经搭建完成! 可以开始进行下一步操作

此时的项目结构如下图,非常简洁:

spring boot查询接口开发 spring boot 接口调用_spring boot查询接口开发_02

二. 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接口测试

返回结果符合预期

spring boot查询接口开发 spring boot 接口调用_feign_03

2.3.2 postman调用post接口测试

可以看到,接口调用成功

spring boot查询接口开发 spring boot 接口调用_feign_04

2.4 最终项目结构

以上就是一个可以通过feign调用远程接口的一个基础项目的实例代码,项目最终架构如下图所示:

spring boot查询接口开发 spring boot 接口调用_spring boot查询接口开发_05

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