Open feign动态切换服务目标地址组件

2025-12-12 0 275

在微服务架构中,我们经常需要调用其他服务的API。随着业务的发展,可能会出现需要在多个服务集群之间动态切换的场景,例如灰度发布、故障转移、负载均衡等。本文介绍一个基于 Spring Boot 和 OpenFeign 的动态切换服务提供方集群的组件,它允许在运行时根据配置动态切换 Feign 调用的下游集群,而无需重启服务。

组件背景

在复杂的微服务环境中,为了提高系统的可用性和稳定性,通常会部署多个服务实例集群(如生产/灰度、蓝绿)。传统通过修改配置或代码并重启服务的方式不够灵活。为了解决这个问题,开发了一个动态切换服务提供方集群的 Spring Boot Starter 组件。该组件基于注解与配置,可在运行时动态切换 Feign 调用的目标集群。

核心设计思路(步骤化概览)

路由键注解 – @RoutingKey

用于标记需要动态路由的方法或类。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingKey {
    String value() default \"\";
}

@RoutingKey 注解可以标记在方法或类上,用于指定路由规则。支持 SpEL 表达式,可以根据方法参数动态确定路由键。

RoutingKeyInterceptor

拦截被 @RoutingKey 注解标记的方法,根据注解值和配置确定目标 URL 并放入上下文。

@Slf4j
public class RoutingKeyInterceptor implements MethodInterceptor {

  private AnnotationClassResolver resolver;
  private Environment environment;
  private final DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

