【OpenFeign】@FeignClient 统一增加 url 或 @FeignClient 的属性增强处理

1  前言

我们服务间的调用基本都是通过 OpenFeign 方式使用的,最近发现小伙伴们本地调试代码的时候或者要调试测试环境的话,有的时候需要给 OpenFeign 中的 @FeignClient 添加 url 来实现服务的调用。这就带来比如 feign 比较多的话,要改好多个有的时候地址变了,又要换,也就是一种硬编码,那么能不能做个类来处理,比如把它的 url 给塞上去呢?这节就是我们要解决的。

2  OpenFeign 了解

首先先简单了解下 OpenFeign 我们平时的使用方法:

@MapperScan("com.virtuous.base.infrastructure.persistence.dao*")
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = {"com.virtuous"})
// 启动类上增加 @EnableFeignClients 来告知我们的 FeignClient 定义
@EnableFeignClients(basePackages={"com.virtuous.core.web.external.feignclient"})
public class VirtuousBaseServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(VirtuousBaseServiceApplication.class, args);
    }

}

还有我们的 FeignClient 定义:

@FeignClient(
    contextId = "serviceAccessLog"
    , value = "virtuous-base-service"
    , path = "/virtuous-base-service/serviceAccessLog"
)
public interface ServiceAccessLogFeign {

    /**
     * 分页查询
     * @param params
     * @return
     */
    @GetMapping(value = "/pageByMap", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    ResultDto<PagerDto<ServiceAccessLogFeignResDto>> pageByMap(@RequestParam Map<String, Object> params);
}

这些就是我们平时使用的方式,然后服务启动的时候就会扫描出 @FeignClient 的类,然后进行代理对象的创建。这个创建我们本节不会去细讲,后边我会单独出一篇文章来说,它主要是通过 FeignClientFactoryBean 也就是 FactoryBean 的方式创建的,和 MyBatis 里的 Mapper接口 创建方式一样哈。

我们本节的目标就是在 FeignClientFactoryBean 创建的时候,给他添加上 url 属性,实现和下方一致的效果。

3  实现步骤

3.1  失败的方式-BeanDefinitionRegistryPostProcessor 

首先我想到的是通过 BeanDefinitionRegistryPostProcessor 这个后置处理器来做的,因为我要对它的 BeanDefinition 进行处理,但是我调试发现这个不生效(可能跟FactoryBean方式有关系),因为到达这个后置的时候,我们的 feignClient 其实都创建出来了,已经生成完了,所以节点上晚了。

package xxx;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.apache.groovy.util.Maps;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 切记仅限本地调试 不要把这个东西发到线上
 * 如果项目本身有环境变量的话 可以根据环境变量直接过掉这个
 * @author kuku
 * 这个是第一次尝试的不太行
 * 因为 FactoryBean 的不会走这里
 */
@Slf4j
public class MyFeignClientBeanDefinitionOld implements BeanDefinitionRegistryPostProcessor {

    /**
     * 默认的 url
     */
    private static final String defaultUrl = "http://xxxx.com/";

    /**
     * 指定值
     * 比如指定哪个 contextId 的 url 为多少
     */
    private static final Map<String, String> specialMap = Maps.of(
            "contextId1", "url1"
            , "contextId2", "url2"
    );

    /**
     * 要筛选的属性
     */
    private static final Set<String> propertyNameSet = Sets.newHashSet("name", "contextId", "url");
    private static final PropertyValue PROPERTY_VALUE = new PropertyValue("", "");

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        log.info(">>>>>>>>>>>>> @FeignClient 进行 url 设置");
        decorateFeignClient(registry);
        log.info(">>>>>>>>>>>>> @FeignClient url 设置结束");
    }

