feign 基于参数动态指定路由主机

news/2024/11/28 22:33:44/

feign 基于参数动态指定路由主机

背景

项目上最近有需求:通过一个公共基础实体定义一个主机地址字段 , feign 远程调用时候根据具体值动态改变进行调用。

官方解决方案

第一种方案

官方支持动态指定 URI

Overriding the Request Line

If there is a need to target a request to a different host then the one supplied when the Feign client was created, or
you want to supply a target host for each request, include a java.net.URI parameter and Feign will use that value
as the request target.

@RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(URI host, Issue issue, @Param("owner") String owner, @Param("repo") String repo);

根据文档的相关指引 , 需要提供一个 URI 参数就可以动态指定目标主机 , 可以实现动态路由。

URI 方式源码分析

官方 URI 动态改变主机源码解析:

Contract 类是 feign 用于提取有效信息到元信息存储

feign.Contract.BaseContract.parseAndValidateMetadata(java.lang.Class<?>, java.lang.reflect.Method)
方法解析元数据时候 , 判断参数类型是否为 URI 类型 , 然后记录下参数位置

if(parameterTypes[i]==URI.class){data.urlIndex(i);
}

feign.ReflectiveFeign.BuildTemplateByResolvingArgs.create 方法执行初始化 RequestTemplate 时候 , 根据 urlIndex()
是否为 null , 直接设置 feign.RequestTemplate.target 方法改变最终目标地址

private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {// ...@Overridepublic RequestTemplate create(Object[] argv) {RequestTemplate mutable = RequestTemplate.from(metadata.template());mutable.feignTarget(target);if (metadata.urlIndex() != null) {int urlIndex = metadata.urlIndex();checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);mutable.target(String.valueOf(argv[urlIndex]));}// ...}
}

URI 方式优缺点

优点:直接 , 直接传入目标主机地址可以直接实现动态路由

缺点:如果是普通三方调用接口形式的话 , 使用起来问题不大;但是我们如果是微服务的模式 , 我们经常会定义一个 API
接口 , FeignClient 客户端和 Controller 层同时实现 , 如果多一个 URI 参数情况下 , 需要远程调用又不需要改变路由 , 会导致我们需要多填写一个参数,请看下面的案例:

API 接口

public interface AccountApi {@PostMapping(value = "/accounts")Result<AccountCreateDTO> saveAccount(@RequestBody BaseCloudReq<AccountCreateReq> req);
}

FeignClient


@FeignClient(value = "app-server-name", contextId = "AccountClient")
public interface AccountClient extends AccountApi {
}

Controller


@RequestMapping("/accounts")
public class AccountController implements AccountApi {@PostMapping@Overridepublic Result<AccountCreateDTO> saveAccount(@RequestBody BaseCloudReq<AccountCreateReq> req) {// ...return Result.success(accountService.saveAccount(request));}
}

上面案例会有以下问题:

  • 我需要改变 @FeignClient 注解的 value 值 , 只能通过参数 URI 指定 , 需要加一个 URI 参数
  • 如果根据上面第一点是微服务互相调用情况 , 我不需要动态路由的话 , 这个参数只能填写 null 而且必须填写参数。

指定 Target

根据 FeignClientBuilder 手工创建 feign 实例,直接指定 FeignClientFactoryBeanname 属性 , 从而达到动态指定 URI


@Component
public class DynamicProcessFeignBuilder {private FeignClientBuilder feignClientBuilder;public DynamicProcessFeignBuilder(@Autowired ApplicationContext appContext) {this.feignClientBuilder = new FeignClientBuilder(appContext);}public <T> T build(String serviceId, Class<T> tClass) {return this.feignClientBuilder.forType(tClass, serviceId).build();}
}

上面操作如何达到动态指定 URI , 进行源码分析

org.springframework.cloud.openfeign.FeignClientBuilder 是建造者模式构造 Feign 使用的

org.springframework.cloud.openfeign.FeignClientBuilder.forType(java.lang.Class<T>, java.lang.String) 方法直接构造
feignClientFactoryBean

org.springframework.cloud.openfeign.FeignClientBuilder.Builder.Builder( org.springframework.context.ApplicationContext, org.springframework.cloud.openfeign.FeignClientFactoryBean, java.lang.Class<T>, java.lang.String) 方法里面设置 feignClientFactoryBeanname / contextId 等属性

调用方法 org.springframework.cloud.openfeign.FeignClientBuilder.Builder.build

最终在 org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget 方法中赋值 构造最终目标 Target 类和对应 Host 地址属性

public class FeignClientFactoryBeanimplements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {// 省略部分门源代码<T> T getTarget() {FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class): applicationContext.getBean(FeignContext.class);Feign.Builder builder = feign(context);if (!StringUtils.hasText(url)) {if (LOG.isInfoEnabled()) {LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");}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 FeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((FeignBlockingLoadBalancerClient) client).getDelegate();}if (client instanceof RetryableFeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();}builder.client(client);}applyBuildCustomizers(context, builder);Targeter targeter = get(context, Targeter.class);return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));}// 省略部分门源代码
}

核心问题

1.能否通过调用时候动态解析某些实体参数进行动态指定主机地址
2.feign 可以在创建实例时候使用不同的 feign.Target 类去指定和改变最终目的主机地址 , 能否有入口动态改变 feign.Target 从而达到动态路由的效果

结合 Capability / Encoder / RequestInterceptor 进行动态主机地址路由

自己通过另一种实现方式 , 但是不算优雅 , 分享一下 , Capability 接口 相当于 我们设计模式上的装饰者模式 , 我们可以装饰已经存在的 Encoder 重新提取包装数据

实现思路:

