feign 基于參數(shù)動(dòng)態(tài)指定路由主機(jī)

feign 基于參數(shù)動(dòng)態(tài)指定路由主機(jī)

背景

項(xiàng)目上最近有需求:通過(guò)一個(gè)公共基礎(chǔ)實(shí)體定義一個(gè)主機(jī)地址字段 , feign 遠(yuǎn)程調(diào)用時(shí)候根據(jù)具體值動(dòng)態(tài)改變進(jìn)行調(diào)用。

官方解決方案

第一種方案

官方支持動(dòng)態(tài)指定 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);

根據(jù)文檔的相關(guān)指引 , 需要提供一個(gè) URI 參數(shù)就可以動(dòng)態(tài)指定目標(biāo)主機(jī) , 可以實(shí)現(xiàn)動(dòng)態(tài)路由放典。

URI 方式源碼分析

官方 URI 動(dòng)態(tài)改變主機(jī)源碼解析:

Contract 類是 feign 用于提取有效信息到元信息存儲(chǔ)

feign.Contract.BaseContract.parseAndValidateMetadata(java.lang.Class<?>, java.lang.reflect.Method)
方法解析元數(shù)據(jù)時(shí)候 , 判斷參數(shù)類型是否為 URI 類型 , 然后記錄下參數(shù)位置

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

feign.ReflectiveFeign.BuildTemplateByResolvingArgs.create 方法執(zhí)行初始化 RequestTemplate 時(shí)候 , 根據(jù) urlIndex()
是否為 null , 直接設(shè)置 feign.RequestTemplate.target 方法改變最終目標(biāo)地址

private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {
    // ...
    @Override
    public 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 方式優(yōu)缺點(diǎn)

優(yōu)點(diǎn):直接 , 直接傳入目標(biāo)主機(jī)地址可以直接實(shí)現(xiàn)動(dòng)態(tài)路由

缺點(diǎn):如果是普通三方調(diào)用接口形式的話 , 使用起來(lái)問(wèn)題不大;但是我們?nèi)绻俏⒎?wù)的模式 , 我們經(jīng)常會(huì)定義一個(gè) API
接口 , FeignClient 客戶端和 Controller 層同時(shí)實(shí)現(xiàn) , 如果多一個(gè) URI 參數(shù)情況下 , 需要遠(yuǎn)程調(diào)用又不需要改變路由 , 會(huì)導(dǎo)致我們需要多填寫(xiě)一個(gè)參數(shù),請(qǐng)看下面的案例:

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
    @Override
    public Result<AccountCreateDTO> saveAccount(@RequestBody BaseCloudReq<AccountCreateReq> req) {
        // ...
        return Result.success(accountService.saveAccount(request));
    }
}

上面案例會(huì)有以下問(wèn)題:

  • 我需要改變 @FeignClient 注解的 value 值 , 只能通過(guò)參數(shù) URI 指定 , 需要加一個(gè) URI 參數(shù)
  • 如果根據(jù)上面第一點(diǎn)是微服務(wù)互相調(diào)用情況 , 我不需要?jiǎng)討B(tài)路由的話 , 這個(gè)參數(shù)只能填寫(xiě) null 而且必須填寫(xiě)參數(shù)岂膳。

指定 Target

根據(jù) FeignClientBuilder 手工創(chuàng)建 feign 實(shí)例,直接指定 FeignClientFactoryBeanname 屬性 , 從而達(dá)到動(dòng)態(tài)指定 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();
    }
}

上面操作如何達(dá)到動(dòng)態(tài)指定 URI , 進(jìn)行源碼分析

org.springframework.cloud.openfeign.FeignClientBuilder 是建造者模式構(gòu)造 Feign 使用的

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

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

調(diào)用方法 org.springframework.cloud.openfeign.FeignClientBuilder.Builder.build

最終在 org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget 方法中賦值 構(gòu)造最終目標(biāo) Target 類和對(duì)應(yīng) Host 地址屬性

public class FeignClientFactoryBean
        implements 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 unwrap
                client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
            }
            if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
                // not load balancing because we have a url,
                // but Spring Cloud LoadBalancer is on the classpath, so unwrap
                client = ((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));
    }
    // 省略部分門源代碼
}

核心問(wèn)題

1.能否通過(guò)調(diào)用時(shí)候動(dòng)態(tài)解析某些實(shí)體參數(shù)進(jìn)行動(dòng)態(tài)指定主機(jī)地址
2.feign 可以在創(chuàng)建實(shí)例時(shí)候使用不同的 feign.Target 類去指定和改變最終目的主機(jī)地址 , 能否有入口動(dòng)態(tài)改變 feign.Target 從而達(dá)到動(dòng)態(tài)路由的效果

結(jié)合 Capability / Encoder / RequestInterceptor 進(jìn)行動(dòng)態(tài)主機(jī)地址路由

自己通過(guò)另一種實(shí)現(xiàn)方式 , 但是不算優(yōu)雅 , 分享一下 , Capability 接口 相當(dāng)于 我們?cè)O(shè)計(jì)模式上的裝飾者模式 , 我們可以裝飾已經(jīng)存在的 Encoder 重新提取包裝數(shù)據(jù)