    private void decorateFeignClient(BeanDefinitionRegistry registry) {
        try {
            // 通过反射获取到所有的 BeanDefinition
            DefaultListableBeanFactory beanFactory = ((DefaultListableBeanFactory) registry);
            Field field = ReflectionUtils.findField(beanFactory.getClass(), "beanDefinitionMap");
            field.setAccessible(true);
            // 获取到所有的 BeanDefinition
            Map<String, BeanDefinition> map = ((Map<String, BeanDefinition>) field.get(beanFactory));
            // 筛选出 FeignClient 的,原理就是 @FeignClient 都会通过 FeignClientFactoryBean 进行增强创建
            List<BeanDefinition> list = map.values().stream()
                    .filter(item -> "org.springframework.cloud.openfeign.FeignClientFactoryBean".equals(item.getBeanClassName()))
                    .collect(Collectors.toList());
            // 如果为空的话,就是没有 @FeignClient 结束
            if (CollectionUtils.isEmpty(list)) {
                log.info(">>>>>>>>>>>>> 未发现 @FeignClient");
                return;
            }
            for (BeanDefinition beanDefinition : list) {
                // 查找有url 的属性么 有的话 就略过 没有的话就设置
                MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
                List<PropertyValue> propertyValueList = propertyValues.getPropertyValueList();

                // 筛选出 name contextId url 三个属性
                Map<String, PropertyValue> propertyMap = propertyValueList.stream().filter(
                        item -> propertyNameSet.contains(item.getName())).collect(Collectors.toMap(PropertyValue::getName, i->i, (k1,k2) -> k2));

                PropertyValue nameProperty = propertyMap.getOrDefault("name", PROPERTY_VALUE);
                PropertyValue contextIdProperty = propertyMap.getOrDefault("contextId", PROPERTY_VALUE);
                if (Objects.isNull(contextIdProperty) || Objects.isNull(contextIdProperty.getValue())) {
                    log.info(">>>>>>>>>>>>> {} 的 contextId 为空,不进行 url 的设置", nameProperty.getValue());
                    continue;
                }

                String contextId = contextIdProperty.getValue().toString();
                PropertyValue urlProperty = propertyMap.getOrDefault("url", PROPERTY_VALUE);
                Object value = urlProperty.getValue();
                if (Objects.nonNull(value) && StringUtils.isNotBlank((String)value)) {
                    log.info(">>>>>>>>>>>>> {} 已经存在 url 属性 = {},故不进行 url 的设置", contextId, value);
                    continue;
                }

                // 没有 url 的进行设置
                String special = specialMap.getOrDefault(contextId, "");
                String urlVal = StringUtils.isNotBlank(special) ? special : defaultUrl;

                PropertyValue propertyValue = new PropertyValue("url", urlVal);
                // 删掉再新加一个
                propertyValueList.remove(urlProperty);
                propertyValueList.add(propertyValue);
                log.info(">>>>>>>>>>>>> {} 塞 url = {}", contextId, urlVal);
            }
        } catch (IllegalAccessException e) {
            log.error(">>>>>>>>>>>>>  @FeignClient 进行 url 设置报错", e);
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }

}

3.2  成功的方式

然后我打了几个断点,看了下执行顺序,发现这样的一个关系:

看此时的 BeanPostProcessor 有如下几个:

然后我写一个 InstantiationAwareBeanPostProcessor :

package com.xxx.adapter.api;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.groovy.util.Maps;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

import java.util.*;

/**
 * 切记仅限本地调试 不要把这个东西发到线上
 * 如果项目本身有环境变量的话 可以根据环境变量直接过掉这个
 * @author kuku
 */
@Slf4j
@Component
public class MyFeignClientInstantiation implements InstantiationAwareBeanPostProcessor{ /** * 默认的 url */ private static final String defaultUrl = "http://xxx.cn-shanghai.alicontainer.com/"; /** * 指定值 * 比如指定哪个 contextId 的 url 为多少 */ private static final Map<String, String> specialMap = Maps.of( "contextId1", "url1" , "contextId2", "url2" ); /** * 根据名称过滤 */ private static final String FILTER_FLAG = "FeignClientFactoryBean"; @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException { // 判断是不是 FeignClient 的,原理就是 @FeignClient 都会通过 FeignClientFactoryBean 进行增强创建 if (!(FILTER_FLAG.equals(bean.getClass().getSimpleName()))) return pvs; // 是的话 对他的 url 属性进行重放 decorateFeignClient(beanName, pvs); return pvs; } private void decorateFeignClient(String beanName, PropertyValues propertyValues) { try { PropertyValue nameProperty = propertyValues.getPropertyValue("name"); if (Objects.isNull(nameProperty)) { log.info(">>>>>>>>>>>>> {} 的 name 为空,不进行 url 的设置", beanName); return; } PropertyValue contextIdProperty = propertyValues.getPropertyValue("contextId"); if (Objects.isNull(contextIdProperty) || Objects.isNull(contextIdProperty.getValue())) { log.info(">>>>>>>>>>>>> {} 的 contextId 为空,不进行 url 的设置", nameProperty.getValue()); return; } String contextId = contextIdProperty.getValue().toString(); PropertyValue urlProperty = propertyValues.getPropertyValue("url"); if (Objects.nonNull(urlProperty) && Objects.nonNull(urlProperty.getValue()) && StringUtils.isNotBlank((String)urlProperty.getValue())) { log.info(">>>>>>>>>>>>> {} 已经存在 url 属性 = {},故不进行 url 的设置", contextId, urlProperty.getValue()); return; } // 没有 url 的进行设置 String special = specialMap.getOrDefault(contextId, ""); String urlVal = StringUtils.isNotBlank(special) ? special : defaultUrl; if (Objects.nonNull(urlProperty)) { urlProperty.setConvertedValue(urlVal); log.info(">>>>>>>>>>>>> {} 塞 url = {}", contextId, urlVal); } } catch (Exception e) { log.error(">>>>>>>>>>>>> @FeignClient 进行 url 设置报错", e); } } }

调试发现怎么没我的 InstantiationAwareBeanPostProcessor :

然后我通过 SpringBoot 提供的监听器 SpringApplicationRunListener,把我的 InstantiationAwareBeanPostProcessor 给添加进来:

最后实现的效果:

我贴一下完整的代码:

package xxxx;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.groovy.util.Maps;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * 切记仅限本地调试 不要把这个东西发到线上
 * 如果项目本身有环境变量的话 可以根据环境变量直接过掉这个
 * @author kuku
 */
@Slf4j
public class MyFeignClientInstantiation implements InstantiationAwareBeanPostProcessor, SpringApplicationRunListener {

