【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 小结
好啦,到这里就结束啦,有的细节我没详细去说,还请见谅,有理解不对的地方欢迎指正哈。