前文中,我自定义一个gatewayRule,实现了根据参数进行路由,从而实现灰度发布的功能。在本地自测的时候,运行良好。放到测试环境后,发现一个重大bug。所有的服务都会走同一个路由,所以服务A请求服务B时,却会调用到服务C!
自己引入的bug,自己跪着也要解决。开始经历了层层排查历史,并最终解决问题。
一,接着前文讲起,RibbonLoadBalancerClient中
public ServiceInstance choose(String serviceId, Object hint) {
//查询server
Server server = getServer(getLoadBalancer(serviceId), hint);
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
}
protected ILoadBalancer getLoadBalancer(String serviceId) {
//通过SpringClientFactory 查询LoadBalancer
return this.clientFactory.getLoadBalancer(serviceId);
}
看到需要通过SpringClientFactory,查找 ILoadBalancer
二,我们看下SpringClientFactory
public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {
public ILoadBalancer getLoadBalancer(String name) {
return getInstance(name, ILoadBalancer.class);
}
@Override
public <C> C getInstance(String name, Class<C> type) {
// 通过父类查询
C instance = super.getInstance(name, type);
if (instance != null) {
return instance;
}
IClientConfig config = getInstance(name, IClientConfig.class);
return instantiateWithConfig(getContext(name), type, config);
}
@Override
protected AnnotationConfigApplicationContext getContext(String name) {
return super.getContext(name);
}
可以看到进入了父类,即NamedContextFactory
三,NamedContextFactory
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
private Map<String, C> configurations = new ConcurrentHashMap<>();
//获取实例,先获取该name的context
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type).length > 0) {
return context.getBean(type);
}
return null;
}
//不存在该context,则创建新context
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
//不存在则装甲
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
//具体创建Context的过程,并在该context中初始加载一些类
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//将包含本name的类注册
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
//将所有detault开头的类注册
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object>singletonMap(this.propertyName, name)));
//将本上下文注册父类及ClassLoader
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
// jdk11 issue
// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
context.setClassLoader(this.parent.getClassLoader());
}
context.setDisplayName(generateDisplayName(name));
//上下文刷新
context.refresh();
return context;
}
可以看到,会默认注册configurations下面的default开头,和key为本name的类。
那么就需要看下这些configurations类究竟是哪些了。
很容易查到,这些configurations的注册地点为RibbonAutoConfiguration
四,RibbonAutoConfiguration
@Configuration
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
//注意这个RibbonClients注解
@RibbonClients
@AutoConfigureAfter(
name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class,
AsyncLoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class,
ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {
// configurations是一个List<RibbonClientSpecification>
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Bean
@ConditionalOnMissingBean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
//在生成SpringClientFactory时注入了configurations
factory.setConfigurations(this.configurations);
return factory;
}
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
那么这些List又是从哪来的呢?
我们查看RibbonClientSpecification被使用的地方,发现RibbonClientConfigurationRegistrar类在用,进入看一下
五,RibbonClientConfigurationRegistrar
public class RibbonClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
//获取被@RibbonClient和@RibbonClients注释的类,将其注册为RibbonClientSpecification
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> attrs = metadata
.getAnnotationAttributes(RibbonClients.class.getName(), true);
if (attrs != null && attrs.containsKey("value")) {
AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
for (AnnotationAttributes client : clients) {
registerClientConfiguration(registry, getClientName(client),
client.get("configuration"));
}
}
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
attrs.get("defaultConfiguration"));
}
Map<String, Object> client = metadata
.getAnnotationAttributes(RibbonClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
//注册为RibbonClientSpecification
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(RibbonClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + ".RibbonClientSpecification",
builder.getBeanDefinition());
}
这里需要重点说一下,实现了ImportBeanDefinitionRegistrar接口,该接口是spring实现bean动态注册的流程。所有实现了该接口的类都会被ConfigurationClassPostProcessor处理,ConfigurationClassPostProcessor实现了BeanFactoryPostProcessor接口,最终注册到容器中。
不过ImportBeanDefinitionRegistrar需要跟@Configuration和@Import配合使用。我们查找看一下,使用到该类的有@RibbonClient 和@RibbonClients两个注解。
@Configuration(proxyBeanMethods = false)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients {
RibbonClient[] value() default {};
Class<?>[] defaultConfiguration() default {};
}
所以这一段总体上是,在RibbonClientConfigurationRegistrar中,所有被@RibbonClient和@RibbonClients注解的类,都被Sping注册为RibbonClientSpecification。从而在RibbonAutoConfiguration中被SpringClientFactory使用,被SpringClientFactory父类NamedContextFactory注册到新生成的context 上下文中。
所以如果想被context注册使用,那么该bean就需要在@RibbonClients或@RibbonClient注释的类中。
这也是目前市面上所有做gateway灰度发布路由的文章都存在多个服务被指向同一个IRule的原因。
也就是我本次遇到的bug。
理解了源码,解决起来也很简单,将我们的自定义路由注册到Context中就好啦。
新建一个Configuration,引入路由Irule的bean,并使用@RibbonClients和@Configuration注解,就好啦。
以上。