Spring Cloud OpenFeign源碼解析

本篇基于Spring Cloud Hoxton.SR9

前言

如果您在看閱讀的時(shí)候感覺吃力,請(qǐng)自行補(bǔ)充基礎(chǔ)知識(shí),該系列只是帶領(lǐng)大家走讀源碼,通過讀源碼,弄懂背后實(shí)現(xiàn)的原理以及一些擴(kuò)展點(diǎn),以便更好的輔助日常工作的開發(fā)。

如果您在看閱讀的時(shí)候感覺吃力,請(qǐng)自行補(bǔ)充基礎(chǔ)知識(shí),該系列只是帶領(lǐng)大家走讀源碼,通過讀源碼,弄懂背后實(shí)現(xiàn)的原理以及一些擴(kuò)展點(diǎn),以便更好的輔助日常工作的開發(fā)。

如果您在看閱讀的時(shí)候感覺吃力,請(qǐng)自行補(bǔ)充基礎(chǔ)知識(shí),該系列只是帶領(lǐng)大家走讀源碼,通過讀源碼,弄懂背后實(shí)現(xiàn)的原理以及一些擴(kuò)展點(diǎn),以便更好的輔助日常工作的開發(fā)。
重要的事情說三遍?。。?/code>

由于之前的排版有問題,代碼不便于閱讀,所以經(jīng)過了重新排版。本篇默認(rèn)讀者已經(jīng)知道OpenFeign是做什么的,如果不知到,請(qǐng)自行百度。另外如果閱讀完本文覺得有些吃力,請(qǐng)?zhí)崆皩W(xué)習(xí)SpringBoot自動(dòng)裝配基礎(chǔ)知識(shí)。廢話不多說,直接上干貨。

基本用法

  1. 引入依賴
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 開啟feign
@EnableFeignClients
  1. 定義接口
@FeignClient("nacos-discovery-provider-sample") // 指向服務(wù)提供者應(yīng)用
public interface EchoService {

    @GetMapping("/echo/{message}")
    String echo(@PathVariable("message") String message);
}

源碼分析

配置解析階段

@EnableFeignClients

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}

通過import注解導(dǎo)入了FeignClientsRegistrar配置類,那么進(jìn)入這個(gè)類中,一看詳情。

FeignClientsRegistrar

這個(gè)類實(shí)現(xiàn)了ImportBeanDefinitionRegistrar接口,那么就需要重寫registerBeanDefinitions方法。如下:

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
        BeanDefinitionRegistry registry) {
    registerDefaultConfiguration(metadata, registry);
    registerFeignClients(metadata, registry);
}

這個(gè)方法做了2兩件事情:

  1. 注冊(cè)全局配置,如果EnableFeignClients注解上配置了defaultConfiguration屬性
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"));
        }
    }
  1. 注冊(cè)FeignClients
public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);

        Set<String> basePackages;

        Map<String, Object> attrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName());
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
                FeignClient.class);
        //省略部分代碼
        ......
        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidateComponents = scanner
                    .findCandidateComponents(basePackage);
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    // verify annotated class is an interface
                    AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                    AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                    Assert.isTrue(annotationMetadata.isInterface(),
                            "@FeignClient can only be specified on an interface");

                    Map<String, Object> attributes = annotationMetadata
                            .getAnnotationAttributes(
                                    FeignClient.class.getCanonicalName());

                    String name = getClientName(attributes);
                    registerClientConfiguration(registry, name,
                            attributes.get("configuration"));

                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }

這個(gè)方法會(huì)掃描類路徑上標(biāo)記@FeignClient注解的接口,根據(jù)@EnableFeignClients注解上的配置,并循環(huán)注冊(cè)FeignClient。
進(jìn)入registerFeignClient方法

private void registerFeignClient(BeanDefinitionRegistry registry,
            AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientFactoryBean.class);
        validate(attributes);
        definition.addPropertyValue("url", getUrl(attributes));
        definition.addPropertyValue("path", getPath(attributes));
        String name = getName(attributes);
        definition.addPropertyValue("name", name);
        String contextId = getContextId(attributes);
        definition.addPropertyValue("contextId", contextId);
        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 = contextId + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

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

        beanDefinition.setPrimary(primary);

        String qualifier = getQualifier(attributes);
        if (StringUtils.hasText(qualifier)) {
            alias = qualifier;
        }

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                new String[] { alias });
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

