Feign是Spring Cloud 提供的声明式、模板化的Http客户端,它主要的作用是简化远程调用,当通过RestTemplate调用其他服务的API时,所需要的参数须在请求的URL中拼接,这种方式容易出错又不美观而且效率低下,Fegin具体实现的方式为创建一个接口,并添加一个注解。
注意:Feign默认集成了Ribbon,所以Feign默认就实现了负载均衡。
Fegin的入门使用:
实际调用服务的流程图如上图所示
来搭建一个Fegin的工程,创建服务提供者feign_provider
pom依赖(引入nacos的注册中心以及Spring Boot的web启动器):
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
Controller:
@RestController
@RequestMapping("/provider")
public class ProviderController {
@Autowired
private UserService userService;
@RequestMapping("/getUserById/{id}")
public User getUserById(@PathVariable Integer id){
return userService.getUserById(id);
}
}
Service接口:
public interface UserService {
User getUserById(Integer id);
}
ServiceImpl:
@Service
public class UserServiceImpl implements UserService {
@Override
public User getUserById(Integer id) {
System.out.println("one...");
return new User(id,"张三",18);
}
}
启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class FeignProviderApp {
public static void main(String[] args) {
SpringApplication.run(FeignProviderApp.class,args);
}
}
application.yaml配置:
server:
port: 9090
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.40.141:8848
application:
name: feign-provider
创建interface工程(当做依赖被服务消费者所引用):
pom依赖(在interface中引入openfeign的依赖及项目的公共模块):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
接口:
@FeignClient(value = "feign-provider")
@RequestMapping("/provider")
public interface UserFeign {
@RequestMapping("/getUserById/{id}")
User getUserById(@PathVariable("id") Integer id);
}
创建消费者工程fegin_consumer:
pom中引入Spring Boot的web启动器,interface工程以及nacos的注册中心
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
config配置类:
@Configuration
public class ConfigBean {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
public IRule iRule(){
return new RandomRule();
}
}
controller:
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
private UserFeign userFeign;
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
return userFeign.getUserById(id);
}
}
启动类:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients("com.bjpowernode")
public class ConsumerApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerApp.class,args);
}
}
application.yaml配置文件:
server:
port: 80
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.40.141:8848
application:
name: feign-consumer
准备就绪,启动服务提供者和服务消费者,在浏览器中访问localhost/consumer/getUserById/2
测试:
访问成功。
@EnableFeignClients注解的作用:
开启Feign的注解扫描,这个注解中引入了FeignClientsRegistrar这个类,在服务启动时,会调用它的registerFeignClients()方法扫描被@FeignClient注解的接口,再将这些接口注入到spring的ioc容器中。
其实现如下:
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
... ... ...
//扫描feign接口
for (String basePackage : basePackages) {
//扫描获取BeanDefinition集合
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
//判断迭代并判断集合中的元素是否是AnnotatedBeanDefinition的实例,如果是则强转
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)
candidateComponent;
//获得UserFeign的详细信息
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
//@FeignClient注解只能用在接口上,被@FeignClient注解的是否为接口
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
//获取注解的属性
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
//注入Feign接口到Spring容器中
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
Feign的执行流程:
SynchronousMethodHandler.invoke(): 当定义的的Feign接口中的方法被调用时,通过JDK的代理方式为Feign接口生成了一个动态代理类,当生成代理时,Feign会为每个接口方法创建一个RequestTemplate。该对象封装了HTTP请求需要的全部信息,如请url、参数,请求方式等信息都是在这个过程中确定的。
public Object invoke(Object[] argv) throws Throwable {
//创建一个RequestTemplate
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
//发出请求
return this.executeAndDecode(template);
} catch (RetryableException var8) {
... ... ...
}
}
}
看一下RequestTemplate定义的属性:
package feign;
public final class RequestTemplate implements Serializable {
... ... ... ... ... ...
private UriTemplate uriTemplate;
private HttpMethod method;
private Body body;
... ... ... ... ... ...
}
当发起请求的时候,执行SynchronousMethodHandler.executeAndDecode();方法
通过RequestTemplate生成Request,然后把Request交给Client去处理,Client可以是JDK原生的URLConnection,Apache的HttpClient,也可以时OKhttp,最后Client结合Ribbon负载均衡发起服务调用。
Object executeAndDecode(RequestTemplate template) throws Throwable {
//生成请求对象
Request request = this.targetRequest(template);
if (this.logLevel != Level.NONE) {
this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
}
long start = System.nanoTime();
Response response;
try {
//发起请求
response = this.client.execute(request, this.options);
} catch (IOException var15) {
... ... ...
throw FeignException.errorExecuting(request, var15);
}
}
Feign的参数传递方式:
1、如果是Restful风格的路径传参
则feign接口的形参必须使用@PathVarible接收,并且必须指定value值,否则报错
启动出现如下错误:
改为:
成功启动!
2.如果是get请求中的占位符拼接传参则需要使用@RequestParam注解接收
同样必须指定value,否则报错!
首先在在服务提供者,消费者以及feign的接口中添加如下方法:
feign_provider的controller:
@RequestMapping(value = "/getUser")
public User getUser(Integer id) {
return userService.getUser(id);
}
@RequestMapping(value = "/addUser")
public User addUser(@RequestBody User user) {
return userService.addUser(user);
}
service:
@Override
public User getUser(Integer id) {
return new User(id,"李四",20);
}
@Override
public User addUser(User user) {
user.setName("王五");
return user;
}
feign_consumer的controller:
@RequestMapping(value = "/getUser")
public User getUser(Integer id) {
return userFeign.getUser(id);
}
@RequestMapping(value = "/addUser")
public User addUser(User user) {
return userFeign.addUser(user);
}
feign接口:
@RequestMapping("/getUser")
User getUser(@RequestParam("id") Integer id);
@RequestMapping("/addUser")
User addUser(@RequestBody User user);
启动测试:
先测试getUser方法:
再测试addUser方法:
这里解释一下为什么fegin接口中要加@RequsetBody和@RequestParam注解
在Controller中,如果拼接传参的是一个对象的属性,只要变量名一致,可以不用加注解直接接收。但是使用feign实际上是用的http通信,服务消费者在调用feign获取服务提供者的服务时,fegin会获取到服务提供者的信息,包括ip端口号接口信息等,并且发送http请求,发送请求的时候会自动拼接url,而如果不使用注解直接发送请求,feign就不知道该如何拼接,比如上面的@PathVarible的例子中,为什么不指定value就会报错呢?feign帮助我们拼接url的时候,获取到了http://localhost:9090/provider/getUserById/{},最后的路径它就不知道该拼接什么。同理@RequestParam的作用也是一样。而为何要使用@RequestBody是因为java对象不能被http请求识别,使用该注解后,feign就会将java对象转换为json串并且处理后发送到服务提供者调用。
Feign的调优:
feign基于http通信协议,从通讯速度上来讲是不如rpc框架的如dubbo
那从速度上考虑为什么不使用更快的dubbo呢,引入feign的调优
主要有四个方面:
1.feign的日志:
feign的日志是基于spring的日志的,所以开启feign日志之前要先配置spring的日志:
application.yaml配置:
feign:
client:
config:
default:
loggerLevel: full
logging:
level:
com.bjpowernode.feign: debug
重启服务:然后再次访问localhost/consumer/addUser
日志配置完毕
2.feign的超时时间
feign的超时有两种配置方式,可以配置ribbon的超时时间,也可以配置feign的
如下:
配置ribbon超时
ribbon:
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 5000 #请求处理的超时时间
配置feign超时:
feign:
client:
config:
feign-provider:
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 5000 #请求处理的超时时间
默认feign的超时时间是一秒
3.feign的http连接池
连接池的配置非常简单,引入依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
在配置文件中开启(默认开启可以不用配置)
feign:
httpclient:
enabled: true
测试是否替换成功:
Debug模式下启动,在SynchronousMethodHandler.executeAndDecode()断点进入,查看client属性
如果这里显示ApacheHttpClient表示替换成功!
4.fegin的压缩
在配置文件中开启fegin的gzip压缩
开启gzip压缩后,效率能提高近乎百分之70
server:
compression:
enabled: true
mime-types配置项默认指定了常用的格式,这里没有自定义,采用默认
重启项目测试
配置成功