FeignSpring Coud

Spring Cloud Feign组件

2019-11-19  本文已影响0人  小波同学

概述

Feign 是Netflix开源的一个声明式的Http 客户端,它的目的就是让Web Service基于Http的远程调用变得更加简单。
Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign 默认集成了 Ribbon,Nacos 也很好的兼容了 Feign,默认实现了负载均衡的效果。

快速入门

1、引入maven依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.10.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</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>
    </dependency>
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <!--整合Spring Cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2、通过@EnableFeignClients注解开启Feign功能

@SpringBootApplication
@EnableFeignClients
public class OrderApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApiApplication.class, args);
    }
}

3、创建Feign接口,通过 @FeignClient("服务名") 注解来指定调用哪个服务

@FeignClient(value="price-center")
public interface PriceCenterFeignClient {

    @GetMapping(value = "/prices/{id}")
    PriceInfo getPrice(@PathVariable("id") Integer id);
}

Feign的组成

接口 作用 默认值
Feign.Builder Feign的入口 Feign.Builder
Client Feign底层用什么去请求 和Ribbon配合时:LoadBalancerFeignClient
不和Ribbon配合时:Feign.Client.Default
Contract 契约,注解支持 SpringMvcContract
Encoder 编码器,用于将对象转换成HTTP请求消息体 SpringEncoder
Decoder 解码器,将响应消息体转换成对象 ResponseEntityDecoder
Logger 日志管理器 Slf4jLogger
RequestInterceptor 用于为每个请求添加通用逻辑 需要手动实现

Feign的配置自定义

细粒度自定义Feign的日志级别配置

Feign的日志级别 打印内容
NONE(默认值) 不记录任何日志
BASIC 仅记录请求方法、URL、响应状态码以及执行时间(适用于线上)
HEADERS 在BASIC的基础上,记录请求和响应的header
FULL 记录请求和响应的header、body和元数据

1、Java代码方式

@FeignClient(value="price-center",configuration = PriceCenterFeignConfiguration.class)
public interface PriceCenterFeignClient {

    @GetMapping(value = "/prices/{id}")
    PriceInfo getPrice(@PathVariable("id") Integer id);
}
/**
 * 通过java代码,细粒度的方式(指定user-center)配置Feign的日志级别
 *
 * 这个类别加@Configuration注解了,否则必须挪到@ComponentScan能扫描到的包以外
 *
 */

public class PriceCenterFeignConfiguration{

    @Bean
    public Logger.Level level(){
        //让Feign打印所有请求的细节
        return Logger.Level.FULL;
    }
}
logging:
  level:
    #配置Feign的日志级别
    com.yibo.contentcenter.configuration.PriceCenterFeignConfiguration: debug

即Feign的日志级别是建立在上面配置的debug基础之上的,如果上面改为info,那么则不会输出任何日志

2、配置属性方式
首先去掉@FeignClient注解上的configuration属性

#细粒度的配置Feign的日志级别,这种是通过配置文件的方式
feign:
  client:
    config:
      #想要调用的微服务的名称
      price-center:
        loggerLevel: full

全局自定义Feign的日志级别配置

Java代码方式

@SpringBootApplication
@EnableFeignClients(defaultConfiguration = GlobalFeignConfiguration.class)
public class ContentCenterApplication {

    public static void main(String[] args) {

        SpringApplication.run(ContentCenterApplication.class, args);
    }
}

public class GlobalFeignConfiguration{

    @Bean
    public Logger.Level level(){
        //让Feign打印所有请求的细节
        return Logger.Level.FULL;
    }
}

配置属性方式
首先去掉@EnableFeignClients中的defaultConfiguration属性

feign:
  client:
    config:
      #全局配置
      default:
        loggerLevel: full

Feign支持的配置项

代码方式支持的配置项

配置项 作用
Feign.Builder Feign的入口
Client Feign底层用什么去请求
Contract 契约,注解支持
Encocer 编码器,用于将对象转换成HTTP请求消息体
Decoder 解码器,将相应消息体转换成对象
Logger 日志管理
LoggerLevel 指定日志级别
Retryer 指定重试策略
ErrorDecoder 指定错误解码器
Request.Options 超时时间
Collection<RequestInterceptor> 拦截器
SetterFactory 用于设置Hystrix的配置属性,Feign整合Hystrix才会用

属性方式支持的配置项

