美文网首页实战收藏-解决篇
如何创建一个动态的feignClient

如何创建一个动态的feignClient

作者: 牧羊人刘俏 | 来源:发表于2019-11-19 20:19 被阅读0次

FeignClient的使用大家可能非常的熟悉

@FeignClient(name = "xxx")
public interface ItemFeignClient {
    @RequestMapping(value = "/api/items/{id}", method = RequestMethod.GET)
    ItemDto getItem(@PathVariable("id") String itemId);

}

如上,我们定义了一个FeignClient的模板,在服务里面可以直接的调用并访问xxx服务里面的/api/items/{id}方法了。

但是如果说我们现在有十个服务,每个服务都提供了/api/items/{id}的api,那是不是我们要定义十个FeignClient呢,如下

@FeignClient(name = "xxx1")
public interface ItemFeignClient1 {
    @RequestMapping(value = "/api/items/{id}", method = RequestMethod.GET)
    ItemDto getItem(@PathVariable("id") String itemId);
}
@FeignClient(name = "xxx2")
public interface ItemFeignClient2 {
    @RequestMapping(value = "/api/items/{id}", method = RequestMethod.GET)
    ItemDto getItem(@PathVariable("id") String itemId);

}
......
@FeignClient(name = "xxx10")
public interface ItemFeignClient10 {
    @RequestMapping(value = "/api/items/{id}", method = RequestMethod.GET)
    ItemDto getItem(@PathVariable("id") String itemId);

}

肯定有点弱,既然FeignClient是一个调用的模板而已,那我们是不是也可以定义一个FeignClient的模板的模板来解决上面的这种情况呢,当然是可以的,我们先给出解决方案,然后再分析里面的原理。代码如下


 public class OutsideAccessorUtils{

    private static final Cache<Pair<String, Class<?>>, Object> cache = CacheBuilder.newBuilder().weakValues().build();

    @SuppressWarnings("unchecked")

    public static <T> T buildAccessor(String serverName, Class<T> accessInterface) {

        val pair = Pair.<String, Class<?>>of(serverName, accessInterface);

        try{

            return (T) cache.get(pair, () ->construct(serverName, accessInterface));

        } catch(ExecutionException e) {

            throw new SystemException(e.getMessage(), e);

        }

}

    @SuppressWarnings("unchecked")

    private static <T> T construct(String name, Class<T> accessInterface) throws Exception{

        val clazz = Class.forName("org.springframework.cloud.openfeign.FeignClientFactoryBean");

        val instance = ClassUtils.newInstance(clazz);

        setField(clazz, "type", instance, accessInterface);

        setField(clazz, "name", instance, name);

        //其要求path值非null

        setField(clazz, "path", instance, "");

        //设置上下文

        ApplicationContext applicationContext = ApplicationContextUtils.getReadyApplicationContext();

        ((ApplicationContextAware) instance).setApplicationContext(applicationContext);

        val obj =((FactoryBean) instance).getObject();

        return (T) obj;

    }

    private static void setField(Class clazz, String fieldName, Object obj, Object value) {

        val field = ReflectionUtils.findField(clazz, fieldName);

        assert field !=null;

        ReflectionUtils.makeAccessible(field);

        ReflectionUtils.setField(field, obj, value);

    }

//    public static RestTemplate wfRestTemplate() {

//        return getRestTemplate();

//    }

}

public interface ItemFeignClient {
    @RequestMapping(value = "/api/items/{id}", method = RequestMethod.GET)
    ItemDto getItem(@PathVariable("id") String itemId);

}
//调用方式如下
//调用xxx1服务的/api/items/{id}方法
 ItemFeignClient client = OutsideAccessorUtils.buildAccessor("xxx1",ItemFeignClient.class);
 client.getItem(122);
//调用xxx2服务的/api/items/{id}方法
 client = OutsideAccessorUtils.buildAccessor("xxx2",ItemFeignClient.class);
 client.getItem(122);

上面是实现动态FeignClient的核心的代码,其中buildAccessor方法会返回一个T 这个T就是我们定义的FeignClient模板的模板ItemFeignClient,缓存的加持,性能上面也有了提升。后面我们在分析下实现的原理。

如何更加优雅的实现动态feignClient的能力
借助于aop和配置中心的能力,我们可以实现的更加灵活
可以定义如下的注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DyFeignClient {
  //属性可以扩展添加
}

使用方式如下

@FeignClient(name="xx1")
public interface CommonFeignClientProxy {


   @DyFeignClient
   @RequestMapping(value = "/api/{boType}/search", method = GET)
   ResponseEntity<?> boSearch(@PathVariable("boType") String boType,
                                      @RequestHeader(RequestHeaderInfo.X_TENANT_ID) String tenant_id);

}

如上代码的意义是,在执行boSearch的时候,aop会拦截DyFeignClient注解,如果找不到相应的路由信息,就默认到xx1服务里面去执行。
相关的aop代码如下

