Spring Cloud -- 動態創建FeignClient實例


一、Feign使用中存在的問題

我們在介紹Spring Cloud —— OpenFeign 核心原理2.2節時候,舉了一個生產者消費者的案例,消費者服務在去調用生產者服務提供的接口時,我們需要定義定義 FeignClient 消費服務接口:

@FeignClient(name= "nacos-produce",contextId="DemoFeignClient") public interface DemoFeignClient { @GetMapping("/hello") String sayHello(@RequestParam("name") String name); }

說到這里,我們就會想到一個問題,如果我們有若干個消費者服務,那我們豈不是需要在每個消費者服務中都定義一次FeignClient 接口。因此為了解決這個問題,生產者服務通常需要提供一套API包,供各個消費者服務相互調用。

消費者服務想調用生產者提供的接口時只需要引入我們提供的API包,並做一些簡單配置:

  • 引入Feign依賴包(spring-cloud-starter-openfeign);
  • 指定Feign配置類(指定Encoder、Decoder、FeignLoggerFactory以及加入一些請求頭信息等)
  • 開啟@EnableFeignCliens注解

然后向Spring容器中,注入FeignClient實例:

 @Autowired private DemoFeignClient demoFeignClient;

由於API包中提供的feign接口,是依賴於Spring Boot的,因此要求外部系統必須是Spring Boot應用,同時由於feign接口配置中沒有指定用戶權限服務的url地址,而采用指定服務名的方式,導致外部系統如果想調用feign接口,必須要和用戶權限服務注冊在同一個注冊中心上。

二、動態創建FeignClient實例(SDK)

在上一節中我們曾經介紹過FeignClient的創建流程,其底層是通過FeignClientFactoryBean的getTarget創建了一個FeignClient接口的實例。既然利用FeignClientFactoryBean可以創建這么一個FeignClient接口的實例,那我們是否可以自己去手動創建這樣一個實例呢,當然是可以的,spring-cloud-openfeign-core為我們提供了一個這樣的類FeignClientBuilder,采用FeignClientBuilder可以動態創建FeignClient實例。同時為了擺脫對Spring Boot的依賴,我們可以自己創建一個Spring ApplicationContext,用來模擬@FeignClient實例的注入過程。

2.1、DemoResourceApi類

我們首先創建一個DemoResourceApi類,用來封裝DemoFeignClient的實例:

package com.jnu.example.feign.sdk.swagger; import com.jnu.example.feign.sdk.service.DemoFeignClient; import com.jnu.example.feign.sdk.swagger.client.ApiClient; import com.jnu.example.feign.sdk.util.FeignClientUtils; /** * @Author: zy * @Date:2021/4/15 * @Description:demo API */
public class DemoResourceApi { /** * demo client feign service */
    private DemoFeignClient demoFeignClient; /** * api client */
    private ApiClient apiClient; /** * constructor * @param apiClient: api client */
    public DemoResourceApi(ApiClient apiClient){ if(apiClient == null){ throw new NullPointerException("Api client NullPointerException"); } this.apiClient = apiClient; this.demoFeignClient = FeignClientUtils.build(apiClient,"DemoFeignClient", DemoFeignClient.class); } /** * @Author: zy * @Date:2021/4/15 14:51 * @Description:say hello * @param name * @return: User */
    public String sayHello(String name) { String res = demoFeignClient.sayHello(name); return res; } }

DemoFeignClient實例我們是通過FeignClientUtils工具類構建的。

2.2、FeignClientUtils

