SpringCloud--Feign RPC调用(五)

一、Feign简介

  Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。
在Spring Cloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。
  Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。
  Spring Cloud Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon和Spring Cloud Hystrix,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。
  Spring Cloud Feign帮助我们定义和实现依赖服务接口的定义。在Spring Cloud feign的实现下,只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量。
  Spring Cloud Feign具备可插拔的注解支持,支持Feign注解、JAX-RS注解和Spring MVC的注解。

文档地址:
https://cloud.spring.io/spring-cloud-openfeign/spring-cloud-openfeign.html

二、Maven依赖

<!--web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--openfeign依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

三、配置文件

server.port=8008
eureka.register.port=8761
#服务实例名
eureka.instance.hostname=order-service
#服务名,如果没有则为 unknown
spring.application.name=order-service

eureka.register.host=localhost
eureka.client.serviceUrl.defaultZone=http\://${eureka.register.host}\:${eureka.register.port}/eureka/
#显示IP
eureka.instance.prefer-ip-address=true
# 注2.0 必须为ip-address
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(客户端告诉服务端自己会按照该规则)
eureka.instance.lease-renewal-interval-in-seconds =3

三、简单示例

  1. 在程序的启动类加上注解@EnableFeignClients开启Feign Client功能
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
  1. RPC调用
@Component
@FeignClient(value = "USER-SERVICE")
public interface UserFeign {
    @GetMapping("/getUser")
    String getUser();
}

  1. 测试
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class OrderServiceApplicationTests {
    @Autowired
    private UserFeign userFeign;

    @Test
    public void contextLoads() {
        String str = userFeign.getUser();
        log.info(str);
    }
}

四、Feign 基本使用

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

@FeignClient(name = "product-provider")  
public interface ProductServiceFeign {  
  
    /** 
     * PathVariable 注解使用时,必须里面要有值,即@PathVariable("")或@PathVariable(name="") 
     * 
     * @param productId 
     * @return 
     */  
    @GetMapping("product/selectOne/{productId}")  
    ProductVO selectByProductId(@PathVariable("productId") String productId);  
  
    /** 
     * 去掉 @RequestParam 注解将变成post请求,加上为get请求 
     * 
     * @param params 
     * @return 
     */  
    @GetMapping("product/selectByProductIdAndName")  
    Map<String, Object> selectByProductIdAndNameMap(@RequestParam Map<String, Object> params);  
  
//  程序启动报错,存在多个参数没有@requestParam注解  
//  @GetMapping("product/selectByProductIdAndName")  
//  Map<String, Object> selectByProductIdAndName(String productId, String productName);  
  
//  程序启动报错,没有指定value的值,且参数不是map  
//  @GetMapping("product/selectByProductIdAndName")  
//  Map<String, Object> selectByProductIdAndName(@RequestParam String productId, String productName);  
  
//  由于后面有一个参数没有加上@RequestParam注解,此时这个请求就变成了post请求发送,即使申明的是get请求  
//  @GetMapping("product/selectByProductIdAndName")  
//  Map<String, Object> selectByProductIdAndName(@RequestParam("productId") String productId, String productName);  
  
    @GetMapping("product/selectByProductIdAndName")  
    Map<String, Object> selectByProductIdAndName(@RequestParam("productId") String productId, @RequestParam("productName") String productName);  
  
    @PostMapping("product/addProduct")  
    Map<String, Object> addProduct(@RequestBody ProductVO productVO);  
  
    @PostMapping("product/updateProduct")  
    Map<String, Object> updateProduct(@RequestParam Map<String, Object> params);  
  
    @PostMapping("product/delete")  
    Map<String, Object> delteProduct(@RequestParam("productId") String productId);  
  
}  

五、Feign 自定义配置

把配置文件放到ComponentScan扫描不到的地方。
Configuration配置类

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

import feign.Contract;
import feign.Feign;

@Configuration
public class FeignConfig {

    //配置是在FeignClient中的接口中使用Feign自带的注解
    @Bean
    public Contract feignContract(){
        return new feign.Contract.Default();
   }
    
    //禁用Hystrix
    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder() {
        return Feign.builder();
    } 
}

然后在UserFeignClient类中指定configuration:

@FeignClient(value = "user-service",configuration = Configuration.class)

