Spring Cloud OpenFeign的原理(六)


通過上篇我們了解OpenFeign他也可以完成遠程通信,但是它並不是真正義意上的RPC通信,因為他是通過封裝代理來實現的,下面和以前一樣,知道了怎么用就來看下他是怎么實現的。

一、思考Feign要做的事情

有了ribbon的鋪墊現在看OpenFeign應該很清楚的知道,這玩意就是通過注解拿到服務名,然后通過服務名獲取服務列表,進行解析和負載最終拼接出一個URI路徑進行代理請求,那么他要完成這一系列動作他就要做下面幾件事。

  • 參數的解析和裝載
  • 針對指定的FeignClient,生成動態代理
  • 針對FeignClient中的方法描述進行解析
  • 組裝出一個Request對象,發起請求

二、源碼分析

看過我寫的ribbon的應該清楚,如果想要找到進入源碼的入口那么應該要找的是FeignClient,但是FeignClient是在哪里被解析的呢,在應用篇中我在啟動類中加了個@EnableFeignClients注解,這 個注解的作用其實就是開啟了一個FeignClient的掃描,那么點擊啟動類的@EnableFeignClients注解看下他是怎么開啟FeignClient的掃描的,進去后發現里面有個@Import(FeignClientsRegistrar.class)這個FeignClientsRegistrar跟Bean的動態裝載有關

 

 

 點擊進去有個registerBeanDefinitions方法通過名稱可以知道是一個Bean的注入方法

 

 

 下面我寫一個簡單的例子來描述他是如何實現動態加載的,學FeignClientsRegistrar類 implements ImportBeanDefinitionRegistrar接口並實現registerBeanDefinitions方法

 

 

 這一步搞完后,定義一個注解,把@EnableFeignClients注解上的注解都抄過來並把@Import注解里面的類改成我們自己定義的類

 

 

 然后在啟動類上用上自定義的注解,那么在啟動類時就可以進行一個Bean的動態裝載了

 通過這個概念已經很清楚源碼中FeignClientsRegistrar類的FeignClientsRegistrar是怎么完成Bean的動態加載了

  • registerDefaultConfifiguration 方法內部從 SpringBoot 啟動類上檢查是否有@EnableFeignClients, 有該注解的話, 則完成 Feign 框架相關的一些配置內容注冊
  • registerFeignClients 方法內部從 classpath 中, 掃描獲得 @FeignClient 修飾的類, 將類的內容解析為 BeanDefifinition , 最終通過調用 Spring 框架中的BeanDefifinitionReaderUtils.resgisterBeanDefifinition 將解析處理過的 FeignClientBeanDeififinition 添加到 spring 容器中. 

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注冊默認配置信息,將EnableFeignClients的defaultConfiguration注冊到Spring容器中
registerDefaultConfiguration(metadata, registry);
//注冊FeignClients(可能有多個),@FeignClient注解的接口注冊到Spring容器中。
registerFeignClients(metadata, registry);
}

如果有該注解,則開啟包掃描,掃描被@FeignClient注解接口。

private void registerDefaultConfiguration(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        Map<String, Object> defaultAttrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

        if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
            String name;
            if (metadata.hasEnclosingClass()) {
                name = "default." + metadata.getEnclosingClassName();
            }
            else {
                name = "default." + metadata.getClassName();
            }
            registerClientConfiguration(registry, name,
                    defaultAttrs.get("defaultConfiguration"));
        }
    }

 

進入registerFeignClients(metadata, registry);這玩意是干啥的呢,在啟動類中的@EnableFeignClients是可以定義多個basePackers的如果定義了多個那就要掃描FeignClients,下面就是掃描處理過程,下面看下是如何注冊FeignClient