@Aspect
@Component
@Slf4j
public class DyFeignClientAspect {

    @Autowired
    private RoutingMapper routingMapper;

    private AntPathMatcher antMatcher = new AntPathMatcher();
   
    @Pointcut("@annotation(DyFeignClient)")
    public void annotation1(DyFeignClient dyFeignClient) {

    }

    @Pointcut("@annotation(requestMapping)")
    public void annotation2(RequestMapping requestMapping) {

    }

    @Around("annotation1(dyFeignClient) && annotation2(requestMapping)")
    public Object aroundFeignClientRequest(ProceedingJoinPoint point, DyFeignClient dyFeignClient, RequestMapping requestMapping) throws Throwable {
        Object result;
        String uri = requestMapping.value()[0];
        String serviceName = findRouteService(routingMapper.getUriRouteMap(), requestMapping.value()[0]);
        MethodSignature methodSignature = (MethodSignature)point.getSignature();
        //serviceName not found. Try to resolve path variable and look for service again
        if (Strings.isEmpty(serviceName)) {
         
           Annotation[][] annotations = methodSignature.getMethod().getParameterAnnotations();
            for (int i=0; i<annotations.length; i++) {
                Annotation[] temp = annotations[i];
                for (int j=0; j<temp.length; j++) {
                    Annotation annotation = temp[j];
                    if(PathVariable.class.isAssignableFrom(annotation.getClass())) {
                        PathVariable pathVariable = (PathVariable)annotation;
                        uri = uri.replaceAll(new StringBuilder().append("\\{").append(pathVariable.value()).append("\\}").toString(), point.getArgs()[i].toString());
                    }
                }
            }
         
            serviceName = findRouteService(routingMapper.getUriRouteMap(), uri);
        }

        try {
            if (!Strings.isEmpty(serviceName)) {
                LOG.info("the service name is:{}", serviceName);
                Object feignClient = OutsideAccessorUtils.buildAccessor(serviceName, methodSignature.getMethod().getDeclaringClass());
                Method method = feignClient.getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getParameterTypes());
                method.setAccessible(true);
                result = method.invoke(feignClient, point.getArgs());
            } else {//service name is still not found
                LOG.info("service name is still not found, call original feign client");
                if (point.getArgs() == null || point.getArgs().length == 0) {
                    result = point.proceed();
                } else {
                    result = point.proceed(point.getArgs());
                }
            }
        } catch (Exception e) {
            Throwable throwable = parseExceptionRootCause(e, FeignException.class);
            if (throwable != null) {
                throw throwable;
            } else {
                throw e;
            }
        }

               
        return result;
    }

    private String findRouteService(Map<String, String> urlRouteMap, String uri) {
        try {
            String matchedUri = urlRouteMap.keySet().stream().filter(key->antMatcher.match(key, uri)).findFirst().orElse(null);
            if (matchedUri != null) {
                return urlRouteMap.get(matchedUri);
            }
        } catch (Throwable e) {
            LOG.warn("findRouteService failed, the uri:{} cannot find its service", uri);
        }
        LOG.info("uri:{} cannot find its service", uri);
        return null;
    }

    private Throwable parseExceptionRootCause(Exception ex, Class<? extends Throwable> expectedRootCauseClass) {
        int maxDepth = 10;
        if (ex.getClass() == expectedRootCauseClass) {
            return ex;
        }
        Throwable rootCause = ex.getCause();
        for (int i = 0; i < 10; i++) {
            if (rootCause == null) {
                return null;
            }
            if (rootCause.getClass() == expectedRootCauseClass) {
                return rootCause;
            }
            if(rootCause.getCause() == null) {
                return null;
            }
            rootCause = rootCause.getCause();
        }
        return null;
    }
}

如上,相当于所有的路由信息,都在RoutingMapper里面,等于说在服务启动的时候,RoutingMapper必须加载到所有的路由信息,现在配置中心可以派上用场了,如果在配置中心里面已经有了所有的路由配置信息,那一切就ok了。
等于说现在的压力都到了RoutingMapper这个bean的身上,大致代码如下

public class RoutingMapper implements ApplicationListener<ApplicationReadyEvent> {
     @Getter
    private static Map<String, String> uriRouteMap = new HashMap();