  • 我们需要拦截请求参数去自定义解析,提取对应的主机 Host 地址,根据官方文档,能获取原始参数的方法一般在 EncoderContract
    (这两个接口的实现只能是一个,不能使用多个,所以才考虑是使用 Capability 重新装饰), 本文是通过 Encoder 重新包装实现
  • 提取出来自定义主机 Host 地址以后,通过自定义 RequestInterceptor 请求拦截器直接动态指定主机 Host 地址

源码实现

动态路由参数接口

import java.util.Optional;public interface ICloudReq<C, D, ID> {ID getServerId();C setServerId(ID serverId);D getData();C setData(D data);default C self() {return (C) this;}default Optional<D> data() {return Optional.of(this).map(ICloudReq::getData);}
}

实现自定义 Encoder

import cn.hutool.core.util.StrUtil;
import com.e.cloudapi.pojo.param.req.ICloudReq;
import feign.RequestTemplate;
import feign.Target;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import lombok.extern.slf4j.Slf4j;import java.lang.reflect.Type;
import java.util.Objects;@Slf4j
public class FeignCloudReqEncoderDecorator implements Encoder {public static final String HEADER_DYNAMIC_CLIENT_NAME = "CLOUD_DYNAMIC_CLIENT";private final Encoder encoder;public FeignCloudReqEncoderDecorator(Encoder encoder) {Objects.requireNonNull(encoder);this.encoder = encoder;}@Overridepublic void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {log.debug("[{}] encode {}", encoder.getClass().getSimpleName(), bodyType);// 原来的逻辑继续走encoder.encode(object, bodyType, template);log.debug("[{}] encode {}", getClass().getSimpleName(), bodyType);// 新逻辑extractTargetUrlHeader(object, bodyType, template);}private void extractTargetUrlHeader(Object object, Type bodyType, RequestTemplate template) {if (object == null) {return;}if (!(object instanceof ICloudReq)) {return;}// 判断参数类型,如果匹配,直接提取相应的主机路由地址ICloudReq<?, ?, ?> req = (ICloudReq<?, ?, ?>) object;Object o = req.getServerId();if (Objects.isNull(o)) {return;}String serverId = o.toString();if (StrUtil.isBlank(serverId)) {log.warn("{} contains empty server id,not inject dynamic client name", object.getClass().getSimpleName());return;}Target<?> target = template.feignTarget();String name = target.name();// 提取出来的参数往 RequestTemplate 请求头添加template.header(HEADER_DYNAMIC_CLIENT_NAME, serverId);log.debug("inject dynamic client name header [{}]:[{}]->[{}]", HEADER_DYNAMIC_CLIENT_NAME, name, serverId);}
}

实现自定义 RequestInterceptor

import cn.hutool.core.util.StrUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Target;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;import java.util.Collection;
import java.util.Map;import static com.e.cmdb.feign.FeignCloudReqEncoderDecorator.HEADER_DYNAMIC_CLIENT_NAME;@Slf4j
@Configuration
public class FeignCloudReqInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {Map<String, Collection<String>> headers = template.headers();if (!headers.containsKey(HEADER_DYNAMIC_CLIENT_NAME)) {return;}// 获取请求头headers.get(HEADER_DYNAMIC_CLIENT_NAME).stream().findFirst().filter(StrUtil::isNotBlank).ifPresent(serverName -> injectClientNameHeader(template, serverName));}private static void injectClientNameHeader(RequestTemplate template, String serverName) {// 提取原来的 Target 信息Target<?> target = template.feignTarget();String url = target.url();if (StrUtil.isBlank(url)) {return;}// 替换成新的路由地址String targetUrl = StrUtil.replaceFirst(url, target.name(), serverName);log.debug("Rewrite template target:{},url:[{}]->[{}]", serverName, url, targetUrl);// 直接设置目标路由template.target(targetUrl);// 移除 RequestTemplate 刚才填充的请求头,因为请求不需要发送template.removeHeader(HEADER_DYNAMIC_CLIENT_NAME);}
}

实现自定义 Capability

import feign.Capability;
import feign.codec.Encoder;
import org.springframework.context.annotation.Configuration;@Configuration
public class FeignCloudReqCapability implements Capability {@Overridepublic Encoder enrich(Encoder encoder) {// 装饰者模式,附加功能return new FeignCloudReqEncoderDecorator(encoder);}
}

总结