public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        // ClassPath的條件掃描組件提供者
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        // 設置資源加載器
        scanner.setResourceLoader(this.resourceLoader);
        // 要掃描的包(@EnableFeignClients注解上添的那個)
        Set<String> basePackages;

        // 獲取注解上的配置
        Map<String, Object> attrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName());
        // 注解過濾器,設置只過濾出FeignClient注解標識的Bean
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
                FeignClient.class);
        final Class<?>[] clients = attrs == null ? null
                : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            // 掃描器設置過濾器
            scanner.addIncludeFilter(annotationTypeFilter);
            // 獲取注解的掃描包路徑
            basePackages = getBasePackages(metadata);
        }
        else {
            final Set<String> clientClasses = new HashSet<>();
            basePackages = new HashSet<>();
            for (Class<?> clazz : clients) {
                basePackages.add(ClassUtils.getPackageName(clazz));
                clientClasses.add(clazz.getCanonicalName());
            }
            // 只知道是類型過濾器,暫時什么作用還不明白,求解!
            AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
                @Override
                protected boolean match(ClassMetadata metadata) {
                    // 將類名上的[$]替換成[.]
                    String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                    return clientClasses.contains(cleaned);
                }
            };
            scanner.addIncludeFilter(
                    new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
        }

        for (String basePackage : basePackages) {
            // 從指定的包中掃描出和規范的BeanDefinition
            Set<BeanDefinition> candidateComponents = scanner
                    .findCandidateComponents(basePackage);
            for (BeanDefinition candidateComponent : candidateComponents) {
                // 掃描的Bean是否是AnnotatedBeanDefinition的子類
                // 雖然看不懂AnnotatedBeanDefinition是什么意思,但是顧名思義我覺得是通過注解掃出來的BeanDefinition就是他的子類
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    // verify annotated class is an interface
                    AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                    // 獲取beanDefinition的元數據,你想要的他基本都有
                    AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                    // 驗證@FeignClient修飾的必須是接口
                    Assert.isTrue(annotationMetadata.isInterface(),
                            "@FeignClient can only be specified on an interface");

                    // 獲取@FeignClient注解的屬性
                    Map<String, Object> attributes = annotationMetadata
                            .getAnnotationAttributes(
                                    FeignClient.class.getCanonicalName());
                    // 獲取客戶端名稱
                    String name = getClientName(attributes);
                    // 為FeignClient指定配置類
                    registerClientConfiguration(registry, name,
                            attributes.get("configuration"));
                    // 注冊客戶端
                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }

有了上面的步驟其實准備工作就做好了,下面就看下注冊FeiginClient具體實現registerFeignClient方法;點擊registerFeignClient(registry, annotationMetadata, attributes);看下做了啥事,這里面的邏輯其實就干了一件事,就是通過BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);注入一個Bean;這個注入的過程中有個比較重要的代碼BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);這是一個構造者,構造一個BeanDefinition,里面把FeignClientFactoryBean.class給傳了進去

private void registerFeignClient(BeanDefinitionRegistry registry,
            AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        // 被@FeignClient修飾的類名,比如 com.xxx.TestFeignClient,是自己編輯的
        String className = annotationMetadata.getClassName();
        // BeanDefinitionBuilder通過FeignClientFactoryBean這個類來生成BeanDefinition
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientFactoryBean.class);
        // 驗證fallback和fallbackFactory是不是接口
        validate(attributes);
        // 通過BeanDefinitionBuilder給beanDefinition增加屬性
        definition.addPropertyValue("url", getUrl(attributes));
        definition.addPropertyValue("path", getPath(attributes));
        String name = getName(attributes);
        definition.addPropertyValue("name", name);
        definition.addPropertyValue("type", className);
        definition.addPropertyValue("decode404", attributes.get("decode404"));
        definition.addPropertyValue("fallback", attributes.get("fallback"));
        definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

        String alias = name + "FeignClient";
        // 用Builder獲取實際的BeanDefinition
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

        boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null

        beanDefinition.setPrimary(primary);

        String qualifier = getQualifier(attributes);
        if (StringUtils.hasText(qualifier)) {
            alias = qualifier;
        }
        // 創建一個Bean定義的持有者
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                new String[] { alias });
        // 這里就是將Bean注冊到Spring容器中
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

由上面一段代碼,FeignClient客戶端注冊就此完成。但是上面還有兩個重點還沒看完

 下面進入.genericBeanDefinition(FeignClientFactoryBean.class);看把 FeignClientFactoryBean.class類傳進去干嘛,發現注冊的Bean就是參數中自己傳進來的beanClass,這個傳進去的beanClass是工廠Bean

Spring Cloud FengnClient實際上是利用Spring的代理工廠來生成代理類,所以在這里地方才會把所有的FeignClient的BeanDefifinition設置為FeignClientFactoryBean類型,而FeignClientFactoryBean繼承自FactoryBean,它是一個工廠Bean。在Spring中,FactoryBean是一個工廠Bean,用來創建代理Bean。工廠 Bean 是一種特殊的 Bean, 對於 Bean 的消費者來說, 他邏輯上是感知不到這個 Bean 是普通的 Bean 還是工廠 Bean, 只是按照正常的獲取 Bean 方式去調用, 但工廠bean 最后返回的實例不是工廠Bean 本身, 而是執行工廠 Bean 的 getObject 邏輯返回的示例。

 

 

 點擊這個工廠Bean的FeignClientFactoryBean類中發現里面有個getObject()方法,這個工廠Bean就是通過這個getTarget();返回一個真正的實例

 

 

 畫下時序圖

 

 

 

 前面說到了在啟動時會通過@EnableFeignClients去掃描所有指定路徑下的@FeignClient注解聲明的一個接口,然后在掃描到以后要去生成一個動態代理的類,這個動態代理的生成就是在調用getObject()時完成 ,而且getObject()又會調用他方法里面的getTarget()去完成這件事,

