Feign 自定義編碼器、解碼器和客戶端,Feign 轉發請求頭(header參數)、Feign輸出Info級別日志


Feign 的編碼器、解碼器和客戶端都是支持自定義擴展,可以對請求以及結果和發起請求的過程進行自定義實現,Feign 默認支持 JSON 格式的編碼器和解碼器,如果希望支持其他的或者自定義格式就需要編寫自己的編碼器和解碼器,如果希望編寫自己的編碼器,需要實現 feign.codec.Encoder 接口,解碼器需要實現 feign.codec.Decoder 接口,示例如下:

自定義編碼器和解碼器

  • 自定義編碼器

    實現的自定義編碼器只是輸出了需要編碼的參數信息,而具體的編碼還是使用 JSON 格式,使用了 GsonEncoder 編碼器來完成具體的編碼工作,參數 object 表示需要進行編碼的對象,參數 bodyType 為 object 對象的類型,參數 template 表示的就是請求模板,該方法就是需要實現將參數 object 進行編碼並賦值到 template 請求模板中。

    package org.lixue.feignclient;

       

    import feign.RequestTemplate;

    import feign.codec.EncodeException;

    import feign.codec.Encoder;

    import feign.gson.GsonEncoder;

       

    import java.lang.reflect.Type;

       

    public class MyEncoder implements Encoder{

    private GsonEncoder gsonEncoder;

       

    publicMyEncoder(){

    gsonEncoder new GsonEncoder();

    }

       

    public void encode(Object object,Type bodyType,RequestTemplate template) throws EncodeException{

    System.out.println("encode object is class"+object.getClass().getName());

    System.out.println("encode object is value"+object);

    System.out.println("encode bodyType is class"+bodyType.getClass().getName());

    System.out.println("encode bodyType is value"+bodyType);

       

    gsonEncoder.encode(object,bodyType,template);

    }

    }

       

  • 自定義解碼器

    實現的自定義解碼器使用了 GsonDecoder 解碼器來完成具體的編碼工作,解碼器相對簡單,只需要從響應中獲取響應報文,然后按照指定的編碼格式相應的解碼並創建指定的類型實例即可。

    package org.lixue.feignclient;

       

    import feign.FeignException;

    import feign.Response;

    import feign.codec.DecodeException;

    import feign.codec.Decoder;

    import feign.gson.GsonDecoder;

       

    import java.io.IOException;

    import java.lang.reflect.Method;

    import java.lang.reflect.Type;

       

    public class MyDecoder implements Decoder{

       

    private GsonDecoder gsonDecoder;

       

    publicMyDecoder(){

    gsonDecoder=newGsonDecoder();

    }

       

    public Object decode(Response response,Type type)throws IOException,DecodeException,FeignException{

    return gsonDecoder.decode(response,type);

    }

    }

  • 測試驗證

    在完成自定義編碼器和解碼器的開發后,只需要在 Feign 的 builder 方法中,增加解碼器和編碼器即可,需要注意的是,如果方法請求參數或返回的不是對象,不需要進行編碼或解碼,就不能增加編碼器或解碼器,示例代碼如下:

    package org.lixue.feignclient;

       

    import feign.Feign;

    import feign.Logger;

    import feign.gson.GsonDecoder;

    import feign.gson.GsonEncoder;

       

    public class Startup{

    public static void main(String[]args){

    HelloWorldClient speakClient=

    Feign.builder().target(HelloWorldClient.class,"http://localhost:8080/");

    // 參數和返回都不是對象,不需要附加編碼器和解碼器

    System.out.println(speakClient.speak("isbody"));

       

    HelloWorldClient findByIdClient=

    Feign.builder().decoder(new GsonDecoder())

    .target(HelloWorldClient.class,"http://localhost:8080/");

    // 返回的是對象,需要附加解碼器

    Person person=findByIdClient.findById(34);

    System.out.println("personid="+person.getId()+"age="+person.getAge()+"name="+person.getName()+"message="+person.getMessage());

       

    HelloWorldClient createClient=

    Feign.builder().client(newMyClient())

    .decoder(newMyDecoder())

    .encoder(newMyEncoder())

    .target(HelloWorldClient.class,"http://localhost:8080/");

       

    Person newPerson=new Person();

    newPerson.setId(3434);

    newPerson.setAge(34);

    newPerson.setName("343434");

    newPerson.setMessage("33333333333333333");

    // 參數和返回都是對象,需要附加解碼器和編碼器

    ReturnValuereturnValue=createClient.create(newPerson);

    System.out.println(returnValue.parseString());

    }

    }

       