第一个bean配置的是使用Feign的默认注解。
注意,我们在此类中修改了Feign的Contract ,那么Contract 是什么呢。它叫做契约。因为Feign一开始使用的契约是SpringMVC,所以刚才我们SpringMVC的注解的时候直接成功了,但是你如果现在启动项目你就会发现已经启动不了了。因为Contract.Default()使用的契约是Feign自己的,也就是说我们要把SpringMVC的注解修改为Feign的注解。
第二个bean配置的是是禁用Hystrix
SpringMVC版本

@GetMapping (value = "/user/getUser/{id}")
public User getUser(@PathVariable("id")Long id);

Feign版本

@RequestLine("GET /user/getUser/{id}")
 public User getUser(@Param("id") Long id);

六、Feign输出日志

Feign日志输出说明
Feign的日志是以下部分组成
1、Feign的Level日志级别配置默认是:NONE,不要跟log日志级别混淆
日志级别枚举类 Logger.Level
NONE 不输出日志
BASIC 只有请求方法、URL、响应状态代码、执行时间
HEADERS基本信息以及请求和响应头
FULL 请求和响应 的heads、body、metadata,建议使用这个级别
2、log日志级别配置,默认是debug
使用指定Feign类会包名配置日志打印级别
此处使用spring logging配置 比如打印 UserFeign logging.level.com.example.feign.UserFeign=debug
Feign日志输出-Logger.Level.FULL+log debug级别
全局开启方式 使用spring java config 配置,注意该类一定要放到spring可以扫描到的包下:

@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLevel() {
        return Logger.Level.FULL;
    }
}

application.properties 配置debug 日志输出级别

user.url=http://localhost:8080/user
logging.level.com.example.feign.UserFeign=debug

运行 UserFeignTest.save方法 可以看到以下保存user输出的请求和响应日志

