哈喽咱们好啊,我是Hydra。
在往常的作业中,OpenFeign
作为微服务间的调用组件运用的非常遍及,接口合作注解的调用办法突出一个简洁,让咱们能无需关注内部细节就能完成服务间的接口调用。
可是作业中用久了,发现Feign也有些运用起来费事的当地,下面先来看一个问题,再看看咱们在作业中是怎样解决,以达到简化Feign运用的目的。
先看问题
在一个项目开发的过程中,咱们通常会区别开发环境、测验环境和出产环境,假如有的项目要求更高的话,或许还会有个预出产环境。
开发环境作为和前端开发联调的环境,一般运用起来都比较随意,而咱们在进行本地开发的时候,有时候也会将本地发动的微服务注册到注册中心nacos上,便利进行调试。
这样,注册中心的一个微服务或许就会具有多个服务实例,就像下面这样:
眼尖的小伙伴肯定发现了,这两个实例的ip地址有一点不同。
线上环境现在一般运用容器化布置,通常都是由流水线工具打成镜像然后扔到docker中运转,因此咱们去看一下服务在docker容器内的ip:
能够看到,这便是注册到nacos上的服务地址之一,而列表中192
最初的另一个ip,则是咱们本地发动的服务的局域网地址。看一下下面这张图,就能对整个流程一望而知了。
总结一下:
- 两个service都是经过宿主机的ip和port,把自己的信息注册到nacos上
- 线上环境的service注册时运用docker内部ip地址
- 本地的service注册时运用本地局域网地址
那么这时候问题就来了,当我本地再发动一个serviceB,经过FeignClient
来调用serviceA中的接口时,由于Feign本身的负载均衡,就或许把恳求负载均衡到两个不同的serviceA实例。
假如这个调用恳求被负载均衡到本地serviceA的话,那么没什么问题,两个服务都在同一个192.168
网段内,能够正常拜访。可是假如负载均衡恳求到运转在docker内的serviceA的话,那么问题来了,由于网络不通,所以会恳求失败:
说白了,便是本地的192.168
和docker内的虚拟网段172.17
属于纯二层的两个不同网段,不能互访,所以无法直接调用。
那么,假如想在调试时把恳求安稳打到本地服务的话,有一个办法,便是指定在FeignClient
中增加url
参数,指定调用的地址:
@FeignClient(value = "hydra-service",url = "http://127.0.0.1:8088/")
public interface ClientA {
@GetMapping("/test/get")
String get();
}
可是这么一来也会带来点问题:
- 代码上线时需求再把注解中的
url
删掉,还要再次修正代码,假如忘了的话会引起线上问题 - 假如测验的
FeignClient
许多的话,每个都需求装备url
,修正起来很费事
那么,有什么办法进行改善呢?为了解决这个问题,咱们还是得从Feign的原理说起。
Feign原理
Feign的完成和作业原理,我曾经写过一篇简略的源码分析,咱们能够简略花个几分钟先衬托一下,Feign中心源码解析。明白了原理,后边理解起来更便利一些。
简略来说,便是项目中加的@EnableFeignClients
这个注解,完成时有一行很重要的代码:
@Import(FeignClientsRegistrar.class)
这个类完成了ImportBeanDefinitionRegistrar
接口,在这个接口的registerBeanDefinitions
办法中,能够手动创立BeanDefinition
并注册,之后spring会依据BeanDefinition
实例化生成bean,并放入容器中。
Feign便是经过这种办法,扫描增加了@FeignClient
注解的接口,然后一步步生成署理目标,详细流程能够看一下下面这张图:
后续在恳求时,经过署理目标的FeignInvocationHandler
进行阻拦,并依据对应办法进行处理器的分发,完成后续的http恳求操作。
ImportBeanDefinitionRegistrar
上面提到的ImportBeanDefinitionRegistrar
,在整个创立FeignClient
的署理过程中非常重要, 所以咱们先写一个简略的例子看一下它的用法。先界说一个实体类:
@Data
@AllArgsConstructor
public class User {
Long id;
String name;
}
经过BeanDefinitionBuilder
,向这个实体类的构造办法中传入详细值,最终生成一个BeanDefinition
:
public class MyBeanDefinitionRegistrar
implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder
= BeanDefinitionBuilder.genericBeanDefinition(User.class);
builder.addConstructorArgValue(1L);
builder.addConstructorArgValue("Hydra");
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
registry.registerBeanDefinition(User.class.getSimpleName(),beanDefinition);
}
}
registerBeanDefinitions
办法的详细调用时刻是在之后的ConfigurationClassPostProcessor
执行postProcessBeanDefinitionRegistry
办法时,而registerBeanDefinition
办法则会将BeanDefinition
放进一个map中,后续依据它实例化bean。
在装备类上经过@Import
将其引进:
@Configuration
@Import(MyBeanDefinitionRegistrar.class)
public class MyConfiguration {
}
注入这个User
测验:
@Service
@RequiredArgsConstructor
public class UserService {
private final User user;
public void getUser(){
System.out.println(user.toString());
}
}
成果打印,阐明咱们经过自界说BeanDefinition
的办法成功手动创立了一个bean并放入了spring容器中:
User(id=1, name=Hydra)
好了,准备作业衬托到这结束,下面开端正式的改造作业。
改造
到这儿先总结一下,咱们纠结的点便是本地环境需求FeignClient
中装备url
,但线上环境不需求,而且咱们又不想来回修正代码。
除了像源码中那样生成动态署理以及阻拦办法,官方文档中还给咱们供给了一个手动创立FeignClient的办法。
docs.spring.io/spring-clou…
简略来说,便是咱们能够像下面这样,经过Feign的Builder API来手动创立一个Feign客户端。
简略看一下,这个过程中还需求装备Client
、Encoder
、Decoder
、Contract
、RequestInterceptor
等内容。
-
Client
:实际http恳求的发起者,假如不涉及负载均衡能够运用简略的Client.Default
,用到负载均衡则能够运用LoadBalancerFeignClient
,前面也说了,LoadBalancerFeignClient
中的delegate
其实运用的也是Client.Default
-
Encoder
和Decoder
:Feign的编解码器,在spring项目中运用对应的SpringEncoder
和ResponseEntityDecoder
,这个过程中咱们借用GsonHttpMessageConverter
作为消息转换器来解析json -
RequestInterceptor
:Feign的阻拦器,一般事务用处比较多,比方增加修正header信息等,这儿用不到能够不配 -
Contract
:字面意思是合约,它的作用是将咱们传入的接口进行解析验证,看注解的运用是否符合规范,然后将关于http的元数据抽取成成果并返回。假如咱们运用RequestMapping
、PostMapping
、GetMapping
之类注解的话,那么对应运用的是SpringMvcContract
其实这儿刚需的就只有Contract
这一个,其他都是可选的装备项。咱们写一个装备类,把这些需求的东西都注入进去:
@Slf4j
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({LocalFeignProperties.class})
@Import({LocalFeignClientRegistrar.class})
@ConditionalOnProperty(value = "feign.local.enable", havingValue = "true")
public class FeignAutoConfiguration {
static {
log.info("feign local route started");
}
@Bean
@Primary
public Contract contract(){
return new SpringMvcContract();
}
@Bean(name = "defaultClient")
public Client defaultClient(){
return new Client.Default(null,null);
}
@Bean(name = "ribbonClient")
public Client ribbonClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory){
return new LoadBalancerFeignClient(defaultClient(), cachingFactory,
clientFactory);
}
@Bean
public Decoder decoder(){
HttpMessageConverter httpMessageConverter=new GsonHttpMessageConverter();
ObjectFactory<HttpMessageConverters> messageConverters= () -> new HttpMessageConverters(httpMessageConverter);
SpringDecoder springDecoder = new SpringDecoder(messageConverters);
return new ResponseEntityDecoder(springDecoder);
}
@Bean
public Encoder encoder(){
HttpMessageConverter httpMessageConverter=new GsonHttpMessageConverter();
ObjectFactory<HttpMessageConverters> messageConverters= () -> new HttpMessageConverters(httpMessageConverter);
return new SpringEncoder(messageConverters);
}
}
在这个装备类上,还有三行注解,咱们一点点解说。
首先是引进的装备类LocalFeignProperties
,里面有三个属性,分别是是否开启本地路由的开关、扫描FeignClient接口的包名,以及咱们要做的本地路由映射关系,addressMapping
中存的是服务名和对应的url地址:
@Data
@Component
@ConfigurationProperties(prefix = "feign.local")
public class LocalFeignProperties {
// 是否开启本地路由
private String enable;
//扫描FeignClient的包名
private String basePackage;
//路由地址映射
private Map<String,String> addressMapping;
}
下面这行注解则表示只有当装备文件中feign.local.enable
这个属性为true
时,才使当时装备文件生效:
@ConditionalOnProperty(value = "feign.local.enable", havingValue = "true")
最终,便是咱们重中之重的LocalFeignClientRegistrar
了,咱们还是依照官方经过ImportBeanDefinitionRegistrar
接口构建BeanDefinition
然后注册的思路来完成。
而且,FeignClientsRegistrar
的源码中现已完成好了许多基础的功用,比方扫扫描包、获取FeignClient
的name
、contextId
、url
等等,所以需求改动的当地非常少,能够放心的大抄特超它的代码。
先创立LocalFeignClientRegistrar
,并注入需求用到的ResourceLoader
、BeanFactory
、Environment
。
@Slf4j
public class LocalFeignClientRegistrar implements
ImportBeanDefinitionRegistrar, ResourceLoaderAware,
EnvironmentAware, BeanFactoryAware{
private ResourceLoader resourceLoader;
private BeanFactory beanFactory;
private Environment environment;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader=resourceLoader;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void setEnvironment(Environment environment) {
this.environment=environment;
}
//先省掉详细功用代码...
}
然后看一下创立BeanDefinition
前的作业,这一部分首要完成了包的扫描和检测@FeignClient
注解是否被增加在接口上的测验。下面这段代码基本上是照搬源码,除了改动一下扫描包的途径,运用咱们自己在装备文件中装备的包名。
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = ComponentScanner.getScanner(environment);
scanner.setResourceLoader(resourceLoader);
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
scanner.addIncludeFilter(annotationTypeFilter);
String basePackage =environment.getProperty("feign.local.basePackage");
log.info("begin to scan {}",basePackage);
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
log.info(candidateComponent.getBeanClassName());
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = FeignCommonUtil.getClientName(attributes);
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
接下来创立BeanDefinition
并注册,Feign的源码中是运用的FeignClientFactoryBean
创立署理目标,这儿咱们就不需求了,直接替换成运用Feign.builder
创立。
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
String contextId = FeignCommonUtil.getContextId(beanFactory, attributes,environment);
String name = FeignCommonUtil.getName(attributes,environment);
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(clazz, () -> {
Contract contract = beanFactory.getBean(Contract.class);
Client defaultClient = (Client) beanFactory.getBean("defaultClient");
Client ribbonClient = (Client) beanFactory.getBean("ribbonClient");
Encoder encoder = beanFactory.getBean(Encoder.class);
Decoder decoder = beanFactory.getBean(Decoder.class);
LocalFeignProperties properties = beanFactory.getBean(LocalFeignProperties.class);
Map<String, String> addressMapping = properties.getAddressMapping();
Feign.Builder builder = Feign.builder()
.encoder(encoder)
.decoder(decoder)
.contract(contract);
String serviceUrl = addressMapping.get(name);
String originUrl = FeignCommonUtil.getUrl(beanFactory, attributes, environment);
Object target;
if (StringUtils.hasText(serviceUrl)){
target = builder.client(defaultClient)
.target(clazz, serviceUrl);
}else if (StringUtils.hasText(originUrl)){
target = builder.client(defaultClient)
.target(clazz,originUrl);
}else {
target = builder.client(ribbonClient)
.target(clazz,"http://"+name);
}
return target;
});
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
definition.setLazyInit(true);
FeignCommonUtil.validate(attributes);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String[] qualifiers = FeignCommonUtil.getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[] { contextId + "FeignClient" };
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
在这个过程中首要做了这么几件事:
- 经过
beanFactory
拿到了咱们在前面创立的Client
、Encoder
、Decoder
、Contract
,用来构建Feign.Builder
- 经过注入装备类,经过
addressMapping
拿到装备文件中服务对应的调用url
- 经过
target
办法替换要恳求的url
,假如装备文件中存在则优先运用装备文件中url
,不然运用@FeignClient
注解中装备的url
,假如都没有则运用服务名经过LoadBalancerFeignClient
拜访
在resources/META-INF
目录下创立spring.factories
文件,经过spi注册咱们的自动装备类:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.feign.local.config.FeignAutoConfiguration
最终,本地打包即可:
mvn clean install
测验
引进咱们在上面打好的包,由于包中现已包含了spring-cloud-starter-openfeign
,所以就不需求再额外引feign
的包了:
<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>feign-local-enhancer</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在装备文件中增加装备信息,启用组件:
feign:
local:
enable: true
basePackage: com.service
addressMapping:
hydra-service: http://127.0.0.1:8088
trunks-service: http://127.0.0.1:8099
创立一个FeignClient
接口,注解的url
中咱们能够随意写一个地址,能够用来测验之后是否会被装备文件中的服务地址覆盖:
@FeignClient(value = "hydra-service",
contextId = "hydra-serviceA",
url = "http://127.0.0.1:8099/")
public interface ClientA {
@GetMapping("/test/get")
String get();
@GetMapping("/test/user")
User getUser();
}
发动服务,过程中能够看见了执行扫描包的操作:
在替换url
过程中增加一个断点,能够看到即使在注解中装备了url
,也会优先被装备文件中的服务url
覆盖:
运用接口进行测验,能够看到运用上面的署理目标进行了拜访并成功返回了成果:
假如项目需求发布正式环境,只需求将装备feign.local.enable
改为false
或删掉,并在项目中增加Feign原始的@EnableFeignClients
即可。
总结
本文供给了一个在本地开发过程中简化Feign调用的思路,比较之前需求费事的修正FeignClient
中的url
而言,能够节约不少的无效劳动,而且经过这个过程,也能够协助咱们了解咱们往常运用的这些组件是怎样与spring结合在一起的,熟悉spring的扩展点。
组件代码已提交到我的github,有需求的小伙伴们能够自取,码字不易,也欢迎咱们点个star~
github.com/trunks2008/…
那么,这次的共享就到这儿,我是Hydra,咱们下篇再会。
作者简介,
码农参上
,一个热爱共享的公众号,有趣、深化、直接,与你聊聊技能。欢迎关注、增加好友,进一步沟通。