zoukankan      html  css  js  c++  java
  • 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为我们提供了一个这样的类FeignClientBuild,采用FeignClientBuild可以动态创建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());
        }
    }

    三、代码下载

    以上只展示了部分代码,更多代码下载:https://github.com/Zhengyang550/jnu-feign-server

  • 相关阅读:
    SpringMvc的服务器端跳转和客户端跳转
    springMvc的一些简介 和基于xml的handlerMapping基本流程
    springMvc 的参数验证 BindingResult result 的使用
    SpringMVC 学习笔记(二) @RequestMapping、@PathVariable等注解
    springmvc处理ajax请求
    取maven copy部分
    maven scope含义的说明
    Maven依赖中的scope详解
    EasyMock 使用方法与原理剖析
    Maven:Generating Project in Batch mode 卡住问题
  • 原文地址:https://www.cnblogs.com/zyly/p/14771134.html
Copyright © 2011-2022 走看看