2018-08-07 16:48:03.011 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] ---> POST http://localhost:8080/user HTTP/1.1
2018-08-07 16:48:03.011 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] Content-Type: application/json;charset=UTF-8
2018-08-07 16:48:03.011 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] Content-Length: 27
2018-08-07 16:48:03.011 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] 
2018-08-07 16:48:03.011 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] {"id":null,"name":"张三"}
2018-08-07 16:48:03.012 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] ---> END HTTP (27-byte body)
2018-08-07 16:48:03.041 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] <--- HTTP/1.1 200 (29ms)
2018-08-07 16:48:03.042 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] content-length: 0
2018-08-07 16:48:03.043 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] date: Tue, 07 Aug 2018 08:48:03 GMT
2018-08-07 16:48:03.043 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] 
2018-08-07 16:48:03.043 DEBUG 75661 --- [           main] com.example.feign.UserFeign              : [UserFeign#save] <--- END HTTP (0-byte body)

七、配置请求用户名密码

在项目中,微服务之间的通信也是通过Feign代理的HTTP客户端通信,为了保护我们的业务微服务不被其他非法未经允许的服务调用, 我们要进行访问授权配置!
Feign是客户端配置,@FeignClient注解有个configuation属性,可以配置我们自定义的配置类,在此类中注入微服务认证拦截器

  1. 创建Feign的配置类
@Configuration
public class FeignConfiguration {
    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor(){
        return new BasicAuthRequestInterceptor("admin","123456");
    }
}

说明:@Bean配置是在请求接口中要进行基于Http Basic的认证后才能调用。

  1. FeignClient接口引用配置
@Component
@FeignClient(value = "USER-SERVICE", configuration= FeignConfiguration.class)
public interface UserFeign {
    @GetMapping("/getUser")
    String getUser();
}

需要注意的是: FeignConfiguration类不能包含在主应用程序上下文的@ComponentScan中,否则该配置会被所有的@FeignClient共享。
Ribbon的自定义配置中也需要注意这个问题。

八、Feign超时设置

feign.client.config.springApplicationName.connectTimeout=1000
feign.client.config.springApplicationName.readTimeout=1000

注意:
@GetMapping注解不支持;
@PathVariable注解需要设置value值;
@RequestParam注解需要设置value值;
接口参数是复杂的JAVA对象的时候,需要采用POST方式请求,且参数名前需要添加@RequestBody注解,且需要保证接口提供者的接口访问方式是POST;
客户端的调用接口的FeignClient接口中,方法名、参数名及参数类型必须和接口方法保持一致;参数名前必须添加@PathVariable或者@RequestParam注解。
FeignClient注解中没有写其他值,则name值只得是服务提供者的服务名称;如果定义了url,则feignClient会查找对应url上的微服务,name此时的值是指feignClient的名称。name值必须填写,还可以设置其他的值,如configuration(feignClient配置:默认是SpringMvcContract)的值;
多个feignCLient类中@FeignClient注解中的name值不能重复,url可以重复;

九、对象参数

  1. 提供端与服务端都使用POST方法。
    参数使用@RequestBody注解。

十、Feign的Encoder、Decoder和ErrorDecoder

Feign将方法签名中方法参数对象序列化为请求参数放到HTTP请求中的过程,是由编码器(Encoder)完成的。同理,将HTTP响应数据反序列化为java对象是由解码器(Decoder)完成的。
默认情况下,Feign会将标有@RequestParam注解的参数转换成字符串添加到URL中,将没有注解的参数通过Jackson转换成json放到请求体中。注意,如果在@RequetMapping中的method将请求方式指定为POST,那么所有未标注解的参数将会被忽略。

    @PostMapping(value = "/goods/sku/skus", consumes = MediaType.APPLICATION_JSON_VALUE)
    Result<List<Sku>> getSkus(List<Long> ids);

注:其中参数中的@RequestBody省略。

十一、常用注解

注解@EnableFeignClients告诉框架扫描所有使用注解@FeignClient定义的feign客户端;
注解@FeignClient 定义feign客户端 ;

@FeignClient(value = "GOODS-SERVICE",path = "/goods/sku")

注解@Autowired使用所定义feign的客户端。

十二、OkHttp替换Feign原生httpclient

  1. Maven依赖
<!--openfeign依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
  1. 配置
# okhttp替换Feign原生httpclient
feign.okhttp.enabled= true
  1. OkHttp连接池配置
/**
 * OkHttp连接池配置
 */
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
    @Bean
    public okhttp3.OkHttpClient okHttpClient(){
        return new okhttp3.OkHttpClient.Builder()
                // 读取超时时间
                .readTimeout(60, TimeUnit.SECONDS)
                // 连接超时时间
                .connectTimeout(60,TimeUnit.SECONDS)
                .connectionPool(new ConnectionPool())
                .build();
    }
}

十三、Apache的HTTP Client替换Feign原生httpclient

  1. Maven依赖
<!--openfeign依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 使用Apache HttpClient替换Feign原生httpURLConnection -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>10.4.0</version>
</dependency>
  1. 配置
# HttpClient替换Feign原生httpclient
feign.httpclient.enabled= true
  1. 测试(可以使用GET方法传实体)
    服务提供:
@GetMapping(value = "/skus",consumes = MediaType.APPLICATION_JSON_VALUE)
public Result<List<Sku>> getSkus(@RequestBody List<Long> ids){
    QueryWrapper<Sku> queryWrapper=new QueryWrapper<>();
    queryWrapper.in("id",ids);
    List<Sku> list=skuService.list(queryWrapper);
    return  Result.ok(list);
}

服务消费:

@GetMapping(value = "/goods/sku/skus", consumes = MediaType.APPLICATION_JSON_VALUE)
    Result<List<Sku>> getSkus(List<Long> ids);

十四、常见问题

  1. java.lang.NoSuchMethodError: feign.Response.create(ILjava/lang/String;Ljava/util/Map;Lfeign/Response$Body;)Lfeign/Response;
    原因:使用包装类Result没有明确指定泛型类型。
    例:
    接口提供端:
@PostMapping(value = "/skus",consumes = MediaType.APPLICATION_JSON_VALUE)
    public Result<List<Sku>> getSkus(@RequestBody List<Long> ids){
}

接口消费端:

@PostMapping(value = "/goods/sku/skus", consumes = MediaType.APPLICATION_JSON_VALUE)
    Result<List<Sku>> getSkus(@RequestBody List<Long> ids);

如果使用了HTTP Client替换Feign原生httpclient:
原因为依赖不正确:
使用以下:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>10.4.0</version>
</dependency>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容