它從applicationContext取出FeignContext,FeignContext繼承了NamedContextFactory,它是用來來統一維護feign中各個feign客戶端相互隔離的上下文。他只所以能完成隔離跟他父類中的contexts()方法有很大關系

<T> T getTarget() {
//FeignContext注冊到容器是在FeignAutoConfiguration上完成的
//在初始化FeignContext時,會把configurations在容器中放入FeignContext中。configurations的
//來源就是在前面registerFeignClients方法中將@FeignClient的配置configuration。
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);//構建Builder對象
//如果url為空,則走負載均衡,生成有負載均衡功能的代理類
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
//如果指定了url,則生成默認的代理類
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}//生成默認代理類
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}

 上面有段代碼Feign.Builder builder = feign(context);是構建Builder對象

    protected Feign.Builder feign(FeignContext context) {
        FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
        Logger logger = loggerFactory.create(this.type);

        // @formatter:off
        Feign.Builder builder = get(context, Feign.Builder.class)
                // required values
//開啟日志級別 .logger(logger)
//編碼 .encoder(
get(context, Encoder.class))
//解碼 .decoder(
get(context, Decoder.class))
//連接。這個連接用在解析模板的,后面會提 .contract(
get(context, Contract.class)); // @formatter:on configureFeign(context, builder); return builder; }

上面的builder構造完后繼續向下走,配置完Feign.Builder之后,再判斷是否需要LoadBalance,如果需要,則通過loadBalance(builder, context,new HardCodedTarget<>(this.type, this.name, this.url));的方法來設置。實際上他們最終調用的是Target.target()方法。 

loadBalance這玩意比較重要因為他是生成具備負載均衡能力的feign客戶端,為feign客戶端構建起綁定負載均衡客戶端Client client = (Client)this.getOptional(context, Client.class); 從上下文中獲取一個Client,默認是LoadBalancerFeignClient。
它是在FeignRibbonClientAutoConfiguration這個自動裝配類中,通過Import實現的