實(shí)現(xiàn)思路:

  • 我們需要攔截請(qǐng)求參數(shù)去自定義解析,提取對(duì)應(yīng)的主機(jī) Host 地址,根據(jù)官方文檔,能獲取原始參數(shù)的方法一般在 EncoderContract
    (這兩個(gè)接口的實(shí)現(xiàn)只能是一個(gè),不能使用多個(gè)粒竖,所以才考慮是使用 Capability 重新裝飾), 本文是通過(guò) Encoder 重新包裝實(shí)現(xiàn)
  • 提取出來(lái)自定義主機(jī) Host 地址以后,通過(guò)自定義 RequestInterceptor 請(qǐng)求攔截器直接動(dòng)態(tài)指定主機(jī) Host 地址

源碼實(shí)現(xiàn)

動(dòng)態(tài)路由參數(shù)接口

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);
    }
}

實(shí)現(xiàn)自定義 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;
    }

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        log.debug("[{}] encode {}", encoder.getClass().getSimpleName(), bodyType);

        // 原來(lái)的邏輯繼續(xù)走
        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;
        }

        // 判斷參數(shù)類型几于,如果匹配蕊苗,直接提取相應(yīng)的主機(jī)路由地址
        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();

        // 提取出來(lái)的參數(shù)往 RequestTemplate 請(qǐng)求頭添加
        template.header(HEADER_DYNAMIC_CLIENT_NAME, serverId);
        log.debug("inject dynamic client name header [{}]:[{}]->[{}]", HEADER_DYNAMIC_CLIENT_NAME, name, serverId);
    }
}

實(shí)現(xiàn)自定義 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 {
    @Override
    public void apply(RequestTemplate template) {
        Map<String, Collection<String>> headers = template.headers();
        if (!headers.containsKey(HEADER_DYNAMIC_CLIENT_NAME)) {
            return;
        }

        // 獲取請(qǐng)求頭
        headers.get(HEADER_DYNAMIC_CLIENT_NAME)
                .stream()
                .findFirst()
                .filter(StrUtil::isNotBlank)
                .ifPresent(serverName -> injectClientNameHeader(template, serverName));
    }

    private static void injectClientNameHeader(RequestTemplate template, String serverName) {
        // 提取原來(lái)的 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);

        // 直接設(shè)置目標(biāo)路由
        template.target(targetUrl);
        // 移除 RequestTemplate 剛才填充的請(qǐng)求頭,因?yàn)檎?qǐng)求不需要發(fā)送
        template.removeHeader(HEADER_DYNAMIC_CLIENT_NAME);
    }
}


實(shí)現(xiàn)自定義 Capability

import feign.Capability;
import feign.codec.Encoder;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignCloudReqCapability implements Capability {
    @Override
    public Encoder enrich(Encoder encoder) {
        // 裝飾者模式沿彭,附加功能
        return new FeignCloudReqEncoderDecorator(encoder);
    }
}

總結(jié)

  • 可以通過(guò)參數(shù)內(nèi)容動(dòng)態(tài)改變主機(jī)路由地址
  • 暫時(shí)沒(méi)發(fā)現(xiàn)其他的入口可以做目標(biāo)路由的替換朽砰,只能以這一種方式實(shí)現(xiàn),在原有基礎(chǔ)上不要做太大的改動(dòng)就可以實(shí)現(xiàn)功能
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末喉刘,一起剝皮案震驚了整個(gè)濱河市瞧柔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌睦裳,老刑警劉巖造锅,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異廉邑,居然都是意外死亡哥蔚,警方通過(guò)查閱死者的電腦和手機(jī)倒谷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)糙箍,“玉大人渤愁,你說(shuō)我怎么就攤上這事∩詈唬” “怎么了抖格?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)塌西。 經(jīng)常有香客問(wèn)我他挎,道長(zhǎng),這世上最難降的妖魔是什么捡需? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任办桨,我火速辦了婚禮,結(jié)果婚禮上站辉,老公的妹妹穿的比我還像新娘呢撞。我一直安慰自己,他們只是感情好饰剥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布殊霞。 她就那樣靜靜地躺著,像睡著了一般汰蓉。 火紅的嫁衣襯著肌膚如雪绷蹲。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天顾孽,我揣著相機(jī)與錄音祝钢,去河邊找鬼。 笑死若厚,一個(gè)胖子當(dāng)著我的面吹牛拦英,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播测秸,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼疤估,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了霎冯?” 一聲冷哼從身側(cè)響起铃拇,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沈撞,沒(méi)想到半個(gè)月后锚贱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡关串,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年拧廊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了监徘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吧碾,死狀恐怖凰盔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情倦春,我是刑警寧澤户敬,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站睁本,受9級(jí)特大地震影響尿庐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呢堰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一抄瑟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧枉疼,春花似錦皮假、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至航闺,卻和暖如春褪测,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背潦刃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工侮措, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人福铅。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像项阴,于是被迫代替她去往敵國(guó)和親滑黔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容