打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
代理与instanceof
userphoto

2023.10.10 北京

关注

为FeignClient自定义代理时,我被它的instanceof坑了一把。

背景

因为一项需求,我需要为Feign的Client做一套代理。代理嘛,并不难:

/** * 代理类。 * 当然,严格来说这是个装饰者模式。 * 不过,实践中谁还严格区分这个呢。 */public class MyFeignClient implements Client{    private final Client delegate;
public MyFeignClient(Client delegate){ this.delegate = delegate; }
@Override public Response execute(Request request, Request.Options options) throws IOException{ // 做一些业务逻辑,然后用delgate做正常的处理 return delegate.execute(request, options); }}
/** 借助BeanPostProcessor,把这个对象放到SpringIoC上下文中去 */public class MyFeignClientProcessor implements BeanPostProcessor{ @Override public Object postProcessAfterInitialization(Object bean, String beanName){ if(bean instanceof Client){ return new MyFeignClient((Client)bean); }else{ return bean; } }}

声明一下,代理类中扩展的业务逻辑与负载均衡毫无关系。

看起来没问题。写完之后,启动服务,调用一下……抛异常了。


问题

出问题的@FeignClient注解是这样配置的:

@FeignClient(name="RemoteServer", url="172.16.xxx.xxx")public interface RemoteService{    @GetMapping("/query")    RemoteResp query(@RequestParam("id") long id);}

调用代码与问题无关,就不贴了。

我这里配置了@FeignClient注解的url参数,主要有两个原因。

主要原因是,在生产环境上,为了调用未接入eureka的老系统,我们需要把url参数配置为该系统的网关地址。其次,在开发联调阶段,为了确保请求发送到联调方的开发环境上,我们也会配置url参数。

总之,配置url参数是个实打实的需求,不能忽视。

然而,配置了url参数之后,一调用这个接口就会抛出异常。直接拉到异常栈的最底层,可以看到这样的信息:

Caused by: com.netfix.client.clientException: Load balancer does not have available server for client : 172.16.xxx.xxx
at com.netflix.loadbalancer.LoadBalancerContext.getServerFromLoadBalancer

报错信息

粗略一看,“这个妹妹我见过的”。如果调用方和服务方没有注册到同一个eureka上,就会出现“Load balancer does not have available server for client”这个异常。这问题也很好解决,只需要统一调用方和服务方的eureka配置就好了嘛!

再仔细看看……奇怪了……


分析

奇怪的异常

这段异常有两个地方很奇怪。

首先,我明明在@FeignClient注解上配置了name参数,为什么出现在报错信息里的是注解中的url参数值呢?正常情况下,这里应该是注解中的name参数值,就像这样才对啊:

Caused by: com.netfix.client.clientException:Load balancer does not have available server for client : RemoteServer
at com.netflix.loadbalancer.LoadBalancerContext.getServerFromLoadBalancer(LoadBalancerContext.java:483)

报错信息

可是这次的异常信息,却是这样的:

Caused by: com.netfix.client.clientException: Load balancer does not have available server for client : 172.16.xxx.xxx
at com.netflix.loadbalancer.LoadBalancerContext.getServerFromLoadBalancer(LoadBalancerContext.java:483) 
异常信息

其次,我明明在注解上配置了url参数,为什么还会抛出软负载相关的异常呢?

众所周知,如果为@FeignClient注解配置了非空的url参数,Feign就会直接访问url指定地址,而不会触发本地软负载相关逻辑。

可是异常信息却明明白白地表明,问题出现在软负载相关代码中:

Caused by: com.netfix.client.clientException: Load balancer does not have available server for client : 172.16.xxx.xxx
at 
com.netflix.loadbalancer.LoadBalancerContext.getServerFromLoadBalancer(LoadBalancerContext.java:483)
异常信息

该用name参数的时候没用name参数;不该做软负载的时候做了软负载。奇怪,真是奇怪。

没头绪的代码

想了半天没有头绪,只好从Feign框架中代码处下手了。看来看去,在FeignClientFacotryBean中看到这样一段代码:

public class FeignClientFactoryBean{    <T> T getTarget() {       FeignContext context = this.applicationContext.getBean(FeignContext.class);       Feign.Builder builder = feign(context);
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(); // 代码位置1 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(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); return (T) targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url)); } // 其它代码略}

由于我们配置了url参数,所以这里会跳过“if (!StringUtils.hasText(this.url)) ”相关处理。因此,我们可以直接从“代码位置1”开始。

在debug模式下可以看到,在“Client client = getOptional(context, Client.class);”这一行,获取到的确实是MyFeignClient实例。继续往下执行,在“if (client instanceof LoadBalancerFeignClient)”处,显然判断结果为false,可以跳过这个if块。再继续执行……咦,后面的逻辑好像都和负载均衡无关啊。

那这个异常是从哪儿抛出来的?没头绪,实在没头绪。

无语的原因

没头绪,只好再捋一捋FeignClientFactoryBean中的代码。

"if (!StringUtils.hasText(this.url))"……嗯……"if(client != null)"……嗯……“if (client instanceof LoadBalancerFeignClient)”……嗯?"if (client instanceof LoadBalancerFeignClient)"?

为什么这里要针对LoadBalancerFeignClient做一个特殊处理呢?

如果没有通过"if (!StringUtils.hasText(this.url)) "判断,说明@FeignClient注解中配置了url参数。配置了url参数时,就应该按照url指定的地址进行调用,而不应该再做软负载相关处理。软负载的功能恰恰由LoadBalancerFeignClient提供。不想调用软负载逻辑的话,就必须把LoadBalancerFeignClient中的delegate剥离出来,用这个不带软负载功能的对象来发送请求。这就是对LoadBalanceFeignClient做特殊处理的目的。

想到这里,我突然灵光一闪:既然这里判断了"if (client instanceof LoadBalancerFeignClient)",那是不是意味着,如果没有MyFeignClient,这里获取到的client可能会是LoadBalancerFeignClient?这么说的话,当MyFeignClientProcessor装配MyFeignClient时,组装到delegate中的对象,会不会也是一个LoadBalancerFeignClient实例呢?

事实果真如此。无论用debug模式、还是在MyFeignClientProcessor中增加日志,或者是查看完整的异常信息,都可以确定这一点。例如,在异常信息中就有这样两行,清楚地说明了MyFeignClient内调用的正是LoadBalancerFeignClient:

at org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient.execute(LoadBalancerFeignClient.java:73)
at com.xxx.service.MyFeignClient.execute(MyFeignClient.java:xxx)
与业务类相关的异常信息

问题原因终于浮出了水面。

由于内部的delegate是一个LoadBalancerFeignClient对象,因而MyFeignClient也拥有了软负载能力。同时,由于无法通过"if (client instanceof LoadBalancerFeignClient)"判断,因而FeignClientFactoryBean无法将软负载能力从MyFeignClient中剥离。这样一来,系统最终得到的FeignClient实例就一定会进入软负载流程。

通过调用时序图可以将异常过程看得很清楚。当发生调用,Feign就会通过MyFeignClient调用到它代理的LoadBalanceFeignClient上。而LoadBalcanceFeignClient会使用url参数中的ip地址去做软负载,最终抛出异常。

出问题时的时序图

为什么异常信息里是@FeignClient中的url而不是name,为什么指定了url却还会触发软负载逻辑,全都解释得通了。

问题查到这里,我实在是有点上火。代理的应用很广泛,尤其在SpringIoC的协助下,几乎成为了扩展框架、自定义组件的不二之选。可是instanceof却是代理的死敌。它只认特定的类及其子类,而不认不在这套继承体系内、但拥有同样的甚至更强大能力的代理类。

就好比……升职加薪的时候,老板只看你是不是xxx的亲戚,而不看你是不是有业绩、能力和证书。

在技术的世界里居然也会遇到这种只看身份、不看能力的事儿。无语,太无语了。


方案

无语归无语,问题还是要解决掉。

方案一:修改FeignClientFactoryBean

因为实在太无语,我第一反应是修改FeignClientFactoryBean。这显然行不通。

方案二:修改代理关系

第二个想法更现实些。如果我能够把MyFeignClient、LoadBalancerFeignClient和OkHttpFeignClient的代理关系调整一下,变成下图这样,不就能解决问题了么:

修改几个类之间的代理关系

这个想法很好。可是当我看到LoadBalanerFeignClient和OkHttpClient之间的构建关系时……

class OkHttpFeignLoadBalancedConfiguraion{    @Bean    @ConditionalOnMissingBean(Client.class)    public Client feignClient(CachingSpringLoadBalancerFactory factory,                                SpringClientFactory clientFactory,                                okhttp3.OkHttpClient okHttpClient){        OkHttpClient delegate = new OkHttpClient(okHttpClient);        return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);    }    // 其它,略}

如果要按这个方案改,那么有两个选择。一种是在这个方法之前注入MyFeignClient:

class MyFeignClientConfiguraion{    @Bean    public Client feignClient(CachingSpringLoadBalancerFactory factory,                                SpringClientFactory clientFactory,                                okhttp3.OkHttpClient okHttpClient){        OkHttpClient delegate = new OkHttpClient(okHttpClient);        MyFeignClient myFeignClint = new MyFeignClient(delegate);        return new LoadBalancerFeignClient(myFeignClint, cachingFactory, clientFactory);    }    // 其它,略}

但这样一来,势必要给默认的Client、Apache的Client……等各种Client都要重写一套配置。不仅麻烦,而且容易出现疏漏。

另一种方法是在MyFeignClientProcessor中,替换掉LoadBalancerFeignClient的delegate属性。麻烦之处在于,LoadBalancerFeignClient类只提供了getDelegate()方法,没有提供setDelegate(Client)方法。如果要替换这个字段,必须借助反射:

public class MyFeignClientProcessor implements BeanPostProcessor{    @Override    public Object postProcessAfterInitialization(Object bean, String beanName){        if(bean instanceof LoadBalancerFeignClient){            LoadBalancerFeignClient lbClient = (LoadBalancerFeignClient)bean;            MyFeignClient myFeignClient = new MyFeignClient(lbClient.getDelegate());                        // 使用反射,将lbClient.delegate赋值为myFeignClient。这里略                        return lbClient;        }else if(bean instanceof Client){            return new MyFeignClient((Client)bean);        }else{            return bean;        }    }}

这样,无论LoadBalancerFeignClient中的delegate是什么类,都可以轻松地转换为MyFeignClient。最大的问题在于这里使用了反射。个人不太喜欢反射……于是就有了第三个方案。

方案三:提供LoadBalancerFeignClient子类

第三个方案其实有点头痛医头、脚痛医脚。既然框架里必须判断 "client instanceof LoadBalancerFeignClient",那我提供一个LoadBalancerFeignClient的子类不就行了嘛:

public class MyLoadBalancerFeignClient extends LoadBalancerFeignClient{    public MyLoadBalancerFeignClient(Client delegate,                                        CachingSpringLoadBalancerFactory factory,                                        SpringClientFactory clientFactory){        super(delegate, factory, clientFactory);                                                                                        }        @Override    public Response execute(Request reuqest, Options options)throws IOException{        // 处理业务逻辑,略        //然后调用父类逻辑处理        return super.execute(request, options);    }}
/** 对应的BeanPostProcessor也要做点修改 */public class MyFeignClientProcessor implements BeanPostProcessor{ @Autowired private CachingSpringLoadBalancerFactory factory, @Autowired private SpringClientFactory clientFactory @Override public Object postProcessAfterInitialization(Object bean, String beanName){ if(bean instanceof LoanBalancerFeignClient){ // 新增针对性的处理 LoanBalancerFeignClient lbClient = (LoanBalancerFeignClient)bean; Client delegate = lbClient.getDelegate(); return new MyLoadBalancerFeignClient(delegate, factory, clientFactory); }else if(bean instanceof Client){ // 保留原逻辑 return new MyFeignClient((Client)bean); }else{ return bean; } }}

这个方案也确实可行。与反射方案孰优孰劣,各位可以自己判断。

回到方案一:如果是我,会怎么做?

解决具体问题之后,我忍不住想入非非了一下。这个FeignClientFactoryBean,如果一开始就由我来写,为了让这个instanceof和代理兼容,我会怎么做呢?

我首先想到的是在Client接口上加一个方法,用以标记该对象是否支持负载均衡。这样,原有的instanceof判断就可以改为用这个方法来判断了:

public interface Client{    /** 已有方法 */    Response execute(Request request, Options options)throws IOException;        /** 新增方法,判断是否支持本地负载均衡 */    default boolean isLoadBalancable(){return false;}}

看起来不错,实际上不太合适。Client接口的调用方并不关心这个对象是否支持负载均衡——最多在装配时关心,而在运行期不关心。在这个接口上增加方法,等于把不必要的底层细节暴露了出去。此外,如果加了这个方法,势必还要在Client接口上再加个getDelegate()方法。这就更糟糕了。

更进一步说,因为instanceof和代理不能共存,就打算把instanceof彻底踢出局,这种想法未免太二极管了一些。我并不需要二选一,如果能调和矛盾、兼容并蓄,那也是皆大欢喜。

比如,既然isLoadBalancable()方法不适合放到Client接口内,那可以考虑把它放在一个新的接口里:

public interface LoadBalancable{    Client removeLoanBalance();}
这样,FeignClientFactoryBean就可以这样实现:
public class FeignClientFactoryBean{    <T> T getTarget() {       FeignContext context = this.applicationContext.getBean(FeignContext.class);       Feign.Builder builder = feign(context);           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 LoadBalancable) {             // not load balancing because we have a url,             // but ribbon is on the classpath, so unwrap             client = ((LoadBalancable) client).removeLoanBalance();          }          builder.client(client);       }       Targeter targeter = get(context, Targeter.class);       return (T) targeter.target(this, builder, context,             new HardCodedTarget<>(this.type, this.name, url));    }    // 其它代码略}

虽然也用到了instanceof,但只要我们的代理这样写,就可以与之兼容:

public class MyFeignClient implements Client, LoadBalancable{    private final Client delegate;        public MyFeignClient(Client delegate){        this.delegate = delegate;        }        @Override    public Response execute(Request request, Request.Options options) throws IOException{        // 做一些业务逻辑,然后用delgate做正常的处理        return delegate.execute(request, options);    }        /**     * 新的方法在这里。当前代理是否支持负载均衡、是否移除负载均衡能力,都可以委托给delegate来处理。     * 当然,如果delegate不支持负载均衡,那当前类也不行。直接返回自己即可。     */    public Client removeLoanBalance(){        if(delegate instanceof LoadBalancable){            return ((LoadBalancable) delegate).removeLoanBalance();                } else{            return this;                }       }}

写到这里发现,其实深层问题并不是insanceof与代理水火不容,而是原生的LoanBalancerFeignClient类难以扩展。如果像我上面这样给它定义一个接口,或者在OkHttpFeignLoadBalancedConfiguraion中使用更灵活的依赖注入方式,我都不用这样大费周章的搞这么些方案。

嗯,定义一个接口,使用更灵活的依赖注入方式,这其实都是依赖倒置原则的要求……不过这是另外的话题,就此打住吧。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
SpringMVC学习笔记
Retrofit 动态管理和修改 BaseUrl,从未如此简单
nginx upstream的五种分配方式
Spring Cloud OpenFeign 动态Url
JMS与Spring之二(用message listener container异步收发消息)
【第四章】资源之4.3访问Resource——跟我学spring3
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服