Spring Cloud优质项目,Feign的使用扩展

1 前言

SpringCloud是以Restful为基础实现的开发框架,在整体调用过程中,即使引入了Eureka,也需要消费端使用完整的路径才可以正常访问远程接口,同时还需要开发者手动利用RestTemplate进行调用与返回结果的转换。为了解决这种复杂的调用逻辑,在SpringCloud中提供Feign技术(依赖于Ribbon技术支持),利用此技术可以将远程的Restful服务映射为远程接口,消费端可通过远程接口实现远程方法调用。

1.1 什么是Feign

Feign英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。

1.2 什么是OpenFeign

OpenFeign是一种声明式调用,我们只需要按照一定的规则描述我们的接口,它就能帮我们完成REST风格的调用,大大减少代码的编写量,提高代码的可读性。注意,这里谈到的OpenFeign是一个第三方组件,为了降低开发者的学习成本,Spring Cloud将 OpenFeign封装后,给出的规则完全采用了Spring MVC的风格,也就是只要开发者熟悉Spring MVC,就能很轻松地完成接口的描述,完成服务调用的功能,而不必学习OpenFeign自身的声明规则。

1.3 Feign和OpenFeign的关系

Feign本身不支持Spring MVC的注解,它有一套自己的注解。

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。
OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

1.4 Feign工作原理

  • 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClients注解。
  • 当程序启动时,会进行包扫描,扫描所有@FeignClients的注解的类,并将这些信息注入Spring IOC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理的方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequetTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都是在这个过程中确定的。
  • 然后由RequestTemplate生成Request,然后把Request交给Client去处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。

2 Feign基础功能

2.1 FeignClient注解

FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上。当打开org.springframework.cloud.openfeign.FeignClient这个注解定义类的时候,可以看到FeignClient注解对应的属性。
FeignClient注解的常用属性归纳如下:

  • name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。
  • url:url一般用于调试,可以手动指定@FeignClient调用的地址。
  • decode404:当发生404错误时,如果该字段为true,会调用decoder进行解码,否则抛出FeignException。
  • configuration:Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract。
  • fallback:定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口。
  • fallbackFactory:工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。
  • path:定义当前FeignClient的统一前缀。

2.2 Feign开启GZIP压缩

Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高通信效率,下面将对Feign开启GZIP压缩的配置进行说明。

feign:
    compression:      
  request:      
      enabled: true	#配置请求GZIP压缩  
      mime-types: text/xml,application/xml,application/json	# 配置压缩支持的MIME TYPE      
      min-request-size: 1024  # 配置压缩数据大小的下限(单位是B)      
  response:      
      enabled: true # 配置响应GZIP压缩

2.3 Feign开启日志

Feign为每一个FeignClient都提供了一个feign.Logger实例,可以在配置中开启日志,开启方式比较简单,分为两步。

第一步:在application.yml中配置日志输出。

logging:
    level: cn.springcloud.book.feign.service.HelloFeignService: debug

第二步:
通过Java代码的方式配置日志Bean,代码如下所示:

package com.pps.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import feign.Logger;

@Configuration
public class FeiginConfig {

	@Bean
	Logger.Level logLevel(){
		
		return Logger.Level.BASIC;
	}
}

2.4 Feign超时设置

Feign的调用分两层,即Ribbon的调用和Hystrix的调用,高版本的Hystrix默认是关闭的。

  • 如果出现下面的报错信息,说明Ribbon处理超时。

    feign.RetryableException: Read timed out executing POST http://******
    at feign.FeignException.errorExecuting(FeignException.java:67) at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMet
    hodHandler.java:104) at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler
    .java:76) at feign.ReflectiveFeign F e i g n I n v o c a t i o n H a n d l e r . i n v o k e ( R e f l e c t i v e F e i g n . j a v a : 103 ) a t c o m . s u n . p r o x y . FeignInvocationHandler.invoke(Reflective Feign.java:103) at com.sun.proxy. FeignInvocationHandler.invoke(ReflectiveFeign.java:103)atcom.sun.proxy.Proxy113.getBaseRow(Unknown Source)Caused by: java.net.SocketTimeoutException: Read timed out

此时设置Ribbon的配置信息如下即可。