    @Autowired
    private ApiRoutingRulesConfigLoader configLoader;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        LOG.info("start to load the api routing rules with ApplicationReadyEvent");
        synchronized(uriRouteMap) {
            uriRouteMap.clear();
            loadFromConfig(null, null) 
           
        }
    }

    public void refreshApiRouteMap(ConfigProperty lastConfigProperty, ConfigProperty newConfigProperty) {
        LOG.info("refresh the api routing rules with ConfigChangeCallback");
        synchronized(uriRouteMap) {
         loadFromConfig(lastConfigProperty, newConfigProperty);
        }
    }

    private void loadFromConfig(ConfigProperty lastConfigItem, ConfigProperty currentConfigItem) {
        if (lastConfigItem != null && currentConfigItem != null) {
            List[] lastAndCurrentRules = configLoader.getApiRoutingRules(lastConfigItem, currentConfigItem);
            List<ApiRoutingRulesConfigLoader.ApiRoutingRule> lastRules = lastAndCurrentRules[0];
            List<ApiRoutingRulesConfigLoader.ApiRoutingRule> currentRules = lastAndCurrentRules[1];
            removeUriRouteMap(lastRules);
            addUriRouteMap(currentRules);
        } else {
            addUriRouteMap(configLoader.getApiRoutingRules());
        }
    }

    private void addUriRouteMap(List<configLoader.ApiRoutingRule> rules) {
        if (rules != null) {
            rules.stream().forEach(rule->{
                if (uriRouteMap.get(rule.getUri()) != null) {
                  rule.getUri(), uriRouteMap.get(rule.getUri()), rule.getServiceId());
                }
                uriRouteMap.put(rule.getUri(), rule.getServiceId());
            });
        }
    }

    private void removeUriRouteMap(List<ApiRoutingRulesConfigLoader.ApiRoutingRule> rules) {
        if (rules != null) {
            rules.stream().forEach(rule->{
                if (StringUtils.equals(uriRouteMap.get(rule.getUri()), rule.getServiceId())) {
                    uriRouteMap.remove(rule.getUri());
                }
            });
        }
    }
}

还一个场景是refresh场景,每个注册中心的sdk不一样,就不贴代码了


图片示例.png

整个的原理非常的清晰,以上!

ps 很多人要ApplicationContextUtils的源码,我就贴这里了

@Component
@Slf4j
public class ApplicationContextUtils implements ApplicationContextAware, BeanFactoryPostProcessor, ApplicationListener<ApplicationReadyEvent> {
    private static final Lock LOCKER = new ReentrantLock();
    private static final Condition CONDITION = LOCKER.newCondition();

    @Getter
    private static volatile boolean contextReady;

    private static ApplicationContext applicationContext;
    private static final Map<Class<?>, String> NAME_MAP = Maps.newHashMap();

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) {
        //do nothing
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        ApplicationContextUtils.applicationContext = applicationContext;
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        LOG.info("init spring context success!");

        LOCKER.lock();
        try{
            contextReady = true;
            CONDITION.signalAll();
        } finally {
            LOCKER.unlock();
        }
    }

    public static ApplicationContext getApplicationContext() {
        if(!contextReady) {
            LOG.warn("spring context not init yet check stack", new Exception());
        }

        return applicationContext;
    }

    public static ApplicationContext getReadyApplicationContext() {
        waitContextReady();

        return applicationContext;
    }

    /** 等待spring容器准备好,如果没有准备好,则阻塞当前线程 */
    public static void waitContextReady() {
        //快速判断,避免加锁
        if(contextReady) {
            return;
        }

        LOCKER.lock();
        try{
            while(!contextReady) {
                ExceptionUtils.doActionLogE(() -> CONDITION.await(3, TimeUnit.SECONDS));
                if(!contextReady) {
                    LOG.debug("waiting context init");
                }
            }
        } finally {
            LOCKER.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T getReadyBean(Class<T> clazz) {
        waitContextReady();

        return getReadyBean(clazz, () -> {
            throw new SystemException("class:" + clazz + "no definition in spring");
        });
    }

    @SuppressWarnings("unchecked")
    public static <T> T getReadyBean(Class<T> clazz, Supplier<T> ifAbsent) {
        waitContextReady();

        String beanName = NAME_MAP.get(clazz);
        if(beanName == null) {
            String[] beanNames = applicationContext.getBeanNamesForType(clazz);
            if(beanNames.length == 0) {
                return ifAbsent.get();
            }
            if(beanNames.length > 1) {
                throw new SystemException("class:" + clazz + "more than on bean definition" + Arrays.toString(beanNames));
            }
            beanName = beanNames[0];
            NAME_MAP.put(clazz, beanName);
        }

        return (T) applicationContext.getBean(beanName);
    }

    @SuppressWarnings("unchecked")
    public static <T> T getReadyBean(String beanName) {
        waitContextReady();

        return (T) applicationContext.getBean(beanName);
    }

    @SuppressWarnings("unchecked")
    public static <T> T getReadyBean(String beanName, Supplier<T> ifAbsent) {
        waitContextReady();

        if(!applicationContext.containsBean(beanName)) {
            return ifAbsent.get();
        }

        return (T) applicationContext.getBean(beanName);
    }

    public static <T> void injectReadyBean(T bean) {
        waitContextReady();
        val factory = applicationContext.getAutowireCapableBeanFactory();
        factory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);
        factory.initializeBean(bean, null);
    }

}

相关文章

网友评论

    本文标题:如何创建一个动态的feignClient

    本文链接:https://www.haomeiwen.com/subject/llkgictx.html