package com.jnu.example.feign.sdk.util; import com.jnu.example.feign.sdk.ApplicationContextBuilder; import com.jnu.example.feign.sdk.feign.FeignContextBuilder; import com.jnu.example.feign.sdk.swagger.client.ApiClient; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FeignClientBuilder; import org.springframework.cloud.openfeign.FeignContext; import org.springframework.context.ApplicationContext; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * @Author: zy * @Date:2021/4/15 * @Description:動態創建FeignClient實例 https://www.jianshu.com/p/172e002e0eb4 * 關於動態創建Feign Client的問題 https://blog.csdn.net/qq_37312208/article/details/112476051
 */ @Slf4j public final class FeignClientUtils { /** * forbid instantiate */
    private FeignClientUtils(){ throw new AssertionError(); } /** * Spring ApplicationContext */
    private  static ApplicationContext applicationContext; /** * save contextId -> feign client mapping */
    private static final Map<String, Object> FEIGN_CLIENT_CACHE = new ConcurrentHashMap<>(); /** * build feign client */
    public static <T> T build(ApiClient apiClient, String contextId, Class<T> targetClass){ return buildClient(apiClient,contextId,targetClass); } /** * build feign client without using the {@link FeignClient} annotation * @param apiClient: api client * @param contextId: Unique Spring ApplicationContext identification for feign client * corresponding to the contextId in @FeignClient. example "NamedContextFactory" * @param targetClass: target class. example: NamedContextFactory.class, */
    private static <T> T buildClient(ApiClient apiClient,String contextId, Class<T> targetClass) { //get
        T t = (T)FEIGN_CLIENT_CACHE.get(contextId); if(Objects.isNull(t)){ synchronized(FeignClientUtils.class) { t = (T)FEIGN_CLIENT_CACHE.get(contextId); if(Objects.isNull(t)) { //null check
                    if (applicationContext == null) { //create Spring ApplicationContext
                        ApplicationContextBuilder builder = new ApplicationContextBuilder(apiClient.getUseRegistry() ,apiClient.getServerAddr(),apiClient.getNamespace()); applicationContext = builder.getApplicationContext(); } //get feign context
                    FeignContext feignContext = applicationContext.getBean(FeignContext.class); //create Spring ApplicationContext for feign client
                    new FeignContextBuilder(feignContext, apiClient, contextId); //A builder for creating Feign client without using the {@link FeignClient} annotation
                    FeignClientBuilder.Builder<T> builder = new FeignClientBuilder(applicationContext).forType(targetClass, apiClient.getServerName()) .contextId(contextId) .url(apiClient.getUrl()); t = builder.build(); FEIGN_CLIENT_CACHE.put(contextId, t); } } } return t; } }

這里我們通過ApplicationContextBuilder創建了一個Spring ApplicationContext,並且向該容器中手動注入了Feign、nacos自動裝配時注入的bean。同時注入了FeignContext。因此我們才可以從容器中獲取到FeignContext實例。然后通過FeignContextBuilder為每個FeignClient接口創建了一個子Spring ApplicationContext。並保存到FeignContext中。

 private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap();

最后我們調用FeignClientBuilder創建對應的FeignClient實例(實際上就是通過contextId拿到FeignContext中保存的子容器、然后去構建Feign.Builder的過程),具體創建過程參考上一篇博客。

2.3、FeignClientBuilder

package com.jnu.example.feign.sdk.feign;

import cn.hutool.core.util.ReflectUtil;
import com.jnu.example.feign.sdk.feign.factory.TokenFactory;
import com.jnu.example.feign.sdk.swagger.client.ApiClient;
import feign.Logger;
import feign.Request;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.FeignContext;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: zy
 * @Date: 2021/4/18
 * @Description:A builder for creating Application context for feign client.
 */
@Slf4j
@Getter
public class FeignContextBuilder {

    /**
     * A factory that creates instances of feign classes. It creates a Spring
     * ApplicationContext per feign client, and extracts the beans that it needs from there.
     */
    private FeignContext feignContext;

    /**
     * Unique Spring ApplicationContext identification for feign client
     */
    private String contextId;

    /*
     * Spring ApplicationContext for feign client
     */
    private ApplicationContext feignClientContext;

    /**
     * constructor
     * @param feignContext:
     * @param apiClient
     * @param contextId
     */
    public FeignContextBuilder(FeignContext feignContext, ApiClient apiClient, String contextId){
        //save parameter
        this.feignContext = feignContext;
        this.contextId = contextId;

        //create Spring ApplicationContext for feign client .
        this.feignClientContext = createContext(feignContext,apiClient,contextId);
    }