    /**
     * 默认的 url
     */
    private static final String defaultUrl = "http://xxxxx.com/";

    /**
     * 指定值
     * 比如指定哪个 contextId 的 url 为多少
     */
    private static final Map<String, String> specialMap = Maps.of(
            "contextId1", "url1"
            , "contextId2", "url2"
    );

    /**
     * 根据名称过滤
     */
    private static final String FILTER_FLAG = "FeignClientFactoryBean";

    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        // 判断是不是 FeignClient 的,原理就是 @FeignClient 都会通过 FeignClientFactoryBean 进行增强创建
        if (!(FILTER_FLAG.equals(bean.getClass().getSimpleName()))) return pvs;

        // 是的话 对他的 url 属性进行重放
        decorateFeignClient(beanName, pvs);

        return pvs;
    }

    private void decorateFeignClient(String beanName, PropertyValues propertyValues) {
        try {

            PropertyValue nameProperty = propertyValues.getPropertyValue("name");
            if (Objects.isNull(nameProperty)) {
                log.info(">>>>>>>>>>>>> {} 的 name 为空,不进行 url 的设置", beanName);
                return;
            }
            PropertyValue contextIdProperty = propertyValues.getPropertyValue("contextId");
            if (Objects.isNull(contextIdProperty) || Objects.isNull(contextIdProperty.getValue())) {
                log.info(">>>>>>>>>>>>> {} 的 contextId 为空,不进行 url 的设置", nameProperty.getValue());
                return;
            }

            String contextId = contextIdProperty.getValue().toString();
            PropertyValue urlProperty = propertyValues.getPropertyValue("url");
            if (Objects.nonNull(urlProperty) && Objects.nonNull(urlProperty.getValue()) && StringUtils.isNotBlank((String)urlProperty.getValue())) {
                log.info(">>>>>>>>>>>>> {} 已经存在 url 属性 = {},故不进行 url 的设置", contextId, urlProperty.getValue());
                return;
            }

            // 没有 url 的进行设置
            String special = specialMap.getOrDefault(contextId, "");
            String urlVal = StringUtils.isNotBlank(special) ? special : defaultUrl;

            if (Objects.nonNull(urlProperty)) {
                urlProperty.setConvertedValue(urlVal);
                log.info(">>>>>>>>>>>>> {} 塞 url = {}", contextId, urlVal);
            }
        } catch (Exception e) {
            log.error(">>>>>>>>>>>>>  @FeignClient 进行 url 设置报错", e);
        }
    }


    public MyFeignClientInstantiation(SpringApplication application, String[] args) {
    }
    public MyFeignClientInstantiation() {
    }
    @Override
    public void starting() {
    }

    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        log.info(">>>>>>>>>>>>> @FeignClient 增加 MyFeignClientInstantiation");
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        beanFactory.addBeanPostProcessor(new MyFeignClientInstantiation());
    }

    @Override
    public void started(ConfigurableApplicationContext context) {
    }

    @Override
    public void running(ConfigurableApplicationContext context) {
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
    }
}

spring.factories

org.springframework.boot.SpringApplicationRunListener=com.MyFeignClientInstantiation

需要用到的小伙伴,复制的时候注意包名到时候改成自己的哈。

4  小结

好啦,到这里就结束啦,有的细节我没详细去说,还请见谅,有理解不对的地方欢迎指正哈。

posted @ 2024-01-17 17:11  酷酷-  阅读(2361)  评论(0编辑  收藏  举报