feign.client.config:
        <feignName>:
            connectTimeout:5000     #连接超时时间
            readTimeout:5000     #读取超时时间
            loggerLevel:full        #日志级别
            errorDecoder:com.example.SimpleErrorDecoder    #错误解码器
            retryer:com.example.SimpleRetryer        #重试策略
            requestInterceptors:
                - com.example.FooRequestInterceptor  #拦截器
            # 是否对404错误码解码
            # 处理逻辑详见feign.SynchronousMethodHandler#executeAndDecode
            decode404:false
            encoder:com.example.SimpleEncoder  # 编码器
            decoder:com.example.SimpleDecoder  # 解码器
            contract:com.example.SimpleContract  # 契约

Feign配置代码方式 VS 属性方式
Feign配置优先级:全局代码<全局属性<细粒度代码<细粒度属性

image.png

Feign的继承特性

Spring Cloud中Feign的继承特性:https://blog.csdn.net/u012702547/article/details/78261306

创建一个基础的Maven工程,定义Controller接口写好SpringMvc注解,由服务提供方和服务消费方通过引入基础Maven工程,分别继承Controller接口,Feign继承特性方式用起来确实很方面,但是也带来一个问题,就是服务提供者和服务消费者的耦合度太高,此时如果服务提供者修改了一个接口的定义,服务消费者可能也得跟着变化,进而带来很多未知的工作量,因此小伙伴们在使用继承特性的时候,要慎重考虑。

Feign脱离Ribbon的使用

@FeignClient(name="baidu",url= "http://www.baidu.com")
public interface BaiduFeignClient {

    @GetMapping("")
    public string index();
}

Feign调用https接口的话需要绕过SSL验证

Feign- 绕过SSL验证的方案

方案一——改写LoadBalancerFeignClient

import feign.Client;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;

/**
 * @Author: huangyibo
 * @Date: 2022/8/3 19:36
 * @Description: feign client配置
 */

public class FeignConfiguration {

    @Bean
    public CachingSpringLoadBalancerFactory cachingFactory(SpringClientFactory clientFactory) {
        return new CachingSpringLoadBalancerFactory(clientFactory);
    }

    @Bean
    @ConditionalOnMissingBean
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                              SpringClientFactory clientFactory) throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext ctx = SSLContext.getInstance("TLSv1.2");
        X509TrustManager tm = new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) {
            }
            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) {
            }
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
        };
        ctx.init(null, new TrustManager[]{tm}, null);
        return new LoadBalancerFeignClient(new Client.Default(ctx.getSocketFactory(),
                (hostname, session) -> true),
                cachingFactory, clientFactory);
    }

}
@FeignClient(name = "OrderFeign", url= "https://yibo.com/order-center/openapi", configuration = FeignConfiguration.class)
public interface OrderFeign {

    @PostMapping("/order")
    ResultBody<List<HouseDO>> queryHouseList(OrderQuery query);
}

方案二——改写RestTemplate

@Bean
public RestTemplate getRestTemplate() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
    SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
        @Override
        public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
            return true;
        }
    }).build();

    SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext,
            new String[]{"TLSv1.2"},
            null,
            NoopHostnameVerifier.INSTANCE);

    CloseableHttpClient httpClient = HttpClients.custom()
            .setSSLSocketFactory(csf)
            .build();

    HttpComponentsClientHttpRequestFactory requestFactory =
            new HttpComponentsClientHttpRequestFactory();

    requestFactory.setHttpClient(httpClient);
    return new RestTemplate(requestFactory);
}
@FeignClient(name = "OrderFeign", url= "https://yibo.com/order-center/openapi")
public interface OrderFeign {

    @PostMapping("/order")
    ResultBody<List<HouseDO>> queryHouseList(OrderQuery query);
}

RestTemplate VS Feign

尽量使用Feign,尽量杜绝使用RestTemplate

角度 RestTemplate Feign
可读性、可维护性 一般 极佳
开发体验 欠佳 极佳
性能 很好 中等(RestTemplate性能的50%左右)
灵活性 极佳 中等(内置功能可满足绝大多数需求)

Open Feign数据压缩功能

feign:
    compression:
        request:
            enabled: true   # 开启请求的数据压缩
            mime-types: text/xml,application/xml,application/json # 配置压缩支持的MIME TYPE
            min-request-size: 1024  # 配置压缩数据大小的下限,当传输的数据类型大于 1024 时,才会进行压缩
        response:
            enabled: true # 配置响应GZIP压缩