可以看到,這個(gè)方法就是將FeignClient注解上的屬性信息,封裝到BeanDefinition中,并注冊(cè)到Spring容器中。但是在這個(gè)方法中,有一個(gè)關(guān)鍵信息,就是真實(shí)注冊(cè)的是FeignClientFactoryBean,它實(shí)現(xiàn)了FactoryBean接口,表明這是一個(gè)工廠bean,用于創(chuàng)建代理Bean,真正執(zhí)行的邏輯是FactoryBeangetObject方法。至此,F(xiàn)eignClient的配置解析階段就完成了。下面進(jìn)入FeignClientFactoryBean,看看在這個(gè)類中都做了什么。

運(yùn)行階段

FeignClientFactoryBean

  1. 核心方法getObject
@Override
public Object getObject() throws Exception {
    return getTarget();
}

當(dāng)我們?cè)跇I(yè)務(wù)代碼中通過@Autowire依賴注入或者通過getBean依賴查找時(shí),此方法會(huì)被調(diào)用。內(nèi)部會(huì)調(diào)用getTarget方法,那么進(jìn)入這個(gè)方法一探究竟。

  1. getTarget
<T> T getTarget() {
        FeignContext context = applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);

        if (!StringUtils.hasText(url)) {
            if (!name.startsWith("http")) {
                url = "http://" + name;
            }
            else {
                url = name;
            }
            url += cleanPath();
            return (T) loadBalance(builder, context,
                    new HardCodedTarget<>(type, name, url));
        }
        if (StringUtils.hasText(url) && !url.startsWith("http")) {
            url = "http://" + 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<>(type, name, url));
    }

首先從Spring上下文中,獲取FeignContext這個(gè)Bean,這個(gè)bean是在哪里注冊(cè)的呢?是在FeignAutoConfiguration中注冊(cè)的。
然后判斷url屬性是否為空,如果不為空,則生成默認(rèn)的代理類;如果為空,則走負(fù)載均衡,生成帶有負(fù)載均衡的代理類。那么重點(diǎn)關(guān)注loadBalance方法。

  1. loadBalance
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
            HardCodedTarget<T> target) {
        Client client = getOptional(context, Client.class);
        if (client != null) {
            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?");
    }

首先調(diào)用getOptional方法,這個(gè)方法就是根據(jù)contextId,獲取一個(gè)子上下文,然后從這個(gè)子上下文中查找Client bean,SpringCloud會(huì)為每一個(gè)feignClient創(chuàng)建一個(gè)子上下文,然后存入以contextId為key的map中,詳見NamedContextFactorygetContext方法。此處會(huì)返回LoadBalancerFeignClient這個(gè)Client。詳見:FeignRibbonClientAutoConfiguration會(huì)導(dǎo)入相關(guān)配置類。
然后會(huì)從子上下文中,查找Targeter bean,默認(rèn)返回的是DefaultTargeter,
最后調(diào)用target方法。

  1. DefaultTargeter
class DefaultTargeter implements Targeter {

    @Override
    public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
            FeignContext context, Target.HardCodedTarget<T> target) {
        return feign.target(target);
    }

}

最終底層調(diào)用Feign.Buildertarget方法。進(jìn)入Feignclass中,看看到底做了什么事情

  1. Feign
public <T> T target(Target<T> target) {
    return this.build().newInstance(target);
}

public Feign build() {
    //省略部分代碼
    ......
    return new ReflectiveFeign(handlersByName, this.invocationHandlerFactory, this.queryMapEncoder);
}

可以看到最終是通過創(chuàng)建ReflectiveFeign對(duì)象,然后調(diào)用newInstance方法返回了一個(gè)代理對(duì)象,通過名字可以發(fā)現(xiàn),底層使用的是java反射創(chuàng)建的。
那么看看ReflectiveFeignnewInstance方法到底做了什么。

  1. ReflectiveFeign
public <T> T newInstance(Target<T> target) {
//根據(jù)接口類和Contract協(xié)議解析方式,解析接口類上的方法和注解,轉(zhuǎn)換成內(nèi)部的MethodHandler處理方式
        Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
        Method[] var5 = target.type().getMethods();
        int var6 = var5.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method method = var5[var7];
            if (method.getDeclaringClass() != Object.class) {
                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 為接口類創(chuàng)建動(dòng)態(tài)實(shí)現(xiàn),將所有的請(qǐng)求轉(zhuǎn)換給InvocationHandler 處理。
        InvocationHandler handler = this.factory.create(target, methodToHandler);
        T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
        Iterator var12 = defaultMethodHandlers.iterator();

        while(var12.hasNext()) {
            DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
            defaultMethodHandler.bindTo(proxy);
        }

        return proxy;
    }

見注釋。此處nvocationHandler handler = this.factory.create(target, methodToHandler);真實(shí)返回的是FeignInvocationHandler,當(dāng)在自己的業(yè)務(wù)類中調(diào)用feign接口方法時(shí),會(huì)調(diào)用FeignInvocationHandlerinvoke方法。

  1. ReflectiveFeign.FeignInvocationHandler
@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }

      return dispatch.get(method).invoke(args);
    }

