参考谷粒商城
1、简介
Feign 是一个声明式的 HTTP 客户端,它的目的就是让远程调用更加简单。Feign 提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。
Feign 整合了 Ribbon(负载均衡)和 Hystrix(服务熔断),可以让我们不再需要显式地使用这两个组件。
SpringCloudFeign 在 NetflixFeign 的基础上扩展了对 SpringMVC 注解的支持,在其实现下,我们只需创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。简化了SpringCloudRibbon 自行封装服务调用客户端的开发量。
2、使用
2.1 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
要使用远程调用,必须先在nacos中注册服务,能使feign找到被调用的服务
2.2 需要远程调用的服务开启 feign 功能
#在springboot启动类上添加该注解
#basePackages为告诉springboot扫描哪个包下的远程调用
@EnableFeignClients(basePackages = "com.atguigu.gulimall.pms.feign")
2.3 声明远程接口
@FeignClient(“gulimall-ware”):声明该接口需要远程调用哪个服务 gulimall-ware为被调用服务的服务名
该接口中的每一个方法都是被远程调用服务中的完整方法签名,(包含完整请求地址)
@FeignClient("gulimall-ware")
public interface WareFeignService {
@PostMapping("/ware/waresku/skus")
public Resp<List<SkuStockVo>> skuWareInfos(@RequestBody List<Long> skuIds);
}
2.4 远程调用原理
比如有一个A服务,需要调用B服务中的一个接口:
B服务接口如下:
/**
* 保存
*/
@PostMapping("/save")
// @RequiresPermissions("coupon:spubounds:save")
public R save(@RequestBody SpuBoundsEntity spuBounds){
spuBoundsService.save(spuBounds);
return R.ok();
}
A服务创建一个远程调用接口
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@PostMapping("/coupon/spubounds/save") // 此处必须是被调用服务接口的完整路径
public R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
}
调用原理:
- 1、A服务的@RequestBody将SpuBoundTo 对象转为json数据。
- 2、feign根据@FeignClient(“gulimall-coupon”)在服务注册中心找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。将上一步转的json数据放在请求体里,发送请求。
- 3、被调用服务收到请求。获得请求体里的json数据,B服务的@RequestBody SpuBoundsEntity spuBounds会将请求体里的json数据转为SpuBoundsEntity ,(SpuBoundTo 内的 字段和SpuBoundsEntity 内的字段必须保持一致)
参数传输总结:只要json数据模型是兼容的,双方服务无需使用同一个TO
3、解决远程调用丢失请求头信息
在远程调用中,由于feign会在远程调用之前会构造一个新请求去远程调用,而新请求内没有我们从浏览器携带过来的各种头信息,所有就需要在feign构造请求的时候,调用的拦截器中解决请求头丢失的问题。
3.1、同一线程下远程调用丢失请求头信息
@Configuration
public class GuliFeignConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
// 1、使用RequestContextHolder拿到刚进来的这个请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 同步请求头数据,Cookie
System.out.println("feign远程调用之前先进行RequestInterceptor.apply");
String cookie = request.getHeader("Cookie");
// 给新请求同步了老请求的cookie
requestTemplate.header("Cookie", cookie);
}
};
}
}
3.1、不同线程下异步远程调用丢失请求头信息
上面的拦截器设置请求头时,获取的是当前主线程的请求信息,但是如果是异步线程远程调用下,则异步线程在拦截器中获取不到主线程的请求信息,如下:
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
// 1、远程查询所有的收货地址
CompletableFuture<List<MemberAddressVo> > addressFuture = CompletableFuture.supplyAsync(() -> {
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
return address;
}, threadPoolExecutor);
// 2、远程查询购物车所有选中的购物项
// feign在远程调用之前要构造请求,构造请求会调用很多拦截器
CompletableFuture<List<OrderItemVo> > orderItemFuture = CompletableFuture.supplyAsync(() -> {
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
return currentUserCartItems;
}, threadPoolExecutor);
// 3、查询用户积分
Integer integration = memberResponseVo.getIntegration();
orderConfirmVo.setIntegration(integration);
// 4、其他数据自动计算
// todo 5、防重令牌
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(addressFuture, orderItemFuture);
voidCompletableFuture.get();
List<MemberAddressVo> memberAddressVos = addressFuture.get();
List<OrderItemVo> orderItemVos = orderItemFuture.get();
orderConfirmVo.setAddress(memberAddressVos);
orderConfirmVo.setItems(orderItemVos);
return orderConfirmVo;
}
所以,我们在异步远程调用时,需要手动设置异步线程的上下文与主线程同步 保证在feign的拦截器中可以使用到主线程的信息
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
// 获取主线程的上下文
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 1、远程查询所有的收货地址
CompletableFuture<List<MemberAddressVo> > addressFuture = CompletableFuture.supplyAsync(() -> {
// 设置异步线程的上下文与主线程同步 保证在feign的拦截器中可以使用到主线程的信息
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
return address;
}, threadPoolExecutor);
// 2、远程查询购物车所有选中的购物项
// feign在远程调用之前要构造请求,构造请求会调用很多拦截器
CompletableFuture<List<OrderItemVo> > orderItemFuture = CompletableFuture.supplyAsync(() -> {
// 设置异步线程的上下文与主线程同步 保证在feign的拦截器中可以使用到主线程的信息
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
return currentUserCartItems;
}, threadPoolExecutor);
// 3、查询用户积分
Integer integration = memberResponseVo.getIntegration();
orderConfirmVo.setIntegration(integration);
// 4、其他数据自动计算
// todo 5、防重令牌
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(addressFuture, orderItemFuture);
voidCompletableFuture.get();
List<MemberAddressVo> memberAddressVos = addressFuture.get();
List<OrderItemVo> orderItemVos = orderItemFuture.get();
orderConfirmVo.setAddress(memberAddressVos);
orderConfirmVo.setItems(orderItemVos);
return orderConfirmVo;
}
此时在feign的拦截器中,就能获取到主线程的请求信息了。