feign技巧 - 同时支持基于url和服务名的调度
0. 目录
-
-
- 1. 背景
- 2. 实现
- 3. 相关
-
1. 背景
本文尝试解决在Feigin使用过程中,希望定义的接口:
- 既支持基于服务名的负载均衡调度的请求调用;
- 又支持基于指定url地址的请求调用。
2. 实现
在前面的feign源码解析 - 初始化我们顺带介绍过可以通过"在方法参数上附加一个URI
类型参数,来实现在运行时动态指定目标服务地址"。这种方式是存在一定缺陷的 —— 那就是你在定义方法所在的接口时,配置的@FeignClients
必须对其url属性进行显式赋值。于是矛盾就出现了:
- 如果对
@FeignClients
的url属性进行了显式赋值,那我们在使用feign方法发起请求时,就会失去"基于服务名的负载均衡调度"能力。 - 如果不对
@FeignClients
的url属性进行了显式赋值,虽然获得了"基于服务名的负载均衡调度"能力,但之后通过feign接口发起请求调用时,默认feign会将你传入URI
方法参数中的ip地址作为服务名去寻找对应的目标主机,而很明显其并不存在。
综上,实现思路也就是浮出水面了 —— 默认启用"基于服务名的负载均衡调度"能力,通过自定义扩展,在用户传入URI
类型参数时,将发起请求的方式修改为直接基于传入的URI
代表的地址。
样例代码如下:
// 配置// ============ 同时支持url和服务名// 只需要向容器中注入自定义的Client实现, 就算是完成了绝大部分的扩展操作。@Beanpublic Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory,okhttp3.OkHttpClient okHttpClient) {final OkHttpClient delegate = new OkHttpClient(okHttpClient);return new LoadBalancerFeignClientEx(delegate, cachingFactory, clientFactory);}// 注意这里的 implements Client 不能省略public static class LoadBalancerFeignClientEx extends LoadBalancerFeignClient implements Client {// 代表当前不指示feign发起请求时的url地址, 采用"服务名"的形式进行标准的负载均衡调用// BuildTemplateByResolvingArgs.create(...) 中会校验URI类型参数, 不允许为null, 于是我们采用 http://__NONE__这样一个固定值来内部约定当前是需要进行标准的负载均衡调度public static final String NONE_ULI_STR = "__NONE__";public static final URI NONE_URI = URLUtil.toURI("http://" + NONE_ULI_STR);private final Client delegate;public LoadBalancerFeignClientEx(Client delegate, CachingSpringLoadBalancerFactory lbClientFactory,SpringClientFactory clientFactory) {super(delegate, lbClientFactory, clientFactory);this.delegate = delegate;}@Overridepublic Response execute(Request request, Options options) throws IOException {final URI asUri = URI.create(request.url());final String clientName = asUri.getHost();// Validator是hutool中的工具类if (Validator.isIpv4(clientName)) {// 直接调度return delegate.execute(request, options);} else {if (clientName.equals(NONE_ULI_STR)) {// 这里有个隐含的前提: hystrix的新建线程名采用的是默认的 hystrix-{servicename}-{num}final String currentThreadName = Thread.currentThread().getName();final String newUrl = request.url().replace(NONE_ULI_STR,StrUtil.split(currentThreadName, '-').get(1));ReflectUtil.setFieldValue(request, "url", newUrl);}// 基于服务名的负载均衡调度return super.execute(request, options);}}}// 应用
定义feign接口
// 实现:
// 1. 方法中如果传递的 URI类型参数不为null, 则按照指定的url进行发送请求
// 2. 方法中如果传递的 URI类型参数为null, 则按照标准微服务名选举节点之后进行发送请求
// ========================================================
// 注意:
// 1. @FeignClient url不要配置, 让 FeignClientFactoryBean.getTarget()方法中认为当前是LoadBalancer, 这样逻辑进入 LoadBalancerFeignClientEx 时救可以正常生效了
// 2. BuildTemplateByResolvingArgs.create(...) 中会校验URI参数, 不允许为null, 于是我们采用 http://__NONE__这样一个固定值来代表需要进行标准的负载均衡调度
@FeignClient(name = "projectB3"/*, url = "http://127.0.0.1:801"*/, fallbackFactory = FeignCallServiceFallbackFactory.class, configuration = FeignLoggerConfig.class)
public interface FeignDynamicHostCallService3 {/*** <p> 有时候,我们可能会需要动态更改请求地址的host,也就是@FeignClient中的url的值在我调用的是才确定。* <p> 在定义的接口的方法中,添加一个URI类型的参数即可,该值就是新的host。此时@FeignClient中的url值在该方法中将不再生效。* <p> 影响的是{@code MethodMetadata.urlIndex}字段* @param name* @param newHost* @return*/@RequestMapping(value = "/projectB/{name}", method = RequestMethod.GET)String callWithDynamicHost3(@PathVariable(value = "name") String name, URI newHost); 调用定义的feign接口@PostMapping("/dynamicHost/{name}")public String dynamicHost(@PathVariable String name) {// 抛出异常位置: BuildTemplateByResolvingArgs.create(Object[] argv)// 解析参数URI的位置: Contract.BaseContract.parseAndValidateMetadata(Class<?> targetType, Method method)System.out.println("PROJECT-B : " + feignDynamicHostCallService3.callWithDynamicHost3(name, LoadBalancerFeignClientEx.NONE_URI));System.out.println("PROJECT-B : "+ feignDynamicHostCallService3.callWithDynamicHost3(name, URLUtil.toURI("http://127.0.0.1:801")));return "hello";}
3. 相关
- feign源码解析 - 初始化
- feign源码解析 - 运行时
- Feign扩展 - 进程内调用