Spring Cloud OpenFeign底层实现原理

Spring Cloud OpenFeign底层实现原理

先说一下写这篇文章的一个原因,就是我被面试官吊打了,我只知道OpenFeign底层采用了RestTemplate进行调用,采用了动态代理,但是具体怎么实现的我就母鸡了。为了防止同样的地方摔倒,我决定我现在这里爬起来。

一 、简介

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。 OpenFeign@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

二、OpenFeign的使用

1. 引入依赖

xml 复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2. 启动类添加@EnableFeignClients注解

less 复制代码
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(basePackages = {"xx.xx"})
@ComponentScan(basePackages = {"xx.xx"})
public class TagApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(TagApplication.class);
    }
}

3. 编写业务类

kotlin 复制代码
@FeignClient(name = "user-service")
public interface BaseFeignApi {
    @GetMapping("/list")
    List<User> getList();
}

三、OpenFeign实现原理

1.@EnableFeignClients(basePackages = {"xx.xx"})注解

less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
​
    String[] value() default {};
​
    String[] basePackages() default {};
​
    Class<?>[] basePackageClasses() default {};
​
    Class<?>[] defaultConfiguration() default {};
​
    Class<?>[] clients() default {};
}
​

2.FeignClientsRegistrar

2.1 FeignClientsRegistrar

java 复制代码
class FeignClientsRegistrar
        implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    ....
}

2.2ImportBeanDefinitionRegistrar()函数

typescript 复制代码
public interface ImportBeanDefinitionRegistrar {
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        this.registerBeanDefinitions(importingClassMetadata, registry);
    }
​
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    }
}

FeignClientsRegistrar本质是一个ImportBeanDefinitionRegistrar,并且持有环境变量和资源加载器的能力,FeignClientsRegistrar重写了registerBeanDefinitions方法,该方法在容器上下文刷新(启动时调用refresh)时被调用,调用时机此处不展开分析,我们看一下.

2.3FeignClientsRegistrar#registerBeanDefinitions()函数

scss 复制代码
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
        BeanDefinitionRegistry registry) {
    // 处理@EnableFeignClients注解上的属性配置,将配置注册到容器仲
    registerDefaultConfiguration(metadata, registry);
    // 核心方法:注册@FeignClient对应的接口,奖@FeignClient注册到容器中
    registerFeignClients(metadata, registry);
}

2.4FeignClientsRegistrar#registerFeignClients()函数

ini 复制代码
    public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
​
        // 创建Spring内置的扫描器
        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);
        // 获取 EnableFeignClients 注解中的 clients 属性值
        final Class<?>[] clients = attrs == null ? null
                : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            // 添加过滤器:过滤器所有被 @FeignClient 标记接口
            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)));
        }
        // 处理@FeignClient类,解析
        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"));
                    // 重点方法,注入FeignClient对象
                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }
​

第一部分是为了找到@Feignclient标识的接口类,第二部分就是对找出的接口类进行处理了处理,主要关注registerClientConfigurationregisterFeignClient函数。

其中registerClientConfiguration是为了处理@FeignClient#configuration属性的,在这个函数会往spirng容器中添加#{serviceName}.FeignClientSpecification作为名字的FeignClientSpecification类对象,例如user-center.FeignClientSpecification。

registerFeignClient函数则是处理接口类的主要方法了。我们在之前考虑到,我们在接口上填写了@FeignClient注解,在之后程序中我们可以直接引用这个接口对象来调用接口上的函数,理论分析一波:接口如果没有实现类,是不能直接在spring中直接进行注入并调用相应的方法的,一定需要我们去实现这个接口,那么我们可以想到,OpenFeign中一定做了这样的操作。

接下来我们看一下registerFeignClient函数

2.5registerFeignClient()函数

ini 复制代码
    private void registerFeignClient(BeanDefinitionRegistry registry,
            AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        // 创建一个 BeanDefinitionBuilder 对象,用于构建并注入 FeignClientFactoryBean
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientFactoryBean.class);
        // 参数校验
        validate(attributes);
        // 设置BeanDefinition参数
        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();
​
        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;
        }
​
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                new String[] { alias });
        // 注入接口实例
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

3. 接口代理对象的构建

3.1FeignClientFactoryBean

OpenFeign 接口代理对象的构建,主要是通过 Spring 的扩展接口 FactoryBean<T> 来实现的。在上面的代码中,通过解析 FeignClient 对象,构建成一个 FeignClientFactoryBean 对象,Spring 在注入对应接口是,会调用 FeignClientFactoryBean 对象中的 getObject() 方法,返回注入对应的代理对象。

kotlin 复制代码
class FeignClientFactoryBean
        implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    
    @Override
    public Object getObject() throws Exception {
        // 获取目标对象
        return getTarget();
    }
