Feign
集成工具(功能整合)
- 远程调用:声明式客户端
- ribbon 负载均衡和重试
- hystrix 降级和熔断
feign 声明式客户端接口
微服务应用中,ribbon 和 hystrix 总是同时出现,feign 整合了两者,并提供了声明式消费者客户端
- 用 feign 代替 hystrix+ribbon
只需要声明一个抽象接口,就可以通过接口做远程调用,不需要再使用 RestTemplate 来调用
// 调用远程的商品服务,获取订单的商品列表
// 通过注解,配置:
// 1. 调用哪个服务
// 2. 调用服务的哪个路径
// 3. 向路径提交什么参数数据
@FeignClient(name="item-service")
public interface ItemClient {
@GetMapping("/{orderId}")
JsonResult> getItems(@PathVariable String orderId);
}
在这里使用 @GetMapping("/{orderId}"), 指定的是向远程服务调用的路径
新建 sp09-feign 项目
pom.xml
- 需要添加 sp01-commons 依赖
application.yml
spring:
application:
name: feign
server:
port: 3001
eureka:
client:
service-url:
defaultZone: http://eureka1:2001/eureka, http://eureka2:2002/eureka
主程序添加 @EnableDiscoveryClient
和 @EnableFeignClients
package cn.tedu.sp09;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class Sp09FeignApplication {
public static void main(String[] args) {
SpringApplication.run(Sp09FeignApplication.class, args);
}
}
feign 声明式客户端
feign 利用了熟悉的 spring mvc 注解来对接口方法进行设置,降低了我们的学习成本。
通过这些设置,feign可以拼接后台服务的访问路径和提交的参数
例如:
@GetMapping("/{userId}/score")
JsonResult addScore(@PathVariable Integer userId, @RequestParam Integer score);
当这样调用该方法:
service.addScore(7, 100);
那么 feign 会向服务器发送请求:
http://用户微服务/7/score?score=100
- 注意:如果 score 参数名与变量名不同,需要添加参数名设置:
@GetMapping("/{userId}/score")
JsonResult addScore(@PathVariable Integer userId, @RequestParam("score") Integer s
ItemClient
package cn.tedu.sp09.feign;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.web.util.JsonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
//根据服务id,从注册表得到主机地址
@FeignClient(name = "item-service")
public interface ItemClient {//封装了RestTemplate
@GetMapping("{orderId}")//(反向)将请求拼接发送
JsonResult> getItems(@PathVariable String orderId);
@PostMapping("/decreaseNumber")//(反向)将请求拼接发送
JsonResult> decreaseNumber(@RequestBody List- items);
}
UserClient
package cn.tedu.sp09.feign;
import cn.tedu.sp01.pojo.User;
import cn.tedu.web.util.JsonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
//根据服务id,从注册表得到主机地址
@FeignClient(name = "user-service")
public interface UserClient {//封装了RestTemplate
@GetMapping("{userId}")//(反向)将请求拼接发送
JsonResult getUser(@PathVariable Integer userId);
//....../8/score?score=1000
@GetMapping("/{userId}/score")//(反向)将请求拼接发送
JsonResult> addScore(@PathVariable Integer userId,
@RequestParam Integer score);//@RequestParam 不能省略
}
OrderClient
package cn.tedu.sp09.feign;
import cn.tedu.sp01.pojo.Order;
import cn.tedu.web.util.JsonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
//根据服务id,从注册表得到主机地址
@FeignClient(name = "order-service")
public interface OrderClient {//封装了RestTemplate
@GetMapping("{orderId}")//(反向)将请求拼接发送
JsonResult getOrder(@PathVariable String orderId);
@GetMapping("/")//(反向)将请求拼接发送
JsonResult> addOrder();
}
FeignController
package cn.tedu.sp09.controller;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.sp01.pojo.Order;
import cn.tedu.sp01.pojo.User;
import cn.tedu.sp09.feign.ItemClient;
import cn.tedu.sp09.feign.OrderClient;
import cn.tedu.sp09.feign.UserClient;
import cn.tedu.web.util.JsonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@Slf4j
public class FeignController {
@Autowired
private ItemClient itemClient;
@Autowired
private UserClient userClient;
@Autowired
private OrderClient orderClient;
//-----------item-service
@GetMapping("/item-service/{orderId}")
public JsonResult> getItems(@PathVariable String orderId){
return itemClient.getItems(orderId);
}
@PostMapping("/item-service/decreaseNumber")
public JsonResult> decreaseNumber(@RequestBody List- items){
return itemClient.decreaseNumber(items);
}
//-----------user-service
@GetMapping("/user-service/{userId}")
public JsonResult
getUser(@PathVariable Integer userId){
return userClient.getUser(userId);
}
@GetMapping("/user-service/{userId}/score")
public JsonResult> addScore(@PathVariable Integer userId,Integer score){
return userClient.addScore(userId, score);
}
//-----------order-service
@GetMapping("/order-service/{orderId}")
public JsonResult getOrder(@PathVariable String orderId){
return orderClient.getOrder(orderId);
}
@GetMapping("/order-service")
public JsonResult> addOrder(){
return orderClient.addOrder();
}
}
调用流程
启动服务,并访问测试
- http://eureka1:2001
- http://localhost:3001/item-service/35
http://localhost:3001/item-service/decreaseNumber
使用postman,POST发送以下格式数据:[{"id":1, "name":"abc", "number":23},{"id":2, "name":"def", "number":11}]
- http://localhost:3001/user-service/7
- http://localhost:3001/user-service/7/score?score=100
- http://localhost:3001/order-service/123abc
- http://localhost:3001/order-service/
Feign 集成 Ribbon 负载均衡和重试
- 无需额外配置,feign 默认已启用了 ribbon 负载均衡和重试机制。可以通过配置对参数进行调整
重试的默认配置参数:
ConnectTimeout=1000
ReadTimeout=1000
MaxAutoRetries=0
MaxAutoRetriesNextServer=1
application.yml 配置 ribbon 超时和重试
ribbon.xxx
全局配置item-service.ribbon.xxx
对特定服务实例的配置
spring:
application:
name: feign
server:
port: 3001
eureka:
client:
service-url:
defaultZone: http://eureka1:2001/eureka,http://eureka2:2002/eureka
# 调整 ribbon 的重试次数
# 针对所有服务的通用配置
ribbon:
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
ConnectTimeout: 1000
ReadTimeout: 500
#只针对 item-service这一个服务有效,对其它服务不应用这个配置
item-service:
ribbon:
MaxAutoRetries: 2
启动服务,访问测试
http://localhost:3001/item-service/35
Feign 集成 Hystrix降级和熔断
Feign默认不启用Hystrix,使用feign时不推荐启用Hystrix
启用Hystrix基础配置:
- hystrix起步依赖
- yml中配置启用hystrix
- 启动类添加注解
@EnableCircuitBreaker
application.yml 添加配置
feign:
hystrix:
enabled: true
添加配置,暂时减小降级超时时间,以便后续对降级进行测试
......
feign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 500
feign + hystrix 降级
feign 远程接口中指定降级类
ItemFeignService
...
@FeignClient(name="item-service", fallback = ItemFeignServiceFB.class)
public interface ItemFeignService {
...
UserFeignService
...
@FeignClient(name="user-service", fallback = UserFeignServiceFB.class)
public interface UserFeignService {
...
OrderFeignService
...
@FeignClient(name="order-service",fallback = OrderFeignServiceFB.class)
public interface OrderFeignService {
...
降级类
降级类需要实现声明式客户端接口,在实现的抽象方法中添加降级代码,
降级类需要添加 @Component
注解
ItemFeignServiceFB
package cn.tedu.sp09.service;
import java.util.List;
import org.springframework.stereotype.Component;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.web.util.JsonResult;
@Component
public class ItemFeignServiceFB implements ItemFeignService {
@Override
public JsonResult> getItems(String orderId) {
return JsonResult.err("无法获取订单商品列表");
}
@Override
public JsonResult decreaseNumber(List- items) {
return JsonResult.err("无法修改商品库存");
}
}
UserFeignServiceFB
package cn.tedu.sp09.service;
import org.springframework.stereotype.Component;
import cn.tedu.sp01.pojo.User;
import cn.tedu.web.util.JsonResult;
@Component
public class UserFeignServiceFB implements UserFeignService {
@Override
public JsonResult getUser(Integer userId) {
return JsonResult.err("无法获取用户信息");
}
@Override
public JsonResult addScore(Integer userId, Integer score) {
return JsonResult.err("无法增加用户积分");
}
}
OrderFeignServiceFB
package cn.tedu.sp09.service;
import org.springframework.stereotype.Component;
import cn.tedu.sp01.pojo.Order;
import cn.tedu.web.util.JsonResult;
@Component
public class OrderFeignServiceFB implements OrderFeignService {
@Override
public JsonResult getOrder(String orderId) {
return JsonResult.err("无法获取商品订单");
}
@Override
public JsonResult addOrder() {
return JsonResult.err("无法保存订单");
}
}
启动服务,访问测试
http://localhost:3001/item-service/35
feign + hystrix 监控和熔断测试
修改sp09-feign项目
pom.xml 添加 hystrix 起步依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
主程序添加 @EnableCircuitBreaker
package cn.tedu.sp09;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableCircuitBreaker
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class Sp09FeignApplication {
public static void main(String[] args) {
SpringApplication.run(Sp09FeignApplication.class, args);
}
}
sp09-feign 配置 actuator,暴露 hystrix.stream
监控端点
actuator 依赖
查看pom.xml, 确认已经添加了 actuator
依赖
org.springframework.boot
spring-boot-starter-actuator
application.yml 暴露 hystrix.stream
端点
management:
endpoints:
web:
exposure:
include: hystrix.stream
启动服务,查看监控端点
http://localhost:3001/actuator
hystrix dashboard
启动 hystrix dashboard 服务,填入 feign 监控路径,开启监控
访问 http://localhost:4001/hystrix
- 填入 feign 监控路径:
http://localhost:3001/actuator/hystrix.stream
- 访问微服务,以产生监控数据
http://localhost:3001/item-service/35
http://localhost:3001/user-service/7
http://localhost:3001/user-service/7/score?score=100
http://localhost:3001/order-service/123abc
http://localhost:3001/order-service/
熔断测试
- 用 ab 工具,以并发50次,来发送20000个请求
`ab -n 20000 -c 50 http://localhost:3001/item-service/35`
- 断路器状态为 Open,所有请求会被短路,直接降级执行 fallback 方法
order service 调用商品库存服务和用户服务
修改 sp04-orderservice 项目,添加 feign,调用 item service 和 user service
- pom.xml
- application.yml
- 主程序
- ItemFeignService
- UserFeignService
- ItemFeignServiceFB
- UserFeignServiceFB
- OrderServiceImpl
pom.xml
- 右键点击项目编辑起步依赖,添加以下依赖:
- actuator
- feign
- hystrix
application.yml
- ribbon 重试和 hystrix 超时这里没有设置,采用了默认值
spring:
application:
name: order-service
server:
port: 8201
eureka:
client:
service-url:
defaultZone: http://eureka1:2001/eureka, http://eureka2:2002/eureka
feign:
hystrix:
enabled: true
management:
endpoints:
web:
exposure:
include: hystrix.stream
主程序
package cn.tedu.sp04;
import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
//@EnableDiscoveryClient
//@SpringBootApplication
@EnableFeignClients
@SpringCloudApplication
public class Sp04OrderserviceApplication {
public static void main(String[] args) {
SpringApplication.run(Sp04OrderserviceApplication.class, args);
}
}
ItemFeignService
package cn.tedu.sp04.order.feignclient;
import java.util.List;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.web.util.JsonResult;
@FeignClient(name="item-service", fallback = ItemFeignServiceFB.class)
public interface ItemFeignService {
@GetMapping("/{orderId}")
JsonResult> getItems(@PathVariable String orderId);
@PostMapping("/decreaseNumber")
JsonResult decreaseNumber(@RequestBody List- items);
}
UserFeignService
package cn.tedu.sp04.order.feignclient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import cn.tedu.sp01.pojo.User;
import cn.tedu.web.util.JsonResult;
@FeignClient(name="user-service", fallback = UserFeignServiceFB.class)
public interface UserFeignService {
@GetMapping("/{userId}")
JsonResult getUser(@PathVariable Integer userId);
@GetMapping("/{userId}/score")
JsonResult addScore(@PathVariable Integer userId, @RequestParam Integer score);
}
ItemFeignServiceFB
- 获取商品列表的降级方法,模拟使用缓存数据
package cn.tedu.sp04.order.feignclient;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Component;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.web.util.JsonResult;
@Component
public class ItemFeignServiceFB implements ItemFeignService {
@Override
public JsonResult> getItems(String orderId) {
if(Math.random()<0.5) {
return JsonResult.ok().data(
Arrays.asList(new Item[] {
new Item(1,"缓存aaa",2),
new Item(2,"缓存bbb",1),
new Item(3,"缓存ccc",3),
new Item(4,"缓存ddd",1),
new Item(5,"缓存eee",5)
})
);
}
return JsonResult.err("无法获取订单商品列表");
}
@Override
public JsonResult decreaseNumber(List- items) {
return JsonResult.err("无法修改商品库存");
}
}
UserFeignServiceFB
- 获取用户信息的降级方法,模拟使用缓存数据
package cn.tedu.sp04.order.feignclient;
import org.springframework.stereotype.Component;
import cn.tedu.sp01.pojo.User;
import cn.tedu.web.util.JsonResult;
@Component
public class UserFeignServiceFB implements UserFeignService {
@Override
public JsonResult getUser(Integer userId) {
if(Math.random()<0.4) {
return JsonResult.ok(new User(userId, "缓存name"+userId, "缓存pwd"+userId));
}
return JsonResult.err("无法获取用户信息");
}
@Override
public JsonResult addScore(Integer userId, Integer score) {
return JsonResult.err("无法增加用户积分");
}
}
OrderServiceImpl
package cn.tedu.sp04.order.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.sp01.pojo.Order;
import cn.tedu.sp01.pojo.User;
import cn.tedu.sp01.service.OrderService;
import cn.tedu.sp04.order.feignclient.ItemFeignService;
import cn.tedu.sp04.order.feignclient.UserFeignService;
import cn.tedu.web.util.JsonResult;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private ItemFeignService itemService;
@Autowired
private UserFeignService userService;
@Override
public Order getOrder(String orderId) {
//调用user-service获取用户信息
JsonResult user = userService.getUser(7);
//调用item-service获取商品信息
JsonResult> items = itemService.getItems(orderId);
Order order = new Order();
order.setId(orderId);
order.setUser(user.getData());
order.setItems(items.getData());
return order;
}
@Override
public void addOrder(Order order) {
//调用item-service减少商品库存
itemService.decreaseNumber(order.getItems());
//TODO: 调用user-service增加用户积分
userService.addScore(7, 100);
log.info("保存订单:"+order);
}
}
order-service 配置启动参数,启动两台服务器
启动服务,访问测试
- 根据orderid,获取订单
http://localhost:8201/123abc
http://localhost:8202/123abc - 保存订单
http://localhost:8201/
http://localhost:8202/
hystrix dashboard 监控 order service 断路器
- 访问 http://localhost:4001/hystrix ,填入 order service 的断路器监控路径,启动监控
- http://localhost:8201/actuator/hystrix.stream
- http://localhost:8202/actuator/hystrix.stream
hystrix dashboard 监控 order service 断路器
- 访问 http://localhost:4001/hystrix ,填入 order service 的断路器监控路径,启动监控
- http://localhost:8201/actuator/hystrix.stream
- http://localhost:8202/actuator/hystrix.stream
Turbine
hystrix + turbine 集群聚合监控
hystrix dashboard 一次只能监控一个服务实例,使用 turbine 可以汇集监控信息,将聚合后的信息提供给 hystrix dashboard 来集中展示和监控
新建 sp10-turbine 项目
pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
cn.tedu
sp10-turbine
0.0.1-SNAPSHOT
sp10-turbine
Demo project for Spring Boot
1.8
Hoxton.RELEASE
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-turbine
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
application.yml
spring:
application:
name: turbin
server:
port: 5001
eureka:
client:
service-url:
defaultZone: http://eureka1:2001/eureka, http://eureka2:2002/eureka
turbine:
app-config: order-service
cluster-name-expression: new String("default")
主程序
添加 @EnableTurbine
和 @EnableDiscoveryClient
注解
package cn.tedu.sp10;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.turbine.EnableTurbine;
@EnableTurbine
@EnableDiscoveryClient
@SpringBootApplication
public class Sp10TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(Sp10TurbineApplication.class, args);
}
}
访问测试
- 8201服务器产生监控数据:
http://localhost:8201/abc123
http://localhost:8201/ - 8202服务器产生监控数据:
http://localhost:8202/abc123
http://localhost:8202/ - turbine 监控路径
http://localhost:5001/turbine.stream - 在 hystrix dashboard 中填入turbine 监控路径,开启监控
http://localhost:4001/hystrix - turbine聚合了order-service两台服务器的hystrix监控信息