前言…
思考: 使用RestTemplate+ribbon已经可以完成微服务间服务间的调用,且能实现负载均衡效果,为什么还要使用feign?
RestTemplate+ribbon存在的问题:
1.ribbon已停止更新
2.每次调用服务都需要写这些代码,存在大量的代码冗余
3.服务地址如果修改,维护成本增高
4.使用时不够灵活
基于以上情况,微服务调用之 OpenFeign组件便出来了!下边一起来进入学习模式吧!
各位客官,里边请儿!
其实一开始呢,是没有OpenFeign 呢,最早的是Feign,Feign是Spring Cloud组件中的一个轻量级RESTful
的HTTP服务客户端
,其内置了ribbon,默认实现了负载均衡的效果,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。…反正优点多多!
但是!Feign 不支持SpringMVC注解 ,它有一套自己的注解!用过springMVC注解的都知道(废话,都微服务了还没用过mvc注解?@RequestMapping、@RequestBody 、@ResponseBody、@PathVariable、@RequestParam 等等),如果这些没法用的话,那么对开发人员来说,天大噩耗,又得码多少代码才能实现原有功能哦!所以呢,这玩意肯定会被废弃淘汰的呀,所以呀 OpenFeign
闪亮登场!
OpenFeign 组件
- 官网地址: https://cloud.spring.io/spring-cloud-openfeign/reference/html
- OpenFeign 在Feign的基础上添加了springmvc注解的支持。使得微服务间负载均衡以及调用如同吃饭喝水般简单快捷又方便!用过的,都说好!
简单地聊了聊feign 、OpenFeign,那么咱们来系统梳理下 Ribbon、feign、OpenFeign 的区别吧!
Ribbon 是 Netflix开源的基于HTTP和TCP等协议负载均衡组件
Ribbon 可以用来做客户端负载均衡,调用注册中心的服务
Ribbon的使用需要代码里手动调用目标服务,存在大量的代码冗余,操作死板不灵活
Feign是Spring Cloud组件中的一个轻量级RESTful
的HTTP服务客户端
Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。
Feign本身不支持Spring MVC的注解 戳我进官方文档,它有一套自己的注解
OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping
等等。
OpenFeign的@FeignClient
可以解析SpringMVC的@RequestMapping
注解下的接口,
并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
这三者呢,我仅用过Ribbon+RestTemplate 、OpenFeign ,至于 feign的话,其仅仅只能算是过渡产物吧,实际一个注册中心下,公司里基本都会使用OpenFeign了!
下边呢,我们开始进入系统的学习吧!
OpenFeign的使用非常简单哈,可以快速上手,完成微服务间调用!且在最开始的注册中心篇其实已经初略的使用过了!
总结来讲的话,就四个步骤吧!
首先我们来回顾一下,服务于服务间相对而言来讲的概念!
服务提供者:
服务提供者实际也是一个独立的微服务,提供者即被调用者,一个服务的接口 依赖于另一个服务接口提供的返回值,那么被依赖的服务即为服务提供者!
服务消费者:
服务消费者实际也是一个独立的微服务,消费者即调用者,请求调用的发起者服务!
例如:现在有用户中心服务模块、登录注册服务模块
我在登录时候是不是用户需要请求登录注册服务模块接口,登录接口中呢,是不是需要去用户中心服务判断用户正确性,然后响应给用户是否登录成功?
此时啊,登录注册服务便是依赖于用户中心模块,此时,登录注册为服务消费者,用户中心模块为提供者!
至于某些服务是否为提供者还是消费者,那得看业务链路了!没有一定死的!
正如同某同志追女神备胎了n年,发现原来女神又是另一位男同学的舔狗! 站的角度不同罢了!
正式开搞,搭建两个商品服务一个订单服务
因为前边演示Ribbon+RestTemplate
做了一些微服务,所以我们copy 改一改即可,暂时将demo-order
作为服务消费者去消费demo-product
吧!
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
导入依赖后,OpenFeign默认是关闭的,必须手动开启(注解开启)
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SpringcloudOpenfeignHystrixOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudOpenfeignHystrixOrderApplication.class, args);
}
}
首先,我们来看一下,服务提供者 商品服务demo-product
向我们提供了什么接口
ok,订单服务 demo-order中编写调用商品服务demo-product
的接口
重点:
@FeignClient(value = "demo-product")
public interface ProductFeign {
@GetMapping("/product/demo/{id}")
Result findProduct(@PathVariable("id") Integer id);
}
order 向外部暴露的接口
@RestController
@RequestMapping("/order")
public class OrderController {
/**
* openfeign客户端对象 调用商品服务
*/
private final ProductFeign productFeign;
@Autowired
public OrderController(ProductFeign productFeign) {
this.productFeign = productFeign;
}
@GetMapping("/find/product/{id}")
public Result findProduct(@PathVariable("id") Integer id) {
return productFeign.findProduct(id);
}
}
这就编写完了,我们order 服务可以直接调用 Product 服务了,且前边也说了OpenFeign是基于Ribbon 的,所以呢,其也开启了负载功能!
那么,我们访问9002 /order/find/product/{id}
接口 测试一下
嗯!商品服务调用成功!并且哈ProductFeign客户端是可以复用的哈,即当前订单服务哪里需要他,只要类是交由spring管理的 都可以将ProductFeign客户端进行注入并使用
Feign编写简单总结
1.编写接口 接口上使用 @FeignClient
属性value指明被调用服务名
2.按照服务提供者接口 编写入参 返回值 url即可
OpenFeign 组件呢,其功能就是微服务间调用以及负载,那么我们首先来理一理微服务间调用!
实际就是一个服务的接口 依赖与其他服务接口响应,凑在一起成为一次完整的请求链路
那么在实际项目中哈,传参可能多种多样!使用OpenFeign 有没有什么注意的点呢?当然是有哈,下边我们就来演示与讲解!
上边的例子是使用的@PathVariable 来完成请求,可能有的老项目呢,不会遵从RestFul风格
,仅仅就是普通的传参 例如/order/find/product?name=zs&age=12
使用@PathVariable @GetMapping 指明Url
服务提供者提供接口如下
@RequestMapping("/demo")
public Result getProductByParam(Integer id,String name) {
log.info("调用成功" + port);
return Result.success("商品获取成功:当前商品Id为:" + id +"商品名为:"+name+ "当前调用商品服务IP 端口" + ip + ":" + port);
}
那么消费者使用OpenFeign如何调用呢?OpenFeign这样写吗?
我们order创建接口外部调用测试一下
我们在两个商品服务接口中打上断点以及在Order 调用那里打上断点看一下!
成功进入order 且调用了ProductFeign 客户端,但是! 我们的product服务却没有接收到请求!
order 服务在调用product那块 报了空指针! 没有获取到参数
解决措施:参数打上@RequestParam
注解
Feign客户端、服务提供者接口、参数打上@RequestParam
注解
再次请求,发现服务提供者断点来了,且拿到了参数!
但是啊!一波平了一波又起了啊!
断点太久,发现我们的order服务(服务消费者)居然直接抛出了异常!看异常意思,接口读取超时.但是呢,事实上请求的确是来到了我们的服务提供者product 啊,仅仅是因为断点了一点时间嘛!这也太扯了是吧!
在实际生产环境或者开发环境中,或许因为网络分区原因,或许因为接口本身逻辑复杂耗时较长,如果这时候,OpenFeign在调用服务的时候直接返回读取超时肯定是不合理的!所以,我们要改OpenFeign超时时间!
OpenFeign超时说明
默认情况下,openFiegn在进行服务调用时,要求服务提供方处理业务逻辑时间必须在1S内返回,如果超过1S没有返回则OpenFeign会直接报错,不会等待服务执行,但是往往在处理复杂业务逻辑是可能会超过1S,因此需要修改OpenFeign的默认服务调用超时时间。
调用超时会出现如下错误:
OpenFeign超时时间设置
yml设置 在服务消费者端设置OpenFeign 超时时间
feign:
client:
config:
default:
#连接超时时间 单位为毫秒
connectTimeout: 5000
#读取超时时间 单位为毫秒
readTimeout: 5000
测试模拟 将我们的两个服务提供者 product 接口 一个睡眠6秒 一个睡眠4秒
测试:
端口为9000的product服务接口调用成功!说明我们的OpenFeign超时时间设置是有效的了!
但是呢,每个微服务处理的业务不同,那么消费者端的OpenFeign应该也随着服务提供者业务来设置超时时间呀!这个呢,Openfeign已经实现了
我们上方那种设置呢,就是针对的全局设置OpenFeign超时时间
OpenFeign全局超时时间 此设置只要是当前消费者调用到Feign客户端均以XX秒(我这里设置5000则 5秒)超时
feign:
client:
config:
default:
#连接超时时间 单位为毫秒
connectTimeout: 5000
#读取超时时间 单位为毫秒
readTimeout: 5000
OpenFeign针对不同服务设置不同超时时间
其实呢,很简单将上边的 default 改为服务提供者具体名字即可例如:demo-product
feign:
client:
config:
#服务提供者名字 设置了此后,那么当前项目调用 demo-product 服务超时时间为xx(我这里为5秒)
demo-product:
#连接超时时间
connectTimeout: 5000
#读取超时时间
readTimeout: 5000
可能有小伙子比较俏皮,相出指明一个或多个具体服务的超时时间,其余统一再设置超时时间可不可以呢
例如:yml配置改成这样!
feign:
client:
config:
#demo-product 服务超时时间设置
demo-product:
#连接超时时间
connectTimeout: 5000
#读取超时时间
readTimeout: 5000
#全部服务提供者超时时间设置
default:
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000
那么,此时我们的Feign客户端访问 Product时候是以全局的3秒为准还是以自己的5秒为准呢?测试一下!
睡6秒的依然超时
睡4秒的,依然能正确获取接口响应
那么说明,指定了具体服务超时时间后,OpenFeign 则以服务名设置的超时时间为准,否则则使用全局 default
OK 碰到问题就先解决嘛,前边是GET 方式调用服务传递参数来着!接下来,看下POST
往往在服务调用时我们需要详细展示feign的日志,默认feign在调用是并不是最详细日志输出,因此在调试程序时应该开启feign的详细日志展示。
feign对日志的处理非常灵活可为每个feign客户端指定日志记录策略,并且呢feign日志的打印只会DEBUG级别做出响应。
我们可以为feign客户端配置各自的logger.level对象,并且设置不同的日志内容打印级别
- NONE #不记录任何日志
- BASIC #仅仅记录请求方法,url,响应状态代码及执行时间
- HEADERS #记录Basic级别的基础上,记录请求和响应的header
- FULL #记录请求和响应的header,body和元数据
怎么设置呢?yml配置文件设置即可
feign.client.config.demo-product.loggerLevel=full #开启指定服务(我这里则为demo-product)日志展示
#feign.client.config.default.loggerLevel=full #全局开启服务日志展示(所有的配置)
logging.level.com.leilei.demo=debug #日志扫包(feign客户端需在此包下),日志级别必须是debug级别
指定具体服务设置feign日志级别
feign:
client:
config:
#demo-product 服务设置
demo-product:
#显示调用日志
loggerLevel: full
#连接超时时间
connectTimeout: 5000
#读取超时时间
readTimeout: 5000
全局服务设置feign日志级别
feign:
client:
config:
#全部服务提供者设置
default:
#显示调用日志
loggerLevel: full
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000
日志扫包
logging:
level:
#指定包路径 feign客户端必须在此包路径之下
com.leilei.demo: debug
与设置超时时间一样,如果指定服务设置日志级别后,再设置全局,那么指定服务生效的依然是其针对具体服务的配置
例如: demo-product 设置了BASIC 全局设置了 FUll ,那么order服务调用 demo-product呢,feign只会是输出其BASIC级别的日志信息
测试看下效果:
未设置前:
请求访问http://localhost:9002/order/find/product/1
啥也没有
设置后
可以看到,我们能够详细的看到feign的调用信息了!
首先,还是得在服务提供者编写对应接口啊!
@PostMapping
public Result addProduct(Product product) {
log.info("添加商品接口调用成功" + port);
product.setId(new Random().nextInt(20));
return Result.success("商品添加成功:当前商品服务Ip 端口为"+ ip + ":" + port,product);
}
消费者端调用
这里服务虽然为order、 product 但是大家不要被这名字迷惑了啊,我演示的内容仅仅是OpenFeign 微服务间调用而已啊!逻辑别当真啊!
你只需要当它们两为不同业务的微服务接口即可!
你只需要当它们两为不同业务的微服务接口即可!
你只需要当它们两为不同业务的微服务接口即可!
order外暴接口
ProductFeign客户端调用 product服务
测试! 熟悉的错误
之前,是个普通参数,我们用@RequestParam
注解 ,现在是个对象,我们又该怎么做呢?
答:使用大家耳熟能详的注解@RequestBody
…
由此 接口 改造!
再次测试!
OK 接受到了参数!
到了这里,GET、POST 都演示了这种错误!可能有小伙伴纳闷了?为什么要讲这些呢?
那是因为:全是血和泪啊!!当初公司没有遵循RestFul规范,而且我入职的时候,前端后端已经是老相识了,传值很随意!最开始上手微服务时候,传参就是这么传的啊啊啊啊啊!那真的是一步一个脚印一步一个脚印啊!这些报错,都是走过的坑啊!
put 一般用作为表单修改,上述的错误呢,已经是演示腻了,所以呢,我们直接来个规范传参即可!
服务提供者提供的接口为:
@PutMapping
public Result editProduct(@RequestBody Product product) {
log.info("修改商品接口调用成功" + port);
return Result.success("商品修改成功:当前商品服务Ip 端口为"+ ip + ":" + port,product);
}
消费者入口
@PutMapping("/place/order")
public Result editProduct(@RequestBody Product product) {
return productFeign.editProduct(product);
}
消费者端 feign客户端调用提供者
@PutMapping("/product")
Result editProduct(@RequestBody Product product);
测试:
提供者端接口
@DeleteMapping("/{id}")
public Result deleteProduct(@PathVariable("id") Integer id) {
return Result.success("商品删除成功:当前商品服务Ip 端口为" + ip + ":" + port, "当前删除商品ID为:"+id);
}
消费者端Feign 调用提供者接口
@DeleteMapping("/product/{id}")
Result editProduct(@PathVariable("id") Integer id);
测试:
那么 CRUD方式都走完了,可能小伙伴还有还有一个疑惑!
我服务提供者端设置了一个公共Url 前缀 /product
那么我在Feign 调用接口是否可以这样写一个统一的呢?
例如这样?
改为下方样式!
测试下:
?????????居然可行?我记得以前使用的时候是不可以这样的啊,会报错的呀??难道改了??这一点我还得下来查查了!但是个人建议还是不要@RequestMapping写在Feign接口类上边了!
OK,OpenFeign 组件的基本使用差不多就这些了!
我们来说下使用注意事项以及重点!加深一下记忆!
使用步骤
1.引入依赖
2.直接开启功能
3.feign客户端接口编写 调用对应服务提供者
注意事项以及重点!!!敲黑板了!!!
1.消费者端注意设置OpenFeign超时时间
2.消费者端参数传递注意事项
尽力少用或不用 @RequestParam
url请求不雅观
为了规范,推荐使用 @PathVariable
无论是get、delete 还是put、post 请求时额外需要url参数时都不建议使用 @RequestParam
服务提供方和调用方对象参数一定要使用 @RequestBody
我们前边讲过 Openfeign 其底层是基于Ribbon的 ,并且 使用OpenFeign 默认的是轮询的负载均衡策略,涉及到项目或者服务器配置,可能会对负载策略做一些更改
我们可以在消费者服务配置Yml中更改负载策略!
#对demo-product 服务设置负载均衡策略为随机
demo-product:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #配置规则 随机
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #配置规则 随机
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #配置规则 轮询
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule #配置规则 重试
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule #配置规则 响应时间权重
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略
# ConnectTimeout: 500 #请求连接超时时间
# ReadTimeout: 1000 #请求处理的超时时间
# OkToRetryOnAllOperations: true #对所有请求都进行重试
# MaxAutoRetriesNextServer: 2 #切换实例的重试次数
# MaxAutoRetries: 1 #对当前实例的重试次数
在某一些业务场景,我们可能需要微服务间传递请求头信息 例如(token) 方便我们快速拿到当前操作对象进行业务处理!
默认情况下呢,OpenFeign 是不会帮我们传递任何请求头信息的
详细请看!
我们首先来改造服务提供者接口,尝试打印请求头信息!
@RequestMapping("/demo/{id}")
public Result getProduct(HttpServletRequest request, @PathVariable("id") Integer id) {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return Result.success("商品获取成功:当前商品Id为:" + id + "当前调用商品服务IP 端口" + ip + ":" + port, "请求头信息:" + map);
}
直接发送请求看看!
那么这个情况怎么办呢?
我们可以在服务消费者复写Feign配置拿到我们的请求头,将请求头包裹在其请求对象中,传递给服务提供者!
直接上代码
package com.leilei.demo;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author lei
* @version 1.0
* @date 2020/8/30 22:28
* @desc feign 请求头传递
*/
@Configuration
public class FeignInterceptor implements RequestInterceptor {
/**
* 复写feign请求对象
* @param requestTemplate
*/
@Override
public void apply(RequestTemplate requestTemplate) {
//获取请求头
Map<String,String> headers = getHeaders(getHttpServletRequest());
for(String headerName : headers.keySet()){
requestTemplate.header(headerName, getHeaders(getHttpServletRequest()).get(headerName));
}
}
//获取请求对象
private HttpServletRequest getHttpServletRequest() {
try {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//拿到请求头信息
private Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}
}
ok,配置也搞好了,咱们重启服务测试一波!
大功告成,打完收工!OpenFeign 的一些基础使用就是这了!
附上项目源码:OpenFeign使用
前边,我们所有的演示均是服务提供者正常为我们提供服务的情况,那么万一服务提供者down机了呢?一段段时间无法提供服务,或者服务提供者业务逻辑执行报错了呢?我们消费者 try catch 返回错误信息吗?我们的消费者还使用OpenFeign 去调用提供者是不是有些不合适呢?
例如:我们这里吧所有服务提供者全部关闭
这个时候,我们仍然访问消费者order 由消费者去调用product(提供者看看什么情况!)
废话,按理说,服务提供者我都关完了,能不报错能访问成功还有个鬼了!
这个时候,无论我访问order多少次他都会直接请求我们的product (虽然Product)已经不在了,并且报错信息不友好(虽然可以全局异常,但用户体验仍是不好的)这个时候呢,我们想,要是有一个组件能知道某服务挂掉了,多次尝试不行后一段时间直接返回给拖底数据给用户,然后呢,服务提供者恢复了以后,OpenFeign又正常请求服务提供者,这既提升了用户体验,又避免了没必要的请求保证了消费者服务的稳定(不然一直打印错误日志,不利于以后排查)
上边说的这个组件是有的,叫什么呢?叫**Hystrix
**
下一篇!微服务守护者 -Hystrix