​
    /**
     * 获取目标对象
     * @param <T> the target type of the Feign client
     * @return a {@link Feign} client created with the specified data and the context
     * information
     */
    <T> T getTarget() {
        FeignContext context = this.applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);
        // 判断当前 FeignClient 注解中的url是否为空,如果不为空,直接通过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));
        }
        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));
    }
}

3.2 Targeter#target()函数

HystrixTargeter类实现了Targeter

scss 复制代码
class HystrixTargeter implements Targeter {
​
    @Override
    public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
            FeignContext context, Target.HardCodedTarget<T> target) {
        if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
            return feign.target(target);
        }
        feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
        String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
                : factory.getContextId();
        SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
        if (setterFactory != null) {
            builder.setterFactory(setterFactory);
        }
        Class<?> fallback = factory.getFallback();
        if (fallback != void.class) {
            return targetWithFallback(name, context, target, builder, fallback);
        }
        Class<?> fallbackFactory = factory.getFallbackFactory();
        if (fallbackFactory != void.class) {
            return targetWithFallbackFactory(name, context, target, builder,
                    fallbackFactory);
        }
        // 返回目标对象
        return feign.target(target);
    }
}

3.3 Feign对象

feign.target(target)函数

kotlin 复制代码
public abstract class Feign {
    
    public <T> T target(Target<T> target) {
        // 创建代理对象
        return this.build().newInstance(target);
    }
    
    public abstract <T> T newInstance(Target<T> target);
​
    /**
     * 构建 Feign对象
     */
    public Feign build() {
        Client client = (Client)Capability.enrich(this.client, this.capabilities);
        Retryer retryer = (Retryer)Capability.enrich(this.retryer, this.capabilities);
        List<RequestInterceptor> requestInterceptors = (List)this.requestInterceptors.stream().map((ri) -> {
            return (RequestInterceptor)Capability.enrich(ri, this.capabilities);
        }).collect(Collectors.toList());
        Logger logger = (Logger)Capability.enrich(this.logger, this.capabilities);
        Contract contract = (Contract)Capability.enrich(this.contract, this.capabilities);
        Options options = (Options)Capability.enrich(this.options, this.capabilities);
        Encoder encoder = (Encoder)Capability.enrich(this.encoder, this.capabilities);
        Decoder decoder = (Decoder)Capability.enrich(this.decoder, this.capabilities);
        // 创建代理对象的 InvocationHandler 工厂实例
        InvocationHandlerFactory invocationHandlerFactory = (InvocationHandlerFactory)Capability.enrich(this.invocationHandlerFactory, this.capabilities);
        QueryMapEncoder queryMapEncoder = (QueryMapEncoder)Capability.enrich(this.queryMapEncoder, this.capabilities);
        Factory synchronousMethodHandlerFactory = new Factory(client, retryer, requestInterceptors, logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy, this.forceDecoding);
        ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory);
        return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }
}
​

点击进入newInstance()函数

3.4ReflectiveFeign 对象

ReflectiveFeign继承了Feign

scala 复制代码
public class ReflectiveFeign extends Feign {
/**
 * 创建代理对象
 */
  @Override
  public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
    // 解析模板:将方法解析,封装为MethodHandler
    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)));
      }
    }
    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;
  }
}

看到这里就比较清晰了,把接口中的方法和默认实现放到Map<Method, MethodHandler>中然后使用InvocationHandlerFactory.Default()创建InvocationHandler,然后使用jdk动态代理生成接口的代理并返回,这里主要看一下FeignInvocationHandler实现:

kotlin 复制代码
  static class FeignInvocationHandler implements InvocationHandler {
​
    private final Target target;
    private final Map<Method, MethodHandler> dispatch;
​
    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }
​
    @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);
    }
   }
​

四、服务调用

正如我们前边所说,通过@Autowired或者@Resource注入的时候,注入的是被封装之后的代理类实现, jdk动态代理持有的是ReflectiveFeign.FeignInvocationHandler类型的InvocationHandler,那么具体调用的时候,会调用FeignInvocationHandler#invoke()方法

4.1 FeignInvocationHandler对象

kotlin 复制代码
​
static class FeignInvocationHandler implements InvocationHandler { 
    private final Target target;
    private final Map<Method, MethodHandler> dispatch;
​
    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }
​
    @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);
    }
   }

前几个判断分支都是调用了Object的基本方法,最后dispatch.get(method).invoke(args)才是接口的业务方法调用,dispatch的类型是Map<Method, MethodHandler>,是接口中方法与MethodHandler的映射关系,而MethodHandler又被SynchronousMethodHandler.Factory封装成SynchronousMethodHandler(实现了MethodHandler):

4.2 SynchronousMethodHandler

SynchronousMethodHandler类实现了MethodHandler。那么dispatch.get(method).invoke(args)最终调用的就是SynchronousMethodHandler#invoke()方法:

java 复制代码
final class SynchronousMethodHandler implements MethodHandler {
      @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 {
        // 可以看到调用的时候默认是带有重试能力,默认是5次,具体调用交给executeAndDecode来实现:
        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;
      }
    }
  }
}

可以看到调用的时候默认是带有重试能力,默认是5次,具体调用交给executeAndDecode来实现:

ini 复制代码
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 {
       // 通过 Client执行,进行远程调用
      response = client.execute(request, options);
    } 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);
​
    boolean shouldClose = true;
    try {
      if (logLevel != Logger.Level.NONE) {
        response =
            logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
      }
      if (Response.class == metadata.returnType()) {
        if (response.body() == null) {
          return response;
        }
        if (response.body().length() == null ||
            response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          return response;
        }
        // Ensure the response body is disconnected
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        return response.toBuilder().body(bodyData).build();
      }
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          Object result = decode(response);
          shouldClose = closeAfterDecode;
          return result;
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
        Object result = decode(response);
        shouldClose = closeAfterDecode;
        return result;
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }

核心是client.execute(),我们选择性看一下Default默认的实现:

vbscript 复制代码
@Override
public Response execute(Request request, Options options) throws IOException {
  HttpURLConnection connection = convertAndSend(request, options);
  return convertResponse(connection, request);
}

很明显,最终服务调用会委托给HttpURLConnection来执行,然后组装结果和状态码返回调用,到这里openfeign的服务调用就分析完了,为了帮助理解和有更直观的概念,我们看一下服务调用时序图:

图是我从网上找的,是OkHttpClient的,大家凑合看一下。

注意:

我们知道OpenFeign的底层默认会交给HttpURLConnection处理,HttpURLConnection是不支持连接池的。所以这里我们可以配置为HttpClientOkHttpClient等进行一个优化。具体配置如下:

yaml 复制代码
feign:
  client:
    config:
      default:
      # 日志级别,这里是我本地开发环境设置为FULL,生产环境不建议设置为FULL,可以设置为HEAD
        loggerLevel: FULL
  # 使用httpclient   
  httpclient:
    enabled: true
    max-connections: 200
    max-connections-per-route: 50
 # OpenFeign第一次加载慢是因为底层采用了懒加载的方式,我们这里采用了饥饿加载的方式
 ribbon:
  eager-load:
    enabled: true
    clients: easyexcelService

五、总结

  1. 通过 @EnableFeignCleints 触发 Spring 应用程序对 classpath 中 @FeignClient 修饰类的扫描
  2. 解析到 @FeignClient 修饰类后, Feign 框架通过扩展 Spring Bean Deifinition 的注册逻辑, 最终注册一个 FeignClientFacotoryBean 进入 Spring 容器
  3. Spring 容器在初始化其他用到 @FeignClient 接口的类时, 获得的是 FeignClientFacotryBean 产生的一个代理对象 Proxy.
  4. 基于 java 原生的动态代理机制, 针对 Proxy 的调用, 都会被统一转发给 Feign 框架所定义的一个 InvocationHandler , 由该 Handler 完成后续的 HTTP 转换, 发送, 接收, 翻译HTTP响应的工作

感觉这块的逻辑还是有点难度的,我debug源码三四次才搞懂OpenFeign底层是如何创建代理对象,如何实现调用的。想要了解OpenFeign底层原理的同学可以自己手动debug一下源码看一看具体的执行流程。

纸上得来终觉浅,绝知此事要躬行。

参考:

  • blog.csdn.net/weixin_4182...^v100^pc_search_result_base4&utm_term=openfeign%E5%BA%95%E5%B1%82%E8%B0%83%E7%94%A8%E5%8E%9F%E7%90%86&spm=1018.2226.3001.4187
相关推荐
luom01024 小时前
springcloud springboot nacos版本对应
spring boot·spring·spring cloud
南昌彭于晏6 小时前
springcloud+openFeign单元测试解决初始化循环依赖的问题
spring·spring cloud·单元测试
⑩-11 小时前
服务注册与发现的原理?Nacos vs Eureka?
spring cloud·云原生·eureka
SmartBrain13 小时前
基于SpringAI架构的多智能体协作(进阶版)
人工智能·spring boot·python·spring cloud
qingwufeiyang_53013 小时前
Nacos学习笔记
java·笔记·学习·spring cloud·服务发现
小江的记录本1 天前
【Spring Boot—— .yml(YAML)】Spring Boot中.yml文件的基础语法、高级特性、实践技巧
xml·java·spring boot·后端·spring·spring cloud·架构
xiaolingting2 天前
Gateway 网关流控与限流架构指南
spring cloud·架构·gateway·sentinel
唯一世2 天前
Open Feign最佳实践
java·spring cloud
Don.TIk2 天前
SpringCloud学习笔记
笔记·学习·spring cloud
z_鑫2 天前
SpringCloud FeignClient 中 Bean 重复注册冲突解决方案解析
java·spring boot·spring cloud