#请求处理的超时时间
ribbon.ReadTimeout: 120000#请求连接的超时时间
ribbon.ConnectTimeout: 30
  • 如果开启Hystrix,Hystrix的超时报错信息如下所示

    com.netflix.hystrix.exception.HystrixRuntimeException: FeignDemo#demo() timed-out and no fallback available.
    at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:819) at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:804) at rx.internal.operators.OperatorOnErrorResumeNextViaFunction 4. o n E r r o r ( O p e r a t o r O n E r r o r R e s u m e N e x t V i a F u n c t i o n . j a v a : 140 ) a t r x . i n t e r n a l . o p e r a t o r s . O n S u b s c r i b e D o O n E a c h 4.onError(OperatorOnErrorResumeNextViaFunc tion.java:140) at rx.internal.operators.OnSubscribeDoOnEach 4.onError(OperatorOnErrorResumeNextViaFunction.java:140)atrx.internal.operators.OnSubscribeDoOnEachDoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87)

看到上面的报错信息,说明Hystrix超时报错,此时设置Hystrix的配置信息如下所示:

hystrix: 
	shareSecurityContext: true    
	command:       
    	default:	
    		circuitBreaker: 
    			sleepWindowInMilliseconds: 100000                
    			forceClosed: true            
    		execution:                
    			isolation:                    
    				thread:
                        timeoutInMilliseconds: 600000

3 Feign高级应用

3.1 Feign默认Client的替换

Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。我们可以用Apache的HTTP Client替换Feign原始的HTTP Client,通过设置连接池、超时时间等对服务之间的调用调优。SpringCloud从Brixtion.SR5版本开始支持这种替换,接下来介绍一下如何用HTTP Client和okhttp去替换Feign默认的Client。

  • 使用HTTP Client替换Feign默认Client

第一步,在pom.xml中引入Apache HttpClient替换Spring Cloud OpenFeign

<dependencies>
    <dependency>        
        <groupId>org.springframework.boot</groupId>        
        <artifactId>spring-boot-starter-web</artifactId>    
    </dependency>
    <!-- Spring Cloud OpenFeign的Starter的依赖 --> 
    <dependency>        
        <groupId>org.springframework.cloud</groupId>       
        <artifactId>spring-cloud-starter-openfeign</artifactId>    
        </dependency>    
    <!-- 使用Apache HttpClient替换Feign原生httpclient -->    
    <dependency>        
        <groupId>org.apache.httpcomponents</groupId>        
        <artifactId>httpclient</artifactId>    
    </dependency>    
    <dependency>        
    	<groupId>com.netflix.feign</groupId>       
    	<artifactId>feign-httpclient</artifactId>       
    	<version>8.17.0</version>    
    </dependency>
</dependencies>

第二步,在application.yml中配置让Feign启动时加载HTTP Client替换默认的Client

server:
    port: 8610
spring: 
	application:       
 		name: pps-httpclient
feign:    
	httpclient:       
 		enabled: true

第三步,运行主程序即可。

  • 使用okhttp替换Feign默认的Client

TP是目前比较通用的网络请求方式,用来访问请求交换数据,有效地使用HTTP可以使应用访问速度变得更快,更节省带宽。okhttp是一个很棒的HTTP客户端,具有以下功能和特性。

(1)支持SPDY,可以合并多个到同一个主机的请求。
(2)使用连接池技术减少请求的延迟(如果SPDY是可用的话)。
(3)使用GZIP压缩减少传输的数据量。
(4)缓存响应避免重复的网络请求。

接下来,介绍一下如何使用okhttp替换Feign默认的Client。

第一步,pom.xml中添加依赖

<dependencies>
    <dependency>        
        <groupId>org.springframework.boot</groupId>        
        <artifactId>spring-boot-starter-web</artifactId>    
    </dependency>    
    <!-- Spring Cloud OpenFeign的Starter的依赖 -->    
    <dependency>        
        <groupId>org.springframework.cloud</groupId>        
        <artifactId>spring-cloud-starter-openfeign</artifactId>    
    </dependency>    
    <dependency>        
        <groupId>io.github.openfeign</groupId>        
        <artifactId>feign-okhttp</artifactId>    
    </dependency>
</dependencies>

第二步,开启okhttp为Feign默认的Client

server:
    port: 8611
spring: 
	application:       
 		name: pps-okhttp

feign:   
	httpclient:       
  		enabled: false    
  	okhttp:       
  		enabled: true

第三步,okHttpClient是okhttp的核心功能的执行者,可以通过OkHttpClient client=new OkHttp Client();来创建默认的OkHttpClient对象,也可以使用如下代码来构建自定义的OkHttpClient对象,上面配置信息只给出了常用的设置项,其他设置项比较复杂,有兴趣的读者可以自己扩展阅读学习。

