一、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
三、简单示例
- 在程序的启动类加上注解@EnableFeignClients开启Feign Client功能
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
- RPC调用
@Component
@FeignClient(value = "USER-SERVICE")
public interface UserFeign {
@GetMapping("/getUser")
String getUser();
}
- 测试
@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属性,可以配置我们自定义的配置类,在此类中注入微服务认证拦截器
- 创建Feign的配置类
@Configuration
public class FeignConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor(){
return new BasicAuthRequestInterceptor("admin","123456");
}
}
说明:@Bean配置是在请求接口中要进行基于Http Basic的认证后才能调用。
- 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
网友评论