Feign声明式服务调用
1.什么是feign
- Feign是SpringCloud Netflix 组件中的一个轻量级RESTful的HTTP服务客户端,实现了负载均衡和Rest调用的开源框架,封装了Ribbon和RestTemplate,实现了WebService的面向接口编程,进一步降低了项目的耦合度。
- Feign内置了Ribbon,用来做客户端负载均衡调用服务注册中心的服务。
- Feign本身并不支持SpringMVC的注解,他有一套自己的注解,为了更方便的使用,Spring Cloud孵化了OpenFeign。
- Feign是一种声明式、模板化的HTTP客户端(仅在Consumer中使用)
- Feign支持的注解和用法请参考官方文档
- Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用注册中心的服务
2.Feign解决了什么问题
- Feign旨在使编写JAVA HTTP 客户端变得更容易,Feign简化了Rest Template代码,实现了Ribbon负载均衡,是代码变得更加简洁,也少了客户端调用的代码,使用Feign实现负载均衡是首选方案。只需要你创建一个接口,然后在上面添加注解即可。
- Feign是声明式服务调用组件,其核心就是:像调用本地方法一样调用远程方法,无感知远程HTTP请求。
- 它解决了让开发者调用远程接口就跟调用本地方法一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个HTTP请求,无需关注与远程的交互细节,更无需关注分布式环境开发。
- 它像Dubbo一样 ,Consumer直接调用Provider接口方法,而不需要通过常规的Http Client狗仔请求再解析返回数据。
3.Feign vs Open Feign
- OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping、@Pathvariable等等。
- OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用服务。
4.Feign入门案例
Feign的使用主要分为一下几个步骤:
- 服务消费者添加Feign依赖;
- 创建业务层接口,添加@FeignClient注解声明要调用的服务;
- 业务层抽象方法使用SpringMVC注解来配置服务地址及参数
- 启动类添加@EnableFeignClient注解激活Feign组件
5.Feign实例项目
先创建一个空项目
然后分别创建两个eureka注册中心
eureka-server
添加eureka依赖
1
2
3
4
5<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>3.0.1</version>
</dependency>编写配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14spring:
application:
name: eureka-server #应用名称
server:
port: 8761
eureka:
instance:
hostname: eureka01 #主机名
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
#设置服务注册中心地址,指向另一个注册中心
service-url: #注册中心对外暴露的注册地址
defaultZone: http://localhost:8762/eureka/
eureka-server02
同上,配置文件修改一下端口和注册地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14spring:
application:
name: eureka-server #应用名称
server:
port: 8762
eureka:
instance:
hostname: eureka02 #主机名
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
#设置服务注册中心地址,指向另一个注册中心
service-url: #注册中心对外暴露的注册地址
defaultZone: http://localhost:8761/eureka/
在创建两个服务端并注册到注册中心去,待会供我们的消费者使用
service-provider
添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>3.0.1</version>
</dependency>编写配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13spring:
application:
name: service-provider
server:
port: 7070
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:8761/eureka/,
http://localhost:8762/eureka/ #将其分别注册进两个注册中心中编写bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package com.feign.serviceprovider.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Integer id;
private String productName;
private Integer productNum;
private Double productPrice;
}编写service
接口
1
2
3
4
5
6
7
8
9package com.feign.serviceprovider.service;
import com.feign.serviceprovider.bean.Product;
import java.util.List;
public interface ProductService {
List<Product> getPro();
}impl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package com.feign.serviceprovider.service.impl;
import com.feign.serviceprovider.bean.Product;
import com.feign.serviceprovider.service.ProductService;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
public class ProductServiceImpl implements ProductService {
@Override
public List<Product> getPro() {
return Arrays.asList(new Product(1,"华为手机-7070",2,5000.00),
new Product(2,"苹果手机",3,6900.00),
new Product(3,"一加手机",2,4999.99)
);
}
}
编写控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package com.feign.serviceprovider.controller;
import com.feign.serviceprovider.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProductController {
@Autowired
ProductService productService;
@GetMapping("/getPro")
public Object getPro(){
return productService.getPro();
}
}
service-provider02
- 同上除了配置文件端口和实现类输出的信息
最后创建我们的消费者远程调用服务端
service-consumer
添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>3.0.1</version>
</dependency>编写配置文件
1
2
3
4
5
6
7
8
9
10
11spring:
application:
name: service-provider
server:
port: 9090
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
register-with-eureka: false #是否将自己注册到注册中心,默认为true
registry-fetch-interval-seconds: 10 #表示Eureka Client间隔多久去服务器拉取注册信息,默认为30秒编写bean
Order
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package com.feign.serviceconsumer.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order implements Serializable {
private Integer id;
private String orderNo;
private String orderAddress;
private Double totalPrice;
private List<Product> productList;
}Product
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Integer id;
private String productName;
private Integer productNum;
private Double productPrice;
}
service层
Product
接口
1
2
3
4
5
6
7
8
9
10
11
12
13package com.feign.serviceconsumer.service;
import com.feign.serviceconsumer.bean.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@FeignClient("SERVICE-PROVIDER") //绑定在注册中心名为SERVICE-PROVIDER的服务端
public interface ProductService {
@GetMapping("/getPro")
List<Product> getPro();
}
Order
接口
1
2
3
4
5
6
7
8package com.feign.serviceconsumer.service;
import com.feign.serviceconsumer.bean.Order;
public interface OrderService {
Order selectOrderById(Integer id);
}impl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package com.feign.serviceconsumer.service.impl;
import com.feign.serviceconsumer.bean.Order;
import com.feign.serviceconsumer.service.OrderService;
import com.feign.serviceconsumer.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
ProductService productService;
@Override
public Order selectOrderById(Integer id) {
return new Order(id,"order-001","中国",2222.00,productService.getPro());
}
}
controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package com.feign.serviceconsumer.controller;
import com.feign.serviceconsumer.bean.Order;
import com.feign.serviceconsumer.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/getOrder/{id}")
public Order getOrder(@PathVariable("id") Integer id){
return orderService.selectOrderById(id);
}
}springboot启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.feign.serviceconsumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients //告诉框架扫描所有通过注解@FeignClient定义的feign客户端
public class ServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceConsumerApplication.class, args);
}
}
效果图:分别为第一次访问和第二次访问,feign自带负载均衡配置项,会依次访问两个服务端
6.feign负载均衡的配置
feign负载均衡默认是轮询的,目前feign3.0.1好像配置没有,里面没有内置ribbonjar包
1
2
3SERVICE-PROVIDER:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
- com.netflix.loadbalancer.RandomRule #配置规则 随机,几个提供者间随机访问
- com.netflix.loadbalancer.RoundRobinRule #配置规则 轮询,轮流访问
- com.netflix.loadbalancer.RetryRule #配置规则 重试,在一段时间内通过RoundRobinRule选择服务实例,一段时间内没有选择出服务则线程终止
- com.netflix.loadbalancer.WeightedResponseTimeRule #配置规则 响应时间权重,根据平均响应时间来计算权重
- com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略
7.feign传参
GET 使用@PathVariable注解或@RequestParam注解接收请求参数
实例
在服务提供者service-provider的接口添加一个根据id查询的方法
1
2
3
4
5
6
7
8
9
10package com.feign.serviceprovider.service;
import com.feign.serviceprovider.bean.Product;
import java.util.List;
public interface ProductService {
List<Product> getPro();
Product getProById(Integer id);
}在impl实现这个接口
1
2
3
4@Override
public Product getProById(Integer id) {
return new Product(id,"冰箱",2,3000.00);
}在控制器层映射这个方法
1
2
3
4@GetMapping("/getProById/{id}")
public Object getProById(@PathVariable("id") Integer id){
return productService.getProById(id);
}在消费者端的接口类调用这个方法
1
2
3
4
5
6
7
8import java.util.List;
@FeignClient("SERVICE-PROVIDER")
public interface ProductService {
@GetMapping("/getPro")
List<Product> getPro();
@GetMapping("/getProById/{id}")
Product getProById(@PathVariable("id") Integer id);
}修改Order实现类,返回调用服务提供者的根据id查询方法
1
2
3
4
5@Override
public Order selectOrderById(Integer id) {
return new Order(id,"order-001","中国",2222.00,
Arrays.asList(productService.getProById(5)));
}成功调用服务提供者的根据id查询方法,效果图
post请求 使用@RequestBody注解接受请求参数
实例
在服务提供者service-provider的接口添加两个方法
1
2
3Product getProByIdPost(Integer id);
Map<Object,Object> createPro(Product product);在impl实现这个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@Override
public Product getProByIdPost(Integer id) {
return new Product(id,"洗衣机",5,3999.00);
}
@Override
public Map<Object, Object> createPro(Product product) {
System.out.println(product);
Map<Object,Object> map = new HashMap<>();
if(product == null){
map.put("code",400);
map.put("msg","添加失败");
return map;
}
map.put("code",200);
map.put("msg","添加成功");
return map;
}在控制器层映射这个方法
1
2
3
4
5
6
7
8
9@PostMapping("/getProByIdPost")
public Object getProByIdPost(@RequestBody Integer id){
return productService.getProByIdPost(id);
}
@PostMapping("save")
//注意这里要加上@RequestBody注解,不然实现类那里获取不到product的值
public Map<Object,Object> save(@RequestBody Product product){
return productService.createPro(product);
}在消费者端的接口类调用这个方法
1
2
3
4
5
6
7
8
9
10
11@FeignClient("SERVICE-PROVIDER")
public interface ProductService {
@GetMapping("/getPro")
List<Product> getPro();
@GetMapping("/getProById/{id}")
Product getProById(@PathVariable("id") Integer id);
@PostMapping("/getProByIdPost")
Product getProByIdPost(Integer id);
@PostMapping("save")
Map<Object,Object> createPro(Product product);
}控制器层映射这两个方法
1
2
3
4
5
6
7
8
9@PostMapping(value = "/getProPost",produces={"application/json;charset=UTF-8"})
public Product getProPost(Integer id){
return productService.getProByIdPost(id);
}
@PostMapping("/save")
public Map<Object,Object> save(Product product){
System.out.println(product);
return productService.createPro(product);
}成功调用服务提供者的方法,效果图:
8.Feign性能优化
Gzip压缩
介绍:gzip是一种数据格式,采用deflate算法压缩数据;gzip是一种流行的文件压缩算法,应用十分广泛,尤其是在Linux平台。
gzip能力:当gzip压缩一个纯文本文件时,效果是非常明显的,大约可以减少70%以上的文件大小。
gzip作用:网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。
实例:百度用的就是gzip压缩
Accept-Encoding向服务端表示客户端支持的压缩格式,如果不发送该消息头,服务端默认是不会压缩的
服务端在收到请求后,如果发现请求头中含有Accept-Encoding字段,并且支持该类型压缩,就会对响应报文压缩之后返回给客户端,并且携带Content-Encoding:gzip消息头,表示响应报文是根据该格式进行压缩的。
客户端接收到请求后,先判断是否有Content-Encoding消息头,如果有,按照格式解压报文。否则按正常报文处理
全局配置 –目前无效,原因不详 –添加这个mime-types: “application/xhtml+xml”就好了
1
2
3
4
5
6
7
8server:
port: 9090
compression:
enabled: true #是否开启压缩
#mime-types:配置压缩支持的类型。默认支持"text/html",
"text/xml", "text/plain", "text/css", "text/javascript",
"application/javascript", "application/json", "application/xml"
# mime-types:局部配置
- 百度一下
HTTP连接池
将Feign的Http客户端工具修改为HttpClient
导入依赖
1
2
3
4
5<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>11.1</version>
</dependency>
使用了HttpClient客户端之后,我们还可以通过Get请求传递对象参数。
服务提供者新增方法
接口
1
Product savePro(Product product);
impl
1
2
3
4
5@Override
public Product savePro(Product product) {
System.out.println(product);
return product;
}控制器
1
2
3
4@GetMapping("savePro")
public Product savePro(@RequestBody Product product){
return product;
}
消费者远程调用
接口
1
2@GetMapping("/savePro")
Product savePro(Product product);控制器
1
2
3
4@GetMapping("/savePro")
public Product savePro(Product product){
return product;
}
效果图:
状态查看 –过时
请求超时 –目前Feign失效,默认1秒超时都不报错
全局
1
2
3ribbon:
ConnectTimeout: 2000
ReadTimeout: 2000局部
- Post title:OpenFeign学习记录
- Post author:周瑜
- Create time:2021-03-16 16:48:18
- Post link:https://xinblog.github.io/2021/03/16/OpenFeign-md/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.