Feign的性能优化

1、为Feign配置连接池,性能提升15%

Feign通过jdk中的HttpURLConnection向下游服务发起http请求(源码详见feign.Client)

public Response execute(Request request, Options options) throws IOException {
    HttpURLConnection connection = this.convertAndSend(request, options);
    return this.convertResponse(connection, request);
}

HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
    HttpURLConnection connection = (HttpURLConnection)(new URL(request.url())).openConnection();
    if (connection instanceof HttpsURLConnection) {
        HttpsURLConnection sslCon = (HttpsURLConnection)connection;
        if (this.sslContextFactory != null) {
            sslCon.setSSLSocketFactory(this.sslContextFactory);
        }

        if (this.hostnameVerifier != null) {
            sslCon.setHostnameVerifier(this.hostnameVerifier);
        }
    }
    ......
}

得出结论:缺乏连接池的支持,在达到一定流量的后服务肯定会出问题,可以用httpclient和okhttp替换掉jdk原生的HttpURLConnection,httpclient和okhttp都是支持连接池的

httpclient替换掉Feign原生的HttpURLConnection

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
feign:
    httpclient:
        #让feign使用apache httpclient做请求,而不是默认的HttpURLConnection
        enabled: true
        #feign的最大连接数
        max-connections: 200
        #feign单个路径的最大连接数127.0.0.1:9876
        max-connections-per-route: 50

okhttp替换掉Feign原生的HttpURLConnection

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>10.1.0</version>
</dependency>
feign:
  client:
    config:
      default: # 服务名,填写 default 为所有服务,或者指定某服务
        connectTimeout: 10000 # 连接超时,10秒
        readTimeout: 20000 # 读取超时,20秒
  httpclient:
    enabled: false # 关闭 ApacheHttpClient
    max-connections: 200 # 连接池连接最大连接数
    max-connections-per-route: 50 # feign单个路径的最大连接数127.0.0.1:9876
    time-to-live: 600 # 连接最大闲置时间,单位为秒,600秒==10分钟(缺省值为 900秒==15分钟)
  okhttp:
    enabled: true # 开启 okhttp
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {

    @Autowired
    OkHttpLoggingInterceptor okHttpLoggingInterceptor;

    @Bean
    public okhttp3.OkHttpClient okHttpClient(){
        return new okhttp3.OkHttpClient.Builder()
            //读取超时时间
            .readTimeout(60, TimeUnit.SECONDS)
            //连接超时时间
            .connectTimeout(60, TimeUnit.SECONDS)
            //写超时时间
            .writeTimeout(120, TimeUnit.SECONDS) 
            //设置连接池
            .connectionPool(new ConnectionPool())
            // .addInterceptor();
            .build();
    }
}

2、为Feign设置合理的日志级别

Feign传递Token

利用@RequestHeader,强烈不建议使用这种方式

利用RequestInterceptor实现Token传递,推荐使用

1、新建TokenFeignClientInterceptor实现RequestInterceptor重写apply()

/**
 * 发送FeignClient设置Header信息
 *
 * 微服务之间通过Feign调用,通过拦截器在feign请求之前,把当前服务的token添加到目标服务的请求头里
 */

@Component
public class TokenFeignClientInterceptor implements RequestInterceptor {

    /**
     * token放在请求头
     * @param requestTemplate
     */
    @Override
    public void apply(RequestTemplate requestTemplate) {
        //1、从header里面获取token
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        if (requestAttributes != null) {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            String token = request.getHeader("Authorization");
            //2、将token传递
            if(!StringUtils.isEmpty(token)){
                requestTemplate.header("Authorization",token);
            }
        }
    }
}

2、配置TokenFeignClientInterceptor

@FeignClient(value="priceServer",configuration = TokenFeignClientInterceptor.class)
public interface PriceFeignClient {

    @GetMapping(value = "/prices/{id}")
    PriceInfo getPrice(@PathVariable("id") Integer id);
}
feign:
  client:
    config:
      default:
        requestInterceptors:
          - com.yibo.orderapi.feignclient.TokenFeignClientInterceptor

RestTemplate传递Token

1、exchange()

/**
 * 此方法为演示restTemplate传递token
 * @param id
 * @param request
 * @return
 */
