Feign 是Spring Cloud Netflix组件中的一个轻量级Restful的 HTTP 服务客户端,实现了负载均衡和 Rest 调用的开源框架,封装了Ribbon和RestTemplate, 实现了WebService的面向接口编程,进一步降低了项目的耦合度。
先来看我们以前利用RestTemplate发起远程调用的代码:
String url = "http://user-service:8081/user/"+order.getUserId();
User user = restTemplate.getForObject(url, User.class);
以上的代码存在参数复杂、URL难以维护等问题,如当我有一台服务地址换了,那么这时候就需要云同步修改url,那要是多台要修改的情况下那就得改很多台,当我们服务多的时候这是个很麻烦的事情。
1.Feign替代RestTemplate
Fegin的使用步骤如下:
1)引入依赖
我们在order-service服务的pom文件中引入feign的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2)添加注解
在order-service的启动类添加注解开启Feign的功能:
3)编写Feign的客户端
在order-service中新建一个接口,内容如下:
package cn.itcast.order.service.feign;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("user-service")
public interface UserFeignClient {
@GetMapping("/user/{id}")
User queryById(@PathVariable("id") Long id);
}
这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- 服务名称:user-service
- 请求方式:GET
- 请求路径:/user/{id}
- 请求参数:Long id
- 返回值类型:User
这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。底层会通过服务名称:user-service去映射到具体的user服务对应的url地址。
4)测试
修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
@Autowired
UserFeignClient feignClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
User user = feignClient.queryById(orderId);
order.setUser(user);
// 4.返回
return order;
}
}
测试调用:
2.自定义配置
Feign可以支持很多的自定义配置,如下表所示:
类型 | 作用 | 说明 |
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
下面以日志为例来演示如何自定义配置。
2.1.配置文件方式
基于配置文件修改feign的日志级别可以针对单个服务:(注意:有时yml配置文件中有中文注释会报错)
feign:
client:
config:
user-service: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别
也可以针对所有服务:
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
而日志的级别分为四种:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
2.2.Java代码方式
也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:
package cn.itcast.order;
import cn.itcast.order.config.DefaultFeignConfiguration;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
// 配置类的方式开启全局日志记录
//@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class) // 开启feign客户端的支持
@EnableFeignClients // 开启feign客户端的支持
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
//......
}
如果是局部生效,则把它放到对应的@FeignClient这个注解中:
package cn.itcast.order.feign;
import cn.itcast.order.config.DefaultFeignConfiguration;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 指定服务日志配置
//@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
@FeignClient(value = "userservice")
public interface UserFeignClient {
@GetMapping("/user/{id}")
User queryById(@PathVariable("id") Long id);
}
2.3.Feign使用优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
•URLConnection:默认实现,不支持连接池
•Apache HttpClient :支持连接池
•OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
这里我们用Apache的HttpClient来演示。
1)引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖:
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2)配置连接池
在order-service的application.yml中添加配置:
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
max-connections解释:比如有以下情况,有一个服务A同时有可能会访问B服务和C服务,这时候配置的最大连接数指的就是A在访问B和C时,总的连接数不超过200。
max-connections-per-route解释:指的是A服务访问B服务时的路径最大连接数据为50,也就是200个连接,A服务到B服务的访问最多只会有50个连接,当超出50个连接时,其他连接就会路由到B服务之外的服务中。
接下来,在FeignClientFactoryBean中的loadBalance方法中打断点:
Debug方式启动order-service服务,可以看到这里的client,底层就是Apache HttpClient:
改成http连接池后,从演示项目后台的请求日志中可以发现会从原来的几十ms变成个位数ms,有兴趣的小伙伴可以自己测试一下。
2.4.最佳实践-抽取feign-api接口
目前存在一个问题,我们目前演示的是只有一个order-service调用user-service,那如果当有多个服务要去调userservice的时候呢,那是否需要在每个service里都去写一份远程调用user-service的代码?完成没必要是不是?所以,把这部分代码直接抽成一个module打成jar包,在需要调用的地方引入即可。
将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
即将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
1)抽取
首先创建一个module,命名为feign-api:
项目结构:
在feign-api中然后引入feign的starter依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
然后,order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
2)在order-service中使用feign-api
首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。
在order-service的pom文件中中引入feign-api的依赖:
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包
扫描包问题
最后,包的扫描要指定一下,不然启动会报错找不到userfeignclient
方式一:
指定Feign应该扫描的包:
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
方式二:
指定需要加载的Client接口:
@EnableFeignClients(clients = {UserClient.class})
Feign实现原理
Feign的底层源码实现主要包括以下几个部分:
1. 接口定义
Feign的接口定义类似于Java的接口定义,但是它使用了注解来描述HTTP请求的参数和返回值。例如,@RequestMapping注解用于指定HTTP请求的URL和请求方法,@RequestParam注解用于指定HTTP请求的参数,@RequestBody注解用于指定HTTP请求的请求体,@PathVariable注解用于指定HTTP请求的路径参数等。
2. 动态代理
Feign使用了Java的动态代理技术来生成HTTP请求的实现类。当应用程序调用Feign接口的方法时,Feign会动态生成一个HTTP请求的实现类,并将请求参数传递给该实现类。该实现类会将请求参数转换为HTTP请求,并发送给远程服务。当远程服务返回响应时,该实现类会将响应转换为Java对象,并返回给应用程序。
3. 编码器和解码器
Feign使用了编码器和解码器来将Java对象转换为HTTP请求和响应。编码器将Java对象转换为HTTP请求的请求体,解码器将HTTP响应的响应体转换为Java对象。Feign支持多种编码器和解码器,例如JSON编码器和解码器、XML编码器和解码器等。
4. 负载均衡
Feign可以与负载均衡器无缝集成,以实现服务的负载均衡。当应用程序调用Feign接口的方法时,Feign会将请求发送给负载均衡器,负载均衡器会选择一个可用的服务实例,并将请求转发给该实例。如果该实例不可用,则负载均衡器会选择另一个可用的服务实例,并将请求转发给该实例。
5. 断路器
Feign可以与断路器无缝集成,以实现服务的容错。当应用程序调用Feign接口的方法时,Feign会将请求发送给断路器,断路器会检查服务实例的可用性。如果服务实例不可用,则断路器会返回一个默认的响应,以避免应用程序出现异常。如果服务实例可用,则断路器会将请求转发给该实例,并返回实例的响应。