invoke方法中,會(huì)調(diào)用 this.dispatch.get(method)).invoke(args) 。this.dispatch.get(method)會(huì)返回一個(gè)SynchronousMethodHandler,進(jìn)行攔截處理。這個(gè)方法會(huì)根據(jù)參數(shù)生成完成的RequestTemplate對(duì)象,這個(gè)對(duì)象是Http請(qǐng)求的模版,代碼如下。
SynchronousMethodHandler中的invoke

  1. SynchronousMethodHandler
@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;
      }
    }
  }

上面的代碼中有一個(gè) executeAndDecode()方法,該方法通過RequestTemplate生成Request請(qǐng)求對(duì)象,然后利用Http Client(默認(rèn))獲取response,來獲取響應(yīng)信息

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      //發(fā)起遠(yuǎn)程通信
      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);
    }
    //省略部分代碼
    ......
  }

client.execute(request, options);默認(rèn)使用HttpURLConnection發(fā)起遠(yuǎn)程調(diào)用,這里的client為LoadBalancerFeignClient。那么看看他的execute方法。

  1. LoadBalancerFeignClient
@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);
        }
    }

最終通過Ribbon負(fù)載均衡器發(fā)起遠(yuǎn)程調(diào)用,具體分析見另一篇關(guān)于Ribbon的源碼分析。

總結(jié)

通過源碼我們了解了Spring Cloud OpenFeign的加載配置創(chuàng)建流程。通過注解@FeignClient@EnableFeignClients注解實(shí)現(xiàn)了client的配置聲明注冊(cè),再通過FeignRibbonClientAutoConfigurationFeignAutoConfiguration類進(jìn)行自動(dòng)裝配
本文僅對(duì)feign源碼的主線進(jìn)行分析,還有很多細(xì)節(jié)并未介紹,如果讀者感興趣,可以參考本文,自行閱讀源碼。

補(bǔ)充

Feign的組成

接口 作用 默認(rèn)值
Feign.Builder Feign的入口 Feign.Builder
Client Feign底層用什么去請(qǐng)求 和Ribbon配合時(shí):LoadBalancerFeignClient不和Ribbon配合時(shí):Fgien.Client.Default
Contract 契約,注解支持 SpringMVCContract
Encoder 編碼器 SpringEncoder
Decoder 解碼器 ResponseEntityDecoder
Logger 日志管理器 Slf4jLogger
RequestInterceptor 用于為每個(gè)請(qǐng)求添加通用邏輯(攔截器,例子:比如想給每個(gè)請(qǐng)求都帶上heared)

Feign的日志級(jí)別

日志級(jí)別 打印內(nèi)容
NONE(默認(rèn)) 不記錄任何日志
BASIC 僅記錄請(qǐng)求方法,URL,響應(yīng)狀態(tài)代碼以及執(zhí)行時(shí)間(適合生產(chǎn)環(huán)境)
HEADERS 記錄BASIC級(jí)別的基礎(chǔ)上,記錄請(qǐng)求和響應(yīng)的header
FULL 記錄請(qǐng)求和響應(yīng)header,body和元數(shù)據(jù)

如何給Feign添加日志級(jí)別

局部配置

方式一:代碼實(shí)現(xiàn)
  1. 編寫配置類
public class FeignConfig {
  @Bean
  public Logger.Level Logger() {
      return Logger.Level.FULL;
   }
}

添加Feign配置類,可以添加在主類下,但是不用添加@Configuration。如果添加了@Configuration而且又放在了主類之下,那么就會(huì)所有Feign客戶端實(shí)例共享,同Ribbon配置類一樣父子上下文加載沖突;如果一定添加@Configuration,就放在主類加載之外的包。(建議還是不用加@Configuration)

  1. 配置@FeignClient
@FeignClient(name = "alibaba-nacos-discovery-server",configuration = FeignConfig.class)
public interface NacosDiscoveryClientFeign {