@GetMapping("/tokenRelay/{id}")
public ResponseEntity<UserDTO> tokenRelay(@PathVariable("id") Integer id, HttpServletRequest request){
    String token = request.getHeader("Authorization");
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.add("Authorization",token);
    return restTemplate
            .exchange("http://user-center/users/{id}",
                    HttpMethod.GET,
                    new HttpEntity<>(httpHeaders),
                    UserDTO.class,
                    id);
}

2、ClientHttpRequestInterceptor

/**
 * @Description: 通过此拦截器给RestTemplate请求添加Header传递Token
 */
public class TestRestTemplateTokenRelayInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
        //1、从header里面获取token
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        String token = request.getHeader("Authorization");
        HttpHeaders headers = httpRequest.getHeaders();
        headers.add("Authorization",token);

        //保证RestTemplate请求继续执行
        return clientHttpRequestExecution.execute(httpRequest,bytes);
    }
}

@Bean
@LoadBalanced
@SentinelRestTemplate //RestTemplate整合Sentinel
public RestTemplate restTemplate(){
    RestTemplate restTemplate = new RestTemplate();
    //通过给RestTemplate添加拦截器,达到通过RestTemplate传递Header
    restTemplate.setInterceptors(Collections.singletonList(new TestRestTemplateTokenRelayInterceptor()));
    return restTemplate;
}

3、使用OAuth2RestTemplate

//此参数SpringBoot已经声明好了只需要注入即可用
@Autowired
private OAuth2ProtectedResourceDetails resource;

//此参数SpringBoot已经声明好了只需要注入即可用
@Autowired
private OAuth2ClientContext context;

//OAuth2RestTemplate在发送请求的时候,在Http请求头里面会自动放入收到的token
@Bean
@LoadBalanced
public OAuth2RestTemplate oAuth2RestTemplate(){

    return new OAuth2RestTemplate(resource,context);
}
RestTemplate整合HttpClient

引入maven依赖:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>
SpringBoot框架RestTemplate+Httpclient的代码配置:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class HttpClientRestConfig {
    @Bean
    public ClientHttpRequestFactory clientHttpRequestFactory() {
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        clientHttpRequestFactory.setHttpClient(HttpsClientPoolThread.builder().createSSLClientDefault());
        //这里是使用了自定义的一个HttpsClientPoolThread线程池单例 以后有机会会单独写文章展示其配置内容, 大家可以先使用默认的HttpClients.createDefault()进行配置,或自定义线程池;
        clientHttpRequestFactory.setConnectTimeout(10000);
        clientHttpRequestFactory.setReadTimeout(10000);
        clientHttpRequestFactory.setConnectionRequestTimeout(200);
        return clientHttpRequestFactory;
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(clientHttpRequestFactory());
    }
}

在这里 要说明下:

Spring框架的RestTemplate+Httpclient的代码配置:
@Configuration
public class RestTemplateConfig {
    private Logger log = Logger.getLogger(this.getClass());
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setRequestFactory(clientHttpRequestFactory());
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
        return restTemplate;
    }
    @Bean
    public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
        try {
            HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                    return true;
                }
            }).build();
            httpClientBuilder.setSSLContext(sslContext);
            HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslConnectionSocketFactory).build();// 注册http和https请求
            PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);// 开始设置连接池
            poolingHttpClientConnectionManager.setMaxTotal(200); // 最大连接数200
            poolingHttpClientConnectionManager.setDefaultMaxPerRoute(20); // 同路由并发数20
            httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
            httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true));// 重试次数
            HttpClient httpClient = httpClientBuilder.build();
            HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);// httpClient连接配置
            clientHttpRequestFactory.setConnectTimeout(20000);// 连接超时
            clientHttpRequestFactory.setReadTimeout(20000);// 数据读取超时时间
            clientHttpRequestFactory.setConnectionRequestTimeout(200);// 连接不够用的等待时间
            return clientHttpRequestFactory;
        } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
            log.error(e.getMessage());
        }
        return null;
    }
}

拓展:

OkHttp设计原理

深入理解Feign之源码解析

Feign常见问题总结

如何使用Feign构造多参数的请求

参考:

spring cloud feign使用okhttp3

https://www.cnblogs.com/zhangbing0615/articles/9238311.html

上一篇下一篇

猜你喜欢

热点阅读