@Import({ HttpClientFeignLoadBalancedConfiguration.class, 
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
            HardCodedTarget<T> target) {
//針對某一個服務的client Client client
= getOptional(context, Client.class); if (client != null) {
//將client設置進去相當於增加了客戶端負載均衡解析的機制 builder.client(client); Targeter targeter
= get(context, Targeter.class); return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); }

點擊上圖的targeter.target(this, builder, context, target);因為熔斷准備在后面講,所以在選tartget的實現時選擇DefaultTarget.target 

 

 點擊feign.target()往下走,走到這里其實就已經到了核心邏輯了,前面不一直說動態代理嗎,前面走的都是人生最長的套路,前面自己寫的控制層代碼通過@Resource注解注入的UserOpenFeign他最終會調用下面的方法返回一個實例,那么下面看下這newInstance()方法做了啥,發現這玩意有兩個實現,至於選擇哪個就要看build()返回的是什么了,向下看發現build()返回的是ReflectiveFeign,所以選第二個

 

 

其實看到這里就已經可以看出來了 feign生成代理是用的反射(生成 ReflectiveFeign對象)。我們通用的代理有 JDK動態代理CGLIB兩種代理。我們繼續看看 Feign用的是哪種,這個方法是用來創建一個動態代理的方法,在生成動態代理之前,會根據Contract協議(協議解析規則,解析接口類的注解信息,解析成內部的MethodHandler的處理方式。從實現的代碼中可以看到熟悉的Proxy.newProxyInstance方法產生代理類。而這里需要對每個定義的接口方法進行特定的處理實現,所以這里會出現一個MethodHandler的概念,就是對應方法級別的InvocationHandler。 

 

 public <T> T newInstance(Target<T> target) {
 //根據接口類和Contract協議解析方式,解析接口類上的方法和注解,轉換成內部的MethodHandler處理方式
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if (Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    // 基於Proxy.newProxyInstance 為接口類創建動態實現,將所有的請求轉換給InvocationHandler 處理。
    InvocationHandler handler = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

下面通過debugger驗證下,會看到userOpenFeign的返回的是代理類,通過下圖可以知道當調用userOpenFeign時他其實是調用ReflectiveFeign中的handler,而通過Debugger發現這個handler是FeginInvocationHandler,

竟然是走了代理那么他一定是走了ReflectiveFeign的代理方法invoke()方法

 下面進入OpenFeign的調用過程

 

 

而接着,在invoke方法中,會調用 this.dispatch.get(method)).invoke(args) 。this.dispatch.get(method) 會返回一個SynchronousMethodHandler,進行攔截處理。這個方法會根據參數生成完成的RequestTemplate對象,這個對象是Http請求的模版,代碼如下。
看SynchronousMethodHandler中的invoke類
  @Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

通過Debugger,訪問請求路徑,可以發現下面這些基本信息都拿到了

 

經過上述的代碼,我們已經將restTemplate拼裝完成,上面的代碼中有一個 executeAndDecode() 方法,該方法通過RequestTemplate生成Request請求對象,然后利用Http Client獲取response,來獲取響應信息
 
  Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
//轉換為HTTP請求報文 Request request
= targetRequest(template); if (logLevel != Logger.Level.NONE) { logger.logRequest(metadata.configKey(), logLevel, request); } Response response; long start = System.nanoTime(); try {
//發起遠程通信 response
= client.execute(request, options); // ensure the request is set. TODO: remove in Feign 12
//獲取返回結果 response = response.toBuilder() .request(request) .requestTemplate(template) .build(); } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); } throw errorExecuting(request, e); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); if (decoder != null) return decoder.decode(response, metadata.returnType()); CompletableFuture<Object> resultFuture = new CompletableFuture<>(); asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response, metadata.returnType(), elapsedTime); try { if (!resultFuture.isDone()) throw new IllegalStateException("Response handling not done"); return resultFuture.join(); } catch (CompletionException e) { Throwable cause = e.getCause(); if (cause != null) throw cause; throw e; } }

其中上圖Client.execute 就已經拿到了返回結果,默認采用JDK的 HttpURLConnection 發起遠程調用。但我們這里用了負載均衡用到的是LoadBalancerFeignClient,到這里的URL還是沒有解析

 
            
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);

FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);

IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
 

回退到下圖所示位置,其實前面所有動作我們只是看到了InvocationHandler handler = factory.create(target, methodToHandler);這玩意做了啥事情,但是很核心的東西怎么解析的並沒有看到。所以回退到下圖位置會發現有個東西叫MethodHandler,他會把target傳過去解析,解析完后得到MethodHandler,這東西其實主是針對方法接口的解析

 那么看下他是怎么做的,點擊targetToHandlersByName.apply(target);進入 apply方法,targetToHandlersByName.apply(target);會解析接口方法上的注解,從而解析出方法粒度的特定的配置信息,然后生產一個SynchronousMethodHandler 然后需要維護一個<method,MethodHandler>的map,放入InvocationHandler的實現FeignInvocationHandler中。 

public Map<String, MethodHandler> apply(Target target) {
//當前的contract默認是SpringMvcContract,因為他默認繼承Springmvc的模板解析 List
<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type()); Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>(); for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else if (md.bodyIndex() != null) { buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else { buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target); } if (md.isIgnored()) { result.put(md.configKey(), args -> { throw new IllegalStateException(md.configKey() + " is not a method handled by feign"); }); } else { result.put(md.configKey(), factory.create(target, md, buildTemplate, options, decoder, errorDecoder)); } } return result; }

 通過下面debugger可以發現這個contract可以做到元數據的解析,去拿到目標進行解析,當前Spring Cloud 微服務解決方案中,為了降低學習成本,采用了Spring MVC的部分注解來完成 請求協議解析,也就是說 ,寫客戶端請求接口和像寫服務端代碼一樣:客戶端和服務端可以通過SDK的方式進行約定,客戶端只需要引入服務端發布的SDK API,就可以使用面向接口的編碼方式對接服務。 

 

 

再次回到上次退回的地方,經過上面的方法后就得到了一個MethodHandler,就會進入下面進行一個循環,因為我們會拿到多個method,拿到多個后會進行一些處理,然后再傳到factory.create(target, methodToHandler);

 

然后接合前面知識又能神奇發現這玩意是default

 

 

補充內容:

前面說過Feign logging,官網中有說明如何調整等級:https://docs.spring.io/spring-cloud-openfeign/docs/2.2.5.RELEASE/reference/html/#feign-logging

 

 下面來驗證下,首先自定一個FooConfiguration類

 

 然后按官網要求在配置文件中配置

#設置日志級別,可以在idea控制台上看日志內容,設置的路徑是@FeignClient注解的路徑
logging.level.com.ghy.demo.UserOpenFeign=DEBUG

啟動兩個項目,發起請求后查看控制台,可以看到控制能胡詳細的請求信息

 

 

 源碼git網址:https://github.com/ljx958720/Spring-Cloud-OpenFeign-.git

 


免責聲明!

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



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