Feign客戶端呼叫服務時丟失Header引數的解決方案

Thinkingcao發表於2020-10-14

前言

在SpringCloud微服務架構的專案中,服務之間的呼叫是通過Feign客戶端實現。預設情況下在使用Feign客戶端時,Feign 呼叫遠端服務存在Header請求頭引數丟失問題,例如一個訂單服務Order和一個商品服務Product,呼叫關係為: 使用者下單呼叫訂單服務,訂單庫建立一筆訂單,同時商品服務扣減庫存數量;在訂單服務通過Feign呼叫商品服務中扣減庫存的介面時,由於Feign是一個偽HTTP客戶端,這時相當於重新生成一個HTTP請求,會出現請求頭Header引數丟失問題,那麼下面給大家介紹一下如何解決這種不足。

一、解決方案

1. 實現Feign提供的RequestInterceptor

首先需要新建一個類FeignRequestInterceptor,用來做攔截器,FeignRequestInterceptor實現Feign中提供的RequestInterceptor類,重寫apply方法,在apply方法方法中通過邏輯程式碼實現獲取header請求頭引數資訊,然後傳遞下去。

具體做法吐下:

  • FeignRequestInterceptor攔截器
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

/**
 * @desc:  微服務之間使用Feign呼叫介面時, 透傳header引數資訊
 * @author: cao_wencao
 * @date: 2020-09-25 16:36
 */
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                String values = request.getHeader(name);
                requestTemplate.header(name, values);
            }
        }
    }
}

2. 配置使用

FeignRequestInterceptor實現Feign提供的RequestInterceptor後,看了網上大部分文件提到需要做一下額外的配置,才能夠使FeignRequestInterceptor生效,我所使用的SpringCloud版本是Greenwich.SR2,在這個版本中我發現無需額外的配置也可以正常生效,查了下文件,說是在Spring-Cloud Brixton之前的版本需要做一下額外的配置,方可生效。

這裡我兼顧到在使用SpringCloud比較老的版本,擴充套件一下需要如何配置才能生效自定義的攔截器實現無縫傳遞Header請求頭引數的目的。

二、配置生效

上述提到,這部分配置生效是為了兼顧老版本的SpringCloud所需要做的,新版本加上因為無妨,這裡給大家介紹兩種配置生效方式,分別是在@FeignClient註解屬性的configuration欄位配置攔截器類和application.yml中配置攔截器類。

1. @FeignClient註解屬性

@FeignClient註解中有個configuration配置屬性,通過configuration可指定自定義的攔截器FeignRequestInterceptor

  • IProductService
import com.example.common.product.entity.Product;
import com.example.order.config.FeignRequestInterceptor;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @desc:
 * @author: cao_wencao
 * @date: 2020-09-22 23:43
 */
@FeignClient(value = "product-service",configuration = FeignRequestInterceptor.class)
public interface ProductService {
    //@FeignClient的value +  @RequestMapping的value值  其實就是完成的請求地址  "http://product-service/product/" + pid
    //指定請求的URI部分
    @RequestMapping("/product/product/{pid}")
    Product findByPid(@PathVariable Integer pid);

    //扣減庫存,模擬全域性事務提交
    //引數一: 商品標識
    //引數二:扣減數量
    @RequestMapping("/product/reduceInventory/commit")
    void reduceInventoryCommit(@RequestParam("pid") Integer pid,
                               @RequestParam("number") Integer number);

    //扣減庫存,模擬全域性事務回滾
    //引數一: 商品標識
    //引數二:扣減數量
    @RequestMapping("/product/reduceInventory/rollback")
    void reduceInventoryRollback(@RequestParam("pid") Integer pid,
                         @RequestParam("number") Integer number);
}
  • ProductController獲取Header傳遞的Token
/**
 * @desc:
 * @author: cao_wencao
 * @date: 2020-09-22 23:16
 */
@RestController
@Slf4j
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 扣減庫存,正常->模擬全域性事務提交
     * @param pid
     * @param number
     */
    @RequestMapping("/reduceInventory/commit")
    public void reduceInventoryCommit(Integer pid, Integer number) {
        String token = ServletUtils.getRequest().getHeader("token");
        log.info("從head請求頭透傳過來的值為token:"+ token);
        productService.reduceInventoryCommit(pid, number);
    }
}    
  • ServletUtils工具類
public class ServletUtils {
    /**
     * 獲取String引數
     */
    public static String getParameter(String name) {
        return getRequest().getParameter(name);
    }

    /**
     * 獲取String引數
     */
    public static String getParameter(String name, String defaultValue) {
        return Convert.toStr(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 獲取Integer引數
     */
    public static Integer getParameterToInt(String name) {
        return Convert.toInt(getRequest().getParameter(name));
    }

    /**
     * 獲取Integer引數
     */
    public static Integer getParameterToInt(String name, Integer defaultValue) {
        return Convert.toInt(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 獲取request
     */
    public static HttpServletRequest getRequest() {
        return getRequestAttributes().getRequest();
    }

    /**
     * 獲取response
     */
    public static HttpServletResponse getResponse() {
        return getRequestAttributes().getResponse();
    }

    /**
     * 獲取session
     */
    public static HttpSession getSession() {
        return getRequest().getSession();
    }

    public static ServletRequestAttributes getRequestAttributes() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }

    /**
     * 將字串渲染到客戶端
     *
     * @param response 渲染物件
     * @param string   待渲染的字串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 是否是Ajax非同步請求
     *
     * @param request
     */
    public static boolean isAjaxRequest(HttpServletRequest request) {
        String accept = request.getHeader("accept");
        if (accept != null && accept.indexOf("application/json") != -1) {
            return true;
        }

        String xRequestedWith = request.getHeader("X-Requested-With");
        if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1) {
            return true;
        }

        String uri = request.getRequestURI();
        if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) {
            return true;
        }

        String ajax = request.getParameter("__ajax");
        return StringUtils.inStringIgnoreCase(ajax, "json", "xml");
    }
}

2. application.yml配置攔截器類

配置 讓所有 FeignClient,使用 FeignRequestInterceptor,具體如下:

  • application.yml
#配置讓所有 FeignClient,使用FeignRequestInterceptor
feign:
  hystrix:
    enabled: true
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
        requestInterceptors: com.example.order.config.FeignRequestInterceptor
        
#修改預設隔離策略為訊號量模式
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE

也可以配置讓 某個 FeignClient 使用這個 FeignRequestInterceptor,具體如下:

#配置讓所有 FeignClient,使用FeignRequestInterceptor
feign:
  hystrix:
    enabled: true
  client:
    config:
      xxxx: ##表示遠端服務名
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
        requestInterceptors: com.example.order.config.FeignRequestInterceptor
        
#修改預設隔離策略為訊號量模式
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE

解釋一下下面這行配置的含義: 在轉發Feign的請求頭的時候, 如果開啟了Hystrix, Hystrix的預設隔離策略是Thread(執行緒隔離策略), 因此轉發攔截器內是無法獲取到請求的請求頭資訊的。

可以修改預設隔離策略為訊號量模式:
在這裡插入圖片描述

總結

通過上述為大家總結的幾點方式,我們可完美的解決Feign客戶端在呼叫服務時丟失了Header引數的問題,有什麼疑問,見下方評論,一起交流。

相關文章