基於Feign的局部請求攔截

由於項目的要求,不能對所有基於Feign的進行攔截,需要對不同的Feign請求進行不同的攔截,經過資料的收集整理以及SpringCloud中對於Feign的集成的源碼的閱讀,解決了針對Feign請求的局部攔截

本項目中SpringCloud的版本是Camden.SR6版本

背景說明

在既有的項目上進行二次開發,服務A需要請求服務B同時需要將服務A中請求的消息頭相關信息傳送給服務B,但是由於既有項目中的相關設計,不支持feign請求的全局攔截,只能針對服務A請求服務B的feign請求進行攔截,所以開發瞭如下的方法;

這裏說明下,之所以採用Feign是由於Feign添加支持負載均衡,這點尤爲重要。

思路說明

既然當前SpringCloud的版本不支持Feign請求的攔截,那麼只能自己開發攔截的方法來攔截Feign請求了,整理資料有如下兩種思路:

  1. Feign內部也是使用Ribbon來完成支持負載均衡的,所以拋開Feign,直接使用Ribbon也是可以的;

爲了擴展方便,可以採用掃描自定義註解和AOP攔截的方式,然後通過前置方法將消息頭相關內容存儲到請求中

這個思路簡單易用,而且方法都是自己開發的,出現問題,定位和修改都是很容易的,但是這種方法也相當於重新開發了一種新的功能,工作量和代碼量肯定是不小的,

  1. 還是使用Feign,既然SpringCloud當前版本不支持,那麼就利用原生的Feign來自己封裝;

SpringCloud的@FeignClient也是基於原生的Feign的基礎上進行封裝的,所以我們也可以開發新的封裝,使之支持目前的需求,對Feign的請求進行局部攔截

如果想進行新的封裝,我們可以借鑑SPringCloud對Feign的封裝方法,這裏我們可以參考 FeignClient源碼深度解析這篇文章,說的很詳細,在這裏感謝大佬的分享。

代碼實現

廢話不多說,爲了讓代碼改動量小,並且利用Feign的特性:(一個接口就可以訪問其他的項目),我們選擇第二種方法來實現

在這裏對於SpringCloud支持Feign的封裝思路就顯得比較重要了,不過在這之前,我們可以使用原生的Feign來支持請求的攔截

首先是依賴的支持

    <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. 首先我們定義一個接口,該接口配置Feign訪問其他項目的路徑
    /**
     * @author: amos
     * @Description: 訪問其他業務的請求
     * @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);
    }

注意:該接口上沒有添加 @FeignClient 註解,因爲目前項目是支持SpringCloud的Feign使用方式的,如果添加了註解,就會直接走SpringCloud的Feign請求方式
2. 原生的Feign使用方式

    /**
     * 
     * @author: amos
     * @Description: 基於原生的Feign請求來獲取請求訪問對象     
     * @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來獲取接口訪問對象
         *
         * @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註冊到SpringContext的上下文中
         *
         * @return
         */
        @Bean("bizClient")
        public BizClient bizClient() {
            return this.feignBuilderRequestInterceptor(BizClient.class);
        }
    }

至此使用原生的Feign結束,但是一測試就報錯

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

從網上查詢資料,並進行了相關的依賴和配置項 都沒有生效

  1. 閱讀源碼,瞭解SpringCloud支持Feign的原理

上面的辦法既然不可行,主要的問題是Ribbon識別不了我們的實例名,也就是代碼中Client有問題,但是SpringCloud的Feign卻是可以支持的,所以這裏的關鍵就是SpringCloud中的Feign是怎麼支持的Ribbon的,然後將他支持的方式移到我們目前代碼,解決由於Ribbon造成的負載均衡的問題就可以了。

爲此,我們需要閱讀SpringCloud支持Feign方面的相關的源碼,源碼的閱讀可以參考上面的博客鏈接,說明的非常詳細,下面我們主要分析下源碼中的代理工廠的代碼;

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

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

這裏我們主要看下構造器中是怎麼獲取Client對象的,我們需要知道他是怎麼處理支持負載均衡的,我們追蹤到對應的代碼:

    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對象 
		// 通過上面的代碼我們知道 FeignContext 中獲取對應的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對象主要來源於FeignContext中,而FeignContext是來源於ApplicationContext中,到這裏就非常請求了,我們需要從ApplicationContext中獲取FeignContext,然後再從FeignContext中獲取Client對象。

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

    /**
     * 
     * @author: amos
     * @Description: 基於原生的Feign請求來獲取請求訪問對象     
     * @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來獲取接口訪問對象
         *
         * @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註冊到SpringContext的上下文中
         *
         * @return
         */
        @Bean("bizClient")
        public BizClient bizClient() {
            return this.feignBuilderRequestInterceptor(BizClient.class);
        }
    }

上面的代碼主要是 類實現ApplicationContextAware接口來獲取 ApplicationContext對象,然後從ApplicationContext對象中獲取FeignContext對象,再獲取到我們需要的Client對象即可;

至此已經完全結束了,我們可以在業務代碼中直接注入 BizClient 直接調用對應得方法了。

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

上面的代碼還可以再進行封裝,如果有多個BizClient的業務請求,可以通過自定義註解來實現系統在啓動的時候,掃描自定義的註解,然後同樣利用代理工廠的方法生成實例對象,然後注入到Spring的ApplicationContext中,方便業務直接拿來使用。

邏輯和Spring支持Feign的邏輯是一樣的,主要依賴ImportBeanDefinitionRegistrarResourceLoaderAwareBeanClassLoaderAware三個類。

我會在下一篇博文中基於該方法來說明,如何實現系統啓動將自定義註解的bean注入到Spring的ApplicationContext

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章