  public RoutingKeyInterceptor(AnnotationClassResolver resolver, Environment environment) {
    this.resolver = resolver;
    this.environment = environment;
  }

  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    String url = determineUrl(invocation);
    try {
      RequestUrlContext.set(url);
      return invocation.proceed();
    } finally {
      RequestUrlContext.clear();
    }
  }

  private String determineUrl(MethodInvocation invocation) {
    String expr = resolver.getValue(invocation.getMethod(), invocation.getThis().getClass());
    if (StrUtil.isBlank(expr)) {
      if (log.isDebugEnabled()) {
        log.debug(\"RoutingKeyInterceptor.determineUrl, no expr found, method={}\", invocation.getMethod());
      }
      return StrUtil.EMPTY;
    }

    String routingKey = calculateExpr(expr, invocation.getMethod(), invocation.getArguments());
    if (StrUtil.isBlank(routingKey)) {
      if (log.isDebugEnabled()) {
        log.debug(\"RoutingKeyInterceptor.determineUrl, expr value blank, expr={}, method={}, args={}\", expr, invocation.getMethod(), Arrays.asList(invocation.getArguments()));
      }
      return StrUtil.EMPTY;
    }

    String url = environment.getProperty(routingKey, StrUtil.EMPTY);
    if (log.isDebugEnabled()) {
      log.debug(\"RoutingKeyInterceptor.determineUrl, end, url={}, routingKey={}\", url, routingKey);
    }

    return url;
  }

  private String calculateExpr(String expr, Method method, Object[] params) {
    String exprValue = \"unknown\";
    if (StrUtil.isNotBlank(expr) && expr.contains(\"#\")) {
      ExpressionParser parser = new SpelExpressionParser();
      StandardEvaluationContext ctx = new StandardEvaluationContext();
      String[] parameterNames = discoverer.getParameterNames(method);
      if (parameterNames != null) {
        for (int i = 0; i < parameterNames.length; i++) {
          ctx.setVariable(parameterNames[i], params[i]);
        }
      }
      Expression expression = parser.parseExpression(expr);
      Object value = expression.getValue(ctx);
      if (value != null) {
        exprValue = value.toString();
      }
    }

    return exprValue;
  }
}

RoutingKeyInterceptor 负责解析 @RoutingKey 注解中的表达式,根据方法参数计算出路由键,从环境中取出对应的 URL,并将其存入 RequestUrlContext。

DynamicClusterRequestInterceptor(Feign 请求拦截器)

在 Feign 请求发送前修改目标 URL。

@Slf4j
public class DynamicClusterRequestInterceptor implements RequestInterceptor {

  private Collection<Class> supportTypes;
  private RequestRouter requestRouter;

  public DynamicClusterRequestInterceptor(Collection<Class> supportTypes, RequestRouter requestRouter) {
    this.supportTypes = supportTypes;
    this.requestRouter = requestRouter;
  }

  @Override
  public void apply(RequestTemplate template) {
    Class type = template.feignTarget().type();
    if (!typeMatch(type)) {
      if (log.isDebugEnabled()) {
        log.debug(\"DynamicClusterRequestInterceptor.apply, not match, type={}\", type.getName());
      }
      return;
    }

    String url = requestRouter.determineUrl(template);
    if (StrUtil.isBlank(url)) {
      if (log.isDebugEnabled()) {
        log.debug(\"DynamicClusterRequestInterceptor.apply, route url is blank, type={}\", type.getName());
      }
      return;
    }

    template.target(url);
  }

  private boolean typeMatch(Class type) {
    for (Class supportType : supportTypes) {
      if (type.isAssignableFrom(supportType)) {
        return true;
      }
    }

    return false;
  }
}

该拦截器通过 RequestRouter 获取目标 URL,并调用 template.target(url) 修改请求目标。

RequestUrlContext(请求上下文)

用于在线程/调用链间传递目标 URL(示例中使用 SkyWalking 的 TraceContext)。

public class RequestUrlContext {

  public static final String KEY = \"routing-url\";

  public static void set(String url) {
    TraceContext.putCorrelation(KEY, url);
  }

  public static String url() {
    return TraceContext.getCorrelation(KEY).orElse(null);
  }

  public static void clear() {
    TraceContext.putCorrelation(KEY, StrUtil.EMPTY);
  }
}

ContextRequestRouter(上下文路由器)

从 RequestUrlContext 中获取目标 URL,作为 RequestRouter 的实现之一。

public class ContextRequestRouter implements RequestRouter {

  @Override
  public String determineUrl(RequestTemplate requestTemplate) {
    return RequestUrlContext.url();
  }
}

启用配置 – @EnabledRoutingKeyAdvisor / DynamicClusterConfiguration

启用路由键顾问功能并注册 AOP advisor:

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

DynamicClusterConfiguration 示例:

@Configuration
public class DynamicClusterConfiguration {

  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  @Bean
  public Advisor routingKeyAnnotationAdvisor(Environment environment) {
    RoutingKeyInterceptor interceptor = new RoutingKeyInterceptor(new AnnotationClassResolver(true, RoutingKey.class), environment);
    AnnotationAdvisor advisor = new AnnotationAdvisor(interceptor, RoutingKey.class);
    return advisor;
  }
}

DynamicClusterConfiguration 创建了一个 AOP 顾问,用于拦截被 @RoutingKey 注解标记的方法。

核心代码解析(按模块复列)

  • @RoutingKey 注解:标记方法或类,支持 SpEL 表达式。
  • RoutingKeyInterceptor:解析注解表达式,计算路由键,从 Environment 获取 URL,放入 RequestUrlContext。
  • RequestUrlContext:使用 TraceContext 存储目标 URL(便于分布式追踪)。
  • DynamicClusterRequestInterceptor:Feign 请求拦截器,根据 RequestRouter 返回的 URL 修改请求目标。
  • ContextRequestRouter:从 RequestUrlContext 获取 URL。

使用示例(步骤化)

启用功能(在 Spring Boot 主类上添加注解)

@SpringBootApplication
@EnableRoutingKeyAdvisor
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

(注:本文前面定义的注解为 @EnabledRoutingKeyAdvisor,示例中使用了 @EnableRoutingKeyAdvisor,请根据实际使用的注解名保持一致。)

在 Feign 客户端方法上使用 @RoutingKey

@FeignClient(name = \"user-service\")
public interface UserServiceClient {

    @GetMapping(\"/users/{id}\")
    @RoutingKey(\"user.service.url\")
    User getUserById(@PathVariable(\"id\") Long id);

    @PostMapping(\"/users\")
    @RoutingKey(\"#user.type + \'.user.service.url\'\")
    User createUser(@RequestBody User user);
}

示例展示了静态路由键(\”user.service.url\”)和基于方法参数的 SpEL 表达式路由键(\”#user.type + \’.user.service.url\’\”)。

在配置文件中定义路由键对应的 URL

示例(application.yml):

user:
  service:
    url: 
vip:
  user:
    service:
      url: 

通过修改这些配置(例如由灰度服务的 URL 切换到生产服务的 URL),配合运行时更改 Environment 或其它配置源的话,可以在不重启的情况下改变路由目标。

总结

该 Spring Boot Starter 组件提供了一种灵活、无侵入的方式,在运行时动态切换 Feign 调用的下游集群,适用于灰度发布、故障切换、蓝绿部署等场景。主要优势包括:

  • 无侵入性:通过注解使用,对业务代码侵入小;
  • 动态性:支持运行时动态切换,无需重启服务;
  • 灵活性:支持 SpEL 表达式,可根据方法参数动态确定路由目标;
  • 可追踪性:示例中集成 SkyWalking TraceContext,支持分布式追踪。

通过上述组件组合(注解 + AOP 拦截 + Feign 请求拦截器 + 请求上下文),可以在复杂微服务场景中更好地管理服务调用,提高系统灵活性与稳定性。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 Open feign动态切换服务目标地址组件 https://www.zuozi.net/36039.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务