    /**
     * create Spring ApplicationContext for feign client
     * FeignContext cannot find the Spring ApplicationContext we created based on the contextId, it will create  Spring ApplicationContext for feign client
     * @param apiClient: api client
     * @param contextId : Unique Spring ApplicationContext identification for feign client
     */
    private ApplicationContext createContext(FeignContext feignContext, ApiClient apiClient, String contextId){
        //create Spring ApplicationContext for feign client
        Method getContext =  ReflectUtil.getMethod(feignContext.getClass(),"getContext",String.class);
        getContext.setAccessible(true);

        AnnotationConfigApplicationContext context= null;
        try {
            //Inject Feign request interceptor bean into the Spring ApplicationContext
            context = (AnnotationConfigApplicationContext)getContext.invoke(feignContext,contextId);

            //Inject Feign request interceptor bean into the Spring ApplicationContext
            registerRequestInterceptor(context,apiClient.getLoginName(),apiClient.getPassword(),apiClient.getTokenFactory());

            //Inject Feign request options bean into the Spring ApplicationContext
            registerRequestOptions(context,apiClient.getConnectTimeoutMillis(),apiClient.getReadTimeoutMillis());

            //Inject Feign logger level bean into the Spring ApplicationContext
            registerLoggerLevel(context,apiClient.getLevel());

            //Inject Feign encoder bean into the Spring ApplicationContext
            registerEncoder(context);

            //Inject Feign decoder bean into the Spring ApplicationContext
            registerDecoder(context);

            //Inject Feign error decoder bean into the Spring ApplicationContext
            registerErrorDecoder(context);

            log.info("jnu-feign-server-sdk:" + contextId + " feign client context refresh success");
        } catch (Exception e) {
            log.error(contextId +": create sub context fail");
        }

        return context;
    }


    /**
     * Inject Feign request interceptor bean into the Spring ApplicationContext
     * @param context : Spring ApplicationContext for feign client
     * @param loginName:loginName
     * @param password: password
     */
    private void registerRequestInterceptor(AnnotationConfigApplicationContext context, String loginName, String password
            , TokenFactory tokenFactory){
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignRequestInterceptor.class);
        builder.addPropertyValue("loginName",loginName);
        builder.addPropertyValue("password",password);
        builder.addPropertyValue("tokenFactory",tokenFactory);
        context.registerBeanDefinition("feignRequestInterceptor",builder.getBeanDefinition());
    }


    /**
     * Inject Feign request options bean into the Spring ApplicationContext
     * @param context : Spring ApplicationContext for feign client
     * @param connectTimeoutMillis : connect timeout
     * @param readTimeoutMillis: read timeout
     */
    private void registerRequestOptions(AnnotationConfigApplicationContext context,int connectTimeoutMillis,int readTimeoutMillis){
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(Request.Options.class);
        builder.addConstructorArgValue(connectTimeoutMillis);
        builder.addConstructorArgValue(readTimeoutMillis);
        builder.addConstructorArgValue(true);
        context.registerBeanDefinition("feignRequestOptions",builder.getBeanDefinition());
    }


    /**
     * Inject Feign encoder bean into the Spring ApplicationContext
     * @param context : Spring ApplicationContext for feign client
     */
    private void registerEncoder(AnnotationConfigApplicationContext context){
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(SpringEncoder.class);
        builder.addConstructorArgValue(feignHttpMessageConverter());
        context.registerBeanDefinition("feignEncoder",builder.getBeanDefinition());
    }

    /**
     * Inject Feign decoder bean into the Spring ApplicationContext
     * @param context : Spring ApplicationContext for feign client
     */
    private void registerDecoder(AnnotationConfigApplicationContext context){
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(SpringDecoder.class);
        builder.addConstructorArgValue(feignHttpMessageConverter());
        context.registerBeanDefinition("feignDecoder",builder.getBeanDefinition());
    }

    /**
     * HttpMessageConverters factory
     */
    private ObjectFactory<HttpMessageConverters> feignHttpMessageConverter() {
        final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(new GateWayMappingJackson2HttpMessageConverter());
        return () -> httpMessageConverters;
    }

    /**
     * HttpMessageConverter
     */
    private static class GateWayMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
        GateWayMappingJackson2HttpMessageConverter(){
            List<MediaType> mediaTypes = new ArrayList<>();
            mediaTypes.add(MediaType.valueOf(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8"));
            setSupportedMediaTypes(mediaTypes);
        }
    }

    /**
     * Inject Feign logger level bean into the Spring ApplicationContext
     */
    private void registerLoggerLevel(AnnotationConfigApplicationContext context,Logger.Level level){
        //https://blog.csdn.net/u014252478/article/details/84869997
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Logger.Level.class, () -> level);
        context.registerBeanDefinition("feignLoggerLevel",beanDefinitionBuilder.getBeanDefinition());
    }


    /**
     * Inject Feign error decoder bean into the Spring ApplicationContext
     */
    private void registerErrorDecoder(AnnotationConfigApplicationContext context){
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(FeignErrorDecoder.class, () -> new FeignErrorDecoder());
        context.registerBeanDefinition("feignErrorDecoder",beanDefinitionBuilder.getBeanDefinition());
    }
}

三、代碼下載

以上只展示了部分代碼,更多代碼下載:jnu-feign-server


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM