一、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。