自定義 Feign 客戶端

  • 自定義 Feign 客戶端

    Feign 使用一個 feign.Client 接口來發送請求,默認實現是使用 HttpURLConnection 連接 HTTP 服務,我們可以實現 feign.Client 接口來完成自定義 Feign 客戶端的開發,該接口只有一個方法 execute ,用於執行請求,下面實現了一個自定義的 Feign 客戶端,主要完成了請求的日志記錄,示例代碼如下:

    package org.lixue.feignclient;

    import feign.Client;

    import feign.Request;

    import feign.Response;

    import java.io.IOException;

    import java.util.Collection;

    import java.util.Map;

    public class MyClient implements Client{

    public Response execute(Request request,Request.Options options)throws IOException{

    System.out.println("execute request method="+request.method());

    System.out.println("execute request headers");

    Map<String,Collection<String>> headers=request.headers();

    for(Map.Entry<String,Collection<String>> entry:headers.entrySet()){

    StringBuilderstringBuilder=newStringBuilder();

    for(intj=0;j<entry.getValue().size();j++){

    if(stringBuilder.length()>0){

    stringBuilder.append(",");

    }

    stringBuilder.append(entry.getValue());

    }

    System.out.println(entry.getKey()+":"+stringBuilder.toString());

    }

    byte[] body=request.body();

    if(body!=null){

    System.out.println("execute request body="+newString(body));

    }

    // 使用 Feign 默認的客戶端請求

    return new Client.Default(null,null).execute(request,options);

    }

    }

測試驗證

和附加編碼器、解碼器類似,只需要在 Feign 的 builder 方法中附加自定義的客戶端即可,代碼如下:

package org.lixue.feignclient;

import feign.Feign;

import feign.Logger;

import feign.gson.GsonDecoder;

import feign.gson.GsonEncoder;

public class Startup{

public static void main(String[]args){

HelloWorldClient findByIdClient=

Feign.builder().client(new MyClient())

.decoder(new GsonDecoder())

.target(HelloWorldClient.class,"http://localhost:8080/");

Person person=findByIdClient.findById(34);

System.out.println("personid="+person.getId()+"age="+person.getAge()+"name="+person.getName()+"message="+person.getMessage());

}

}

Feign 轉發請求頭(header參數)

在做接口請求時,我們經常會在header頭中增加一些鑒權信息,如token 或 jwt,那么在通過fegin從A server去調用B server的接口時,如果B server的接口需要header信息,我們需要將A sever獲取的header轉發到B上。

解決方式

我們需要實現Feign提供的一個接口RequestInterceptor

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
26
27
28
29
30
31
32
33
34
35
@Configuration
public  class  FeignConfiguration  implements  RequestInterceptor{
     private  final  Logger logger = LoggerFactory.getLogger(getClass());
 
             @Override
             public  void  apply(RequestTemplate template) {
                 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);
                         template.header(name, values);
 
                     }
                     logger.info( "feign interceptor header:{}" ,template);
                 }
                /* Enumeration<String> bodyNames = request.getParameterNames();
                 StringBuffer body =new StringBuffer();
                 if (bodyNames != null) {
                     while (bodyNames.hasMoreElements()) {
                         String name = bodyNames.nextElement();
                         String values = request.getParameter(name);
                         body.append(name).append("=").append(values).append("&");
                     }
                 }
                 if(body.length()!=0) {
                     body.deleteCharAt(body.length()-1);
                     template.body(body.toString());
                     //logger.info("feign interceptor body:{}",body.toString());
                 }*/
             }
         }

@FeignClient注解里面的屬性加上configuration = FeignConfiguration.class就可以了。如

1
2
3
@FeignClient (name =  "a-server" ,  configuration = FeignConfiguration. class )
public  interface  AServer{
}

 

bootstrap.yml增加

復制代碼
hystrix:
  command:
    default:
      execution:
        timeout:
          enabled: false
        isolation:
          strategy: SEMAPHORE

 

 

Feign調用開啟Hystrix時無法獲取ThreadLocal

在項目中使用根據請求頭處理異常信息國際化的問題,但是在feign調用的時候無法傳遞請求頭,這個問題看了好久最后才知道feign開啟hystrix默認會新建一個線程,而我的請求頭數據是通過攔截器放到ThreadLocal里的在新線程就無法獲取了

先看一下原來是怎么實現的

首先是header封裝類


@Data
public class CommonRequestHeader { /** * version 版本號 */ private String version; /** * 平台類型 */ private String platform; } 

把請求頭封裝到ThreadLocal中

