基于Feign的局部請(qǐng)求攔截

由于項(xiàng)目的要求,不能對(duì)所有基于Feign的進(jìn)行攔截辐益,需要對(duì)不同的Feign請(qǐng)求進(jìn)行不同的攔截,經(jīng)過(guò)資料的收集整理以及SpringCloud中對(duì)于Feign的集成的源碼的閱讀,解決了針對(duì)Feign請(qǐng)求的局部攔截

本項(xiàng)目中SpringCloud的版本是Camden.SR6版本

背景說(shuō)明

在既有的項(xiàng)目上進(jìn)行二次開(kāi)發(fā)贴妻,服務(wù)A需要請(qǐng)求服務(wù)B同時(shí)需要將服務(wù)A中請(qǐng)求的消息頭相關(guān)信息傳送給服務(wù)B,但是由于既有項(xiàng)目中的相關(guān)設(shè)計(jì)蝙斜,不支持feign請(qǐng)求的全局?jǐn)r截名惩,只能針對(duì)服務(wù)A請(qǐng)求服務(wù)B的feign請(qǐng)求進(jìn)行攔截,所以開(kāi)發(fā)了如下的方法孕荠;

這里說(shuō)明下娩鹉,之所以采用Feign是由于Feign添加支持負(fù)載均衡攻谁,這點(diǎn)尤為重要。

思路說(shuō)明

既然當(dāng)前SpringCloud的版本不支持Feign請(qǐng)求的攔截弯予,那么只能自己開(kāi)發(fā)攔截的方法來(lái)攔截Feign請(qǐng)求了戚宦,整理資料有如下兩種思路:

  1. Feign內(nèi)部也是使用Ribbon來(lái)完成支持負(fù)載均衡的,所以拋開(kāi)Feign,直接使用Ribbon也是可以的锈嫩;

為了擴(kuò)展方便受楼,可以采用掃描自定義注解和AOP攔截的方式,然后通過(guò)前置方法將消息頭相關(guān)內(nèi)容存儲(chǔ)到請(qǐng)求中

這個(gè)思路簡(jiǎn)單易用呼寸,而且方法都是自己開(kāi)發(fā)的艳汽,出現(xiàn)問(wèn)題,定位和修改都是很容易的对雪,但是這種方法也相當(dāng)于重新開(kāi)發(fā)了一種新的功能河狐,工作量和代碼量肯定是不小的,

  1. 還是使用Feign,既然SpringCloud當(dāng)前版本不支持瑟捣,那么就利用原生的Feign來(lái)自己封裝馋艺;

SpringCloud的@FeignClient也是基于原生的Feign的基礎(chǔ)上進(jìn)行封裝的,所以我們也可以開(kāi)發(fā)新的封裝蝶柿,使之支持目前的需求丈钙,對(duì)Feign的請(qǐng)求進(jìn)行局部攔截

如果想進(jìn)行新的封裝,我們可以借鑒SPringCloud對(duì)Feign的封裝方法交汤,這里我們可以參考 FeignClient源碼深度解析這篇文章雏赦,說(shuō)的很詳細(xì),在這里感謝大佬的分享芙扎。

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

廢話不多說(shuō)星岗,為了讓代碼改動(dòng)量小,并且利用Feign的特性:(一個(gè)接口就可以訪問(wèn)其他的項(xiàng)目)戒洼,我們選擇第二種方法來(lái)實(shí)現(xiàn)

在這里對(duì)于SpringCloud支持Feign的封裝思路就顯得比較重要了俏橘,不過(guò)在這之前,我們可以使用原生的Feign來(lái)支持請(qǐng)求的攔截

首先是依賴的支持

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
        <version>1.2.6.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>com.netflix.feign</groupId>
        <artifactId>feign-ribbon</artifactId>
        <version>8.18.0</version>
    </dependency>
  1. 首先我們定義一個(gè)接口圈浇,該接口配置Feign訪問(wèn)其他項(xiàng)目的路徑
    /**
     * @author: amos
     * @Description: 訪問(wèn)其他業(yè)務(wù)的請(qǐng)求
     * @date: 2019/12/23 0023 下午 17:39
     * @Version: V1.0
     */
    public interface BizClient {
        @RequestMapping(value = "/biz/list", method = RequestMethod.POST)
        Result list(@RequestBody BizDTO dto);
    }

注意:該接口上沒(méi)有添加 @FeignClient 注解寥掐,因?yàn)槟壳绊?xiàng)目是支持SpringCloud的Feign使用方式的,如果添加了注解磷蜀,就會(huì)直接走SpringCloud的Feign請(qǐng)求方式

  1. 原生的Feign使用方式
    /**
     * 
     * @author: amos
     * @Description: 基于原生的Feign請(qǐng)求來(lái)獲取請(qǐng)求訪問(wèn)對(duì)象     
     * @date: 2020/2/19 0019 下午 16:09
     * @Version: V1.0
     */
    @Configuration
    public class BasicFeignBuilderConfig {
    
        public Client client;
    
        private HttpMessageConverter jsonConverter;
    
        private ObjectFactory<HttpMessageConverters> converter;
        private static final String CLINET_URL = "http://APPLICATION-NAME";
        /**
         * 初始化client
         */
        @PostConstruct
        public void initClient() {
            this.client = RibbonClient.create();
            this.jsonConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper());
            this.converter = () -> new HttpMessageConverters(jsonConverter);
        }
         /**
         * 利用Feign來(lái)獲取接口訪問(wèn)對(duì)象
         *
         * @param clazz
         * @param <T>
         * @return
         */
        public <T> T feignBuilderRequestInterceptor(Class<T> clazz) {
            T t = Feign.builder()
                    .encoder(new SpringEncoder(converter))
                    .client(client)
                    .decoder(new SpringDecoder(converter))
                    .contract(new SpringMvcContract())
                    .requestInterceptor(new FeignBasicTenantIdRequestInterceptor())
                    .target(clazz, CLINET_URL);
            return t;
        }   
        /**
         * 將BizClient注冊(cè)到SpringContext的上下文中
         *
         * @return
         */
        @Bean("bizClient")
        public BizClient bizClient() {
            return this.feignBuilderRequestInterceptor(BizClient.class);
        }
    }

至此使用原生的Feign結(jié)束召耘,但是一測(cè)試就報(bào)錯(cuò)

java.lang.RuntimeException: com.netflix.client.ClientException: Load balancer does not have available server for client: APPLICATION-NAME

從網(wǎng)上查詢資料,并進(jìn)行了相關(guān)的依賴和配置項(xiàng) 都沒(méi)有生效

  1. 閱讀源碼褐隆,了解SpringCloud支持Feign的原理

上面的辦法既然不可行污它,主要的問(wèn)題是Ribbon識(shí)別不了我們的實(shí)例名,也就是代碼中Client有問(wèn)題,但是SpringCloud的Feign卻是可以支持的衫贬,所以這里的關(guān)鍵就是SpringCloud中的Feign是怎么支持的Ribbon的,然后將他支持的方式移到我們目前代碼固惯,解決由于Ribbon造成的負(fù)載均衡的問(wèn)題就可以了梆造。

為此,我們需要閱讀SpringCloud支持Feign方面的相關(guān)的源碼缝呕,源碼的閱讀可以參考上面的博客鏈接澳窑,說(shuō)明的非常詳細(xì)斧散,下面我們主要分析下源碼中的代理工廠的代碼;

FeignClientFactoryBean這個(gè)類就是FeignClient的代理工廠類供常,我們看下工廠類的入口getObject()方法:

    @Override
    public Object getObject() throws Exception {
        // 從Spring的ApplicationContext中獲取FeignContext
        FeignContext context = applicationContext.getBean(FeignContext.class);
        // 利用構(gòu)造器來(lái)構(gòu)造Feign的對(duì)象
        Feign.Builder builder = feign(context);
        .....
    }

這里我們主要看下構(gòu)造器中是怎么獲取Client對(duì)象的,我們需要知道他是怎么處理支持負(fù)載均衡的鸡捐,我們追蹤到對(duì)應(yīng)的代碼:

    protected <T> T getOptional(FeignContext context, Class<T> type) {
        return context.getInstance(this.name, type);
    }
    
    protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
            HardCodedTarget<T> target) {
        // 這里就是我們需要的client對(duì)象 
        // 通過(guò)上面的代碼我們知道 FeignContext 中獲取對(duì)應(yīng)的Bean
        Client client = getOptional(context, Client.class);
        if (client != null) {
            builder.client(client);
            Targeter targeter = get(context, Targeter.class);
            return targeter.target(this, builder, context, target);
        }

        throw new IllegalStateException(
                "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?");
    }

至此栈暇,我們知道了Client對(duì)象主要來(lái)源于FeignContext中,而FeignContext是來(lái)源于ApplicationContext中箍镜,到這里就非常請(qǐng)求了源祈,我們需要從ApplicationContext中獲取FeignContext,然后再?gòu)腇eignContext中獲取Client對(duì)象色迂。

所以我們需要改造上面 BasicFeignBuilderConfig的代碼:

    /**
     * 
     * @author: amos
     * @Description: 基于原生的Feign請(qǐng)求來(lái)獲取請(qǐng)求訪問(wèn)對(duì)象     
     * @date: 2020/2/19 0019 下午 16:09
     * @Version: V1.0
     */
    @Configuration
    public class BasicFeignBuilderConfig implements ApplicationContextAware{
    
        public Client client;
    
        private HttpMessageConverter jsonConverter;
    
        private ObjectFactory<HttpMessageConverters> converter;
        
        private static final String CLINET_URL = "http://APPLICATION-NAME";
        
        private ApplicationContext applicationContext;

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
        /**
         * 初始化client
         */
        @PostConstruct
        public void initClient() {
            this.client =  FeignContext context = applicationContext.getBean(FeignContext.class);
            this.jsonConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper());
            this.converter = () -> new HttpMessageConverters(jsonConverter);
        }
         /**
         * 利用Feign來(lái)獲取接口訪問(wèn)對(duì)象
         *
         * @param clazz
         * @param <T>
         * @return
         */
        public <T> T feignBuilderRequestInterceptor(Class<T> clazz) {
            T t = Feign.builder()
                    .encoder(new SpringEncoder(converter))
                    .client(client)
                    .decoder(new SpringDecoder(converter))
                    .contract(new SpringMvcContract())
                    .requestInterceptor(new FeignBasicTenantIdRequestInterceptor())
                    .target(clazz, CLINET_URL);
            return t;
        }   
        /**
         * 將BizClient注冊(cè)到SpringContext的上下文中
         *
         * @return
         */
        @Bean("bizClient")
        public BizClient bizClient() {
            return this.feignBuilderRequestInterceptor(BizClient.class);
        }
    }

上面的代碼主要是 類實(shí)現(xiàn)ApplicationContextAware接口來(lái)獲取 ApplicationContext對(duì)象香缺,然后從ApplicationContext對(duì)象中獲取FeignContext對(duì)象,再獲取到我們需要的Client對(duì)象即可歇僧;

至此已經(jīng)完全結(jié)束了图张,我們可以在業(yè)務(wù)代碼中直接注入 BizClient 直接調(diào)用對(duì)應(yīng)得方法了。

    @Autowired
    BizClient bizClient
    
    public Result list(BizDTO dto){
        return bizClient.list(dto);
    }

上面的代碼還可以再進(jìn)行封裝诈悍,如果有多個(gè)BizClient的業(yè)務(wù)請(qǐng)求祸轮,可以通過(guò)自定義注解來(lái)實(shí)現(xiàn)系統(tǒng)在啟動(dòng)的時(shí)候,掃描自定義的注解侥钳,然后同樣利用代理工廠的方法生成實(shí)例對(duì)象适袜,然后注入到Spring的ApplicationContext中,方便業(yè)務(wù)直接拿來(lái)使用。

邏輯和Spring支持Feign的邏輯是一樣的舷夺,主要依賴ImportBeanDefinitionRegistrar苦酱、ResourceLoaderAwareBeanClassLoaderAware三個(gè)類给猾。

我會(huì)在下一篇博文中基于該方法來(lái)說(shuō)明疫萤,如何實(shí)現(xiàn)系統(tǒng)啟動(dòng)將自定義注解的bean注入到Spring的ApplicationContext

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市耙册,隨后出現(xiàn)的幾起案子给僵,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件帝际,死亡現(xiàn)場(chǎng)離奇詭異蔓同,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蹲诀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門斑粱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人脯爪,你說(shuō)我怎么就攤上這事则北。” “怎么了痕慢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵尚揣,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我掖举,道長(zhǎng)快骗,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任塔次,我火速辦了婚禮方篮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘励负。我一直安慰自己藕溅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布继榆。 她就那樣靜靜地躺著巾表,像睡著了一般。 火紅的嫁衣襯著肌膚如雪裕照。 梳的紋絲不亂的頭發(fā)上攒发,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音晋南,去河邊找鬼惠猿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛负间,可吹牛的內(nèi)容都是我干的偶妖。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼政溃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼趾访!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起董虱,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤扼鞋,失蹤者是張志新(化名)和其女友劉穎申鱼,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體云头,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捐友,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了溃槐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匣砖。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖昏滴,靈堂內(nèi)的尸體忽然破棺而出猴鲫,到底是詐尸還是另有隱情,我是刑警寧澤谣殊,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布拂共,位于F島的核電站,受9級(jí)特大地震影響蟹倾,放射性物質(zhì)發(fā)生泄漏匣缘。R本人自食惡果不足惜猖闪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一鲜棠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧培慌,春花似錦豁陆、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至馅而,卻和暖如春祥诽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瓮恭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工雄坪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人屯蹦。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓维哈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親登澜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阔挠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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