    @GetMapping("/hello")
    String hello(@RequestParam(name = "name") String name);
}
方式二:配置文件實(shí)現(xiàn)
feign:
  client:
    config:
      #要調(diào)用的微服務(wù)名稱
      clientName:
        loggerLevel: FULL

全局配置

方式一:代碼實(shí)現(xiàn)
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
方式二:配置文件實(shí)現(xiàn)
feign:
  client:
    config:
      #將調(diào)用的微服務(wù)名稱改成default就配置成全局的了
      default:
        loggerLevel: FULL

Feign支持的配置項(xiàng)

代碼方式支持配置項(xiàng)

配置項(xiàng) 作用
Logger.Level 指定日志級(jí)別
Retryer 指定重試策略
ErrorDecoder 指定錯(cuò)誤解碼器
Request.Options 超時(shí)時(shí)間
Collection<RequestInterceptor> 攔截器
SetterFactory 用于設(shè)置Hystrix的配置屬性,F(xiàn)gien整合Hystrix才會(huì)用

詳見FeignClientsConfiguration中配置

配置文件屬性支持配置項(xiàng)
feign:
  client:
    config:
      feignName:
        connectTimeout: 5000  # 相當(dāng)于Request.Optionsn 連接超時(shí)時(shí)間
        readTimeout: 5000     # 相當(dāng)于Request.Options 讀取超時(shí)時(shí)間
        loggerLevel: full     # 配置Feign的日志級(jí)別,相當(dāng)于代碼配置方式中的Logger
        errorDecoder: com.example.SimpleErrorDecoder  # Feign的錯(cuò)誤解碼器,相當(dāng)于代碼配置方式中的ErrorDecoder
        retryer: com.example.SimpleRetryer  # 配置重試,相當(dāng)于代碼配置方式中的Retryer
        requestInterceptors: # 配置攔截器,相當(dāng)于代碼配置方式中的RequestInterceptor
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        # 是否對(duì)404錯(cuò)誤解碼
        decode404: false
        encode: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

Feign還支持對(duì)請(qǐng)求和響應(yīng)進(jìn)行GZIP壓縮,以提高通信效率,
僅支持Apache HttpClient,詳見FeignContentGzipEncodingAutoConfiguration
配置方式如下:

# 配置請(qǐng)求GZIP壓縮
feign.compression.request.enabled=true
# 配置響應(yīng)GZIP壓縮
feign.compression.response.enabled=true
# 配置壓縮支持的MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置壓縮數(shù)據(jù)大小的下限
feign.compression.request.min-request-size=2048

Feign默認(rèn)使用HttpUrlConnection進(jìn)行遠(yuǎn)程調(diào)用,可以通過配置開啟HttpClient或OkHttp3,具體詳見FeignRibbonClientAutoConfiguration,配置如下:

feign.httpclient.enabled=true
//或
feign.okhttp.enabled=true

并添加相應(yīng)的依賴即可

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
//或
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

關(guān)于Spring Cloud OpenFeign的配置有很多,本文只是列出了部分配置,更多配置請(qǐng)自行閱讀源碼。

歡迎關(guān)注我的公眾號(hào):程序員L札記

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、簡(jiǎn)介 Feign是一個(gè)http請(qǐng)求調(diào)用的輕量級(jí)框架,可以以Java接口注解的方式調(diào)用Http請(qǐng)求,而不用像Ja...
    小波同學(xué)閱讀 2,474評(píng)論 0 8
  • 歡迎Java工程師關(guān)注簡(jiǎn)書專欄Java架構(gòu)技術(shù)進(jìn)階本專欄收錄各種Java相關(guān)技術(shù),面試題,以及學(xué)習(xí)感悟,心得! 隨...
    Java_蘇先生閱讀 1,520評(píng)論 0 3
  • 在使用Feign接口的時(shí)候我們首先要添加@EnableFeignClient注解,注解@EnableFeignCl...
    aiwen2017閱讀 1,378評(píng)論 1 1
  • 轉(zhuǎn)載請(qǐng)標(biāo)明出處:本文出自方志朋的博客 什么是Feign Feign是受到Retrofit,JAXRS-2.0和We...
    方志朋閱讀 1,690評(píng)論 1 10
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者,不喜歡去冒險(xiǎn),但是人生放棄了冒險(xiǎn),也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 7,604評(píng)論 0 4

友情鏈接更多精彩內(nèi)容