@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {   
    @Bean   
    public okhttp3.OkHttpClient okHttpClient(){
        return new okhttp3.OkHttpClient.Builder() 
            //设置连接超时
            .connectTimeout(60, TimeUnit.SECONDS)            
            //设置读超时            
            .readTimeout(60, TimeUnit.SECONDS)           
            //设置写超时            
            .writeTimeout(60,TimeUnit.SECONDS)           
            //是否自动重连            
            .retryOnConnectionFailure(true)            
            .connectionPool(new ConnectionPool())           
            //构建OkHttpClient对象            
            .build();    
	}              
}

第四步,启动主程序即可。

3.2 Feign的Post和Get多参数传递

在实际项目开发过程中,我们使用Feign实现服务与服务之间的调用。但是在很多情况下,多参数传递是无法避免的。通过实现Feign的RequestInterceptor中的apply方法来进行统一拦截转换处理Feign中的GET方法多参数传递的问题。

@Component
public class FeignRequestInterceptor implements RequestInterceptor {    
    @Autowired   
    private ObjectMapper objectMapper;    
    @Override    
    public void apply(RequestTemplate template) {       
        //feign不支持GET方法传POJO, json body转query       
        if ("GET".equals(template.method()) && null != template.body() ) {          
          try {    
              JsonNode jsonNode = objectMapper.readTree(template.body());          
              template.body(null);          
              Map<String, Collection<String>> queries = new HashMap<>();          
              buildQuery(jsonNode, "", queries);          
              template.queries(queries);          
          } catch (IOException e) {     
              e.printStackTrace();          
		}        
	}    
}

3.3 Feign首次请求失败问题

当Feign和Ribbon整合了Hystrix之后,可能会出现首次调用失败的问题,造成该问题出现的原因分析如下:
Hystrix默认的超时时间是1秒,如果超过这个时间尚未做出响应,将会进入fallback代码。由于Bean的装配以及懒加载机制等,Feign首次请求都会比较慢。如果这个响应时间大于1秒,就会出现请求失败的问题。

下面以feign为例,介绍三种方法处理Feign首次请求失败的问题。

  • 方案一:将Hystrix的超时时间改为5秒,配置如下所示:

    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000

  • 方案二:禁用Hystrix的超时时间,配置如下所示:

    hystrix.command.default.execution.timeout.enabled: false

3.4 Feign调用传递Token

在进行认证鉴权的时候,不管是jwt,还是security,当使用Feign时就会发现外部请求到A服务的时候,A服务是可以拿到Token的,然而当服务使用Feign调用B服务时,Token就会丢失,从而认证失败。解决方法相对比较简单,需要做的就是在Feign调用的时候,向请求头里面添加需要传递的Token。

我们只需要实现Feign提供的一个接口RequestInterceptor,假设我们在验证权限的时候放在请求头里面的key为oauthToken,先获取当前请求中的key为oauthToken的Token,然后放到Feign的请求Header上。

package com.pps.common.config;

import feign.RequestInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;

/**
 * @author pangps
 * feign拦截器
 */
@Configuration
public class FeignInterceptorConfig {
    private static final Logger logger = LoggerFactory.getLogger(FeignInterceptorConfig.class);
    /**
     * 使用feign client访问别的微服务时,将access_token放入参数或者header ,Authorization:Bearer xxx
     * 或者url?access_token=xxx
     */
    @Bean
    public RequestInterceptor requestInterceptor() {
        return template -> {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null) {
                if (authentication instanceof OAuth2Authentication) {
                    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
                    String accessToken = details.getTokenValue();
                    template.header("Authorization", OAuth2AccessToken.BEARER_TYPE + " " + accessToken);
                }
            }
            template.header("isInterService", "true");
        };
    }
}

4 码农来洞见

4.1 venus-cloud-feign推荐

venus-cloud-feign,对Spring Cloud Feign的实战增强。

  • 包名规范

cn.springcloud.feign

  • Maven仓库

    cn.springcloud.feign venus-cloud-starter-feign 1.0.0
  • 应用场景

主要由于使用了API(SDK)为了偷懒,以及Restful API路径中的版本带来的一系列问题。

为了方便,API中使用Feign替代RestTemplate手动调用,在写Feign接口的时候,想用Spring MVC注解只在Feign接口写一遍,然后实现类实现此接口即可。但是Spring MVC不支持实现接口中方法参数上的注解(支持继承类、方法上的注解)。Spring Cloud中国社区对spring-cloud-openfeign进行增强,命名为venus-cloud-feign。

  • 版本支持

Spring Cloud的版本为Finchley.RELEASE。

  • 项目开源地址

https://github.com/springcloud/venus-cloud-feign

4.2 资料参考

Feign官网:英文水平高的可以直接看官方文档

Feign官网中文版:英语水平稍差的可以先看中文版,在看官网

猜你喜欢

转载自blog.csdn.net/pangpengshuai/article/details/121376005