  • 可以通过参数内容动态改变主机路由地址
  • 暂时没发现其他的入口可以做目标路由的替换,只能以这一种方式实现,在原有基础上不要做太大的改动就可以实现功能

http://www.ppmy.cn/news/451478.html

相关文章

CB备份一体机恢复

一、查看备份情况。 点击虚拟机主机名&#xff0c;查看详细 二、选中还原&#xff0c;展开文件系统。 三、点击主机名&#xff0c;查看备份详情&#xff0c;点击恢复。 四、异地恢复

监测机房蓄电池,原来这么简单

机房作为存储、交换、传输等所有信息网络工程功能的载体&#xff0c;必须对机房内的物理运行环境、配电、设备运行、人员活动、消防情况等进行24小时实时监控和智能调控。 传统蓄电池管理3大常见弊端 01.机房发生停电、市电异常等情况时&#xff0c;蓄电池用电数据没有实时监控…

电池管理系统(BMS)功能与作用/BMS 故障分析方法/15种常见故障案例分析

提示&#xff1a;本篇文章仅供学习参考 文章目录 一、电池管理系统&#xff08;BMS&#xff09;功能与作用二、BMS 故障分析方法三、15种常见故障案例分析 一、电池管理系统&#xff08;BMS&#xff09;功能与作用 从整车角度&#xff0c;电池管理系统&#xff08;BMS&#xf…

新能源锂电池极片制造设备如何实现故障智能诊断?

近年来&#xff0c;受国家新能源政策以及新能源汽车的快速发展&#xff0c;锂电池产能急剧上升&#xff0c;这对锂电企业与相关生产设备都是极大的考验。本文主要基于PreMaint在锂电行业的大量实践&#xff0c;分享锂电池极片制造设备实现故障智能诊断和智能运维的可落地方案。…

笔记本电池修复常见方法大全

笔记本电池用一段时间之后都会出现不同程度的损伤&#xff0c;在某些不利于电池运行的情况下甚至损耗率可以达到60%以上&#xff0c;在这种情况下电池一般支撑不了40分钟&#xff0c;很不方便&#xff0c;怎么修复笔记本电池&#xff1f;下面就为大家介绍笔记本电池修复常见方法…

电池管理系统(BMS)作用、故障分析方法及常见故障分析

电池管理系统&#xff08;BATTERY MANAGEMENT SYSTEM&#xff09;&#xff0c;俗称电池保姆或电池管家&#xff0c;是连接车载动力电池和电动汽车的重要纽带&#xff0c;其主要功能包括&#xff1a;电池物理参数实时监测&#xff1b;电池状态估计&#xff1b;在线诊断与预警&am…

机器视觉运动控制一体机应用例程|锂电池组装线上的读码应用

应用背景 读码识别技术作为工业物联网信息收集的关键途径&#xff0c;它在生产型企业中得到了广泛的应用。 锂电池在新能源汽车、消费电子和储能领域都有着不可或缺的地位。 它也与人的生命安全保证密切相关。 因此&#xff0c;锂电池从原材料到生产、组装、运输的过程&#…

蓄电池充放电一体机(铅酸电池组和铁锂电池组)放电、充电、循环充电设备应用

FGCD系列充放电一体机是福光电子新研发的电池组智能型充放电一体机&#xff0c;采用先进的充放电技术&#xff0c;根据铅酸电池和铁锂电池的充放电特点&#xff0c;内置了多种的测试维护模式&#xff0c;适用于市面上各种铅酸电池组和铁锂电池组的放电、充电、循环充放等测试。…