@Data public class CommonRequestHeaderHolder { public static final ThreadLocal<CommonRequestHeader> context = new ThreadLocal<>(); public static void clear() { context.set(null); } public static void setContext(CommonRequestHeader header) { context.set(header); } public static CommonRequestHeader getContext() { return context.get(); } } 

每次請求的時候通過filter封裝把請求頭數據放入context中

但是在feign中開啟hystrix的話新線程的ThreadLocal是無法獲取主線程的數據的,這個時候就要用到InheritableThreadLocal,只需要改一行代碼

private static final ThreadLocal<CommonRequestData> context = new InheritableThreadLocal<>(); 

InheritableThreadLocal是ThreadLocal的子類,可以解決父線程和子線程的數據傳輸問題

當在主線程開啟一個新線程時,會執行Thread的init方法
init方法中有這么一段代碼

        if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 

當父線程中的inheritableThreadLocal被賦值時,會將當前線程的inheritableThreadLocal變量進行createInheritedMap(),看一下這個方法的具體實現,它會繼續調用ThreadLocalMap(parentMap),主要的目的是父線程的變量值賦值給子線程。

當讀取ThreadLocal的地方是個線程池的時候,inheritableThreadLocal也會有問題
 
 

背景

  spring cloud netfix組件中,feign相關的日志默認是不會輸出的,需要自定義配置才能輸出,並且Feign只對Debug基本的日志做出響應, 實際業務需要輸出Info級別的日志,所以需要做自定義配置,覆蓋相關配置Bean。

Feign配置

  Feign客戶端可以配置各種的Logger.Level對象,告訴Feign記錄哪些日志。Logger.Level的值有以下選擇。

 

    NONE,無記錄(DEFAULT)。

 

    BASIC,只記錄請求方法和URL以及響應狀態代碼和執行時間。

 

    HEADERS,記錄基本信息以及請求和響應標頭。

 

    FULL,記錄請求和響應的頭文件,正文和元數據。

打印Feign日志

  1、Feign的配置類

    根據Feign配置的描述,需要將Logger.Level 配置到客戶端中:

復制代碼
 @Configuration
  public class FeignClientConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
復制代碼

  2、在客戶端中修改@FeignClient注解

 

復制代碼
  @FeignClient(name = "qex-comsrv", fallback = ComsrvHystrix.class, configuration = { FeignClientConfig.class }) public abstract interface ComsrvFeignApi{ @RequestMapping({"/otp/esgMsg/send.do"}) public abstract JSONObject send(OTPRequest otprequest); }
復制代碼

  3、修改客戶端的日志打印級別

    因為feign只對日志級別為debug級別做出響應,所以如果需要打印出日志,還需要修改客戶端的日志級別在application.properties中要設定一行這樣的配置:
        logging.level.<你的feign client全路徑類名>: DEBUG
    操作完成這三步驟,當調用Send 方法的時候就會打印出Debug級別的日志。

打印Info級別日志

  在實際生產環境中,我們常常需要使用Info級別日志,使用上述針對對每個客戶端的配置進行修改,那樣將會有大量的配置。所以,需要將修改Feign的日志,對Info級別進行相應。

  1、重寫feign.logger類

復制代碼
public class QjxFeignLogger extends feign.Logger { private final Logger logger; public QjxFeignLogger() { this(feign.Logger.class); } public QjxFeignLogger(Class<?> clazz) { this(LoggerFactory.getLogger(clazz)); } public QjxFeignLogger(String name) { this(LoggerFactory.getLogger(name)); } QjxFeignLogger(Logger logger) { this.logger = logger; } @Override protected void logRequest(String configKey, Level logLevel, Request request) { if (logger.isInfoEnabled()) { super.logRequest(configKey, logLevel, request); } } @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { if (logger.isInfoEnabled()) { return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime); } return response; } @Override protected void log(String configKey, String format, Object... args) { // Not using SLF4J's support for parameterized messages (even though it // would be more efficient) because it would // require the incoming message formats to be SLF4J-specific. if (logger.isInfoEnabled()) { logger.info(String.format(methodTag(configKey) + format, args)); } } }
復制代碼

  自定義一個Logger類,繼承Feign.Logger,將代碼中的Debug修改成為Info

  2、自定義Logger類加入配置

復制代碼
 @Configuration
  public class FeignClientConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } @Bean Logger QjxFeign(){ return new QjxFeignLogger(); } }
復制代碼

  將自定義Logger類加入到Feign客戶端配置的Config中,這樣Feign系統間調用就能打印出Info級別的日志。

總結

  打印出Feign系統間調用Info級別的日志,核心的思想是Spring Boot項目中,能夠自定義配置,自定義的配置優先級大於默認的配置。詳情參見Spring Boot自定義配置


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM