nacos动态配置刷新机制原理

2023年 9月 16日 61.5k 0

nacos动态配置刷新机制原理

项目里面许多业务场景以及灵活配置的需求经常要用到动态配置。一般就是apollo和nacos两种选型。

nacos动态刷新导致的bug

nacos一般为了实现动态配置一般会加入@RefreshScope注解进行实现,例如下面的代码加入了@RefreshScope想要实现跨域的动态配置,例如跨域白名单等。

@slf4j
@configuration
@RefreshScope
public class FilterConfiguration {
@value("${jlpay.business.filter.allowCredentials:true}")
private boolean allowCredentials;
@value("${jlpay.business.filter.allowedHeader:}")
private String allowedHeader;
@value("${jlpay.business.filter.allowedMethod:}")
private String allowedMethod;
@value("${jlpay.business.filter.allowedOrigin:*}")
private String allowedOrigin;
@value("${jlpay.business.filter.corsPath:/**}")
private String corsPath;
​
/**
 * 跨域请求配置
 * @return
 */
private CorsConfiguration buildCorsConfig() {
    CorsConfiguration corsConfig = new CorsConfiguration();
    corsConfig.setAllowCredentials(allowCredentials);
    corsConfig.addAllowedHeader(allowedHeader);
    corsConfig.addAllowedMethod(allowedMethod);
    //指定域名拦截配置
    if (!StringUtils.isEmpty(allowedOrigin) && !CorsConfiguration.ALL.equals(allowedOrigin)) {
        String[] originArr = allowedOrigin.split(",");
        for (String origin : originArr) {
            corsConfig.addAllowedOrigin(origin);
        }
    } else {
        corsConfig.addAllowedOrigin(CorsConfiguration.ALL);
    }
​
    return corsConfig;
}
​
/**
 * 跨域请求过滤器配置
 */
@Bean
public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
    //注:暂定所有接口均允许跨域,建议针对具体接口配置允许跨域
    configSource.registerCorsConfiguration(corsPath, buildCorsConfig());
​
    log.info("跨域请求过滤器配置:{}", JSONObject.toJSONString(configSource.getCorsConfigurations()));
    return new CorsFilter(configSource);
}
​

但是发现这个注解后直接导致了跨域配置失效,前端访问我接口的时候报了跨域错误。问题的根因在于serverletContext没有注入生成的filterbean,为什么没有注入,是serverletContext在初始化的时候去找符合的filer类,是通过getBeanNamesForType获取的注入的filter类名称,按道理根据filter实现统一接口类是可以获取到的,但是这个方法isFactoryBean 判定了CorsFilter是factorybean,因此,CorsFilter没有被获取到加入到filter链中。所以@RefreshScope注解影响了CorsFilter bean的生成 方式。下面仔细看下@RefreshScope的原理。

​
@Override
public boolean isFactoryBean(String name) throws NoSuchBeanDefinitionException {
   String beanName = transformedBeanName(name);
   Object beanInstance = getSingleton(beanName, false);
   if (beanInstance != null) {
      return (beanInstance instanceof FactoryBean);
   }
   // No singleton instance found -> check bean definition.
   if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory cbf) {
      // No bean definition found in this factory -> delegate to parent.
      return cbf.isFactoryBean(name);
   }
   return isFactoryBean(beanName, getMergedLocalBeanDefinition(beanName));
}

因为@RefreshScope 导致FilterConfiguration 变成了一个facotorybean, 导致无法加载了serverletContext的filter链中。 所以解决防范是建议使用ConfigurationProperties bean 方式使用,@RefreshScope并不是一个很好的使用方式。

RefreshScope动态刷新原理

RefreshScope注解的Bean可以在运行时刷新,并且使用它们的任何组件都将在下一个方法调用前获得一个新实例,该实例将完全初始化并注入所有依赖项。

public @interface RefreshScope {
​
  /**
     * @see Scope#proxyMode()
     * @return proxy mode
     */
  ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
​

RefreshScope用到的是spring bean的scope模式能力。scope模式在获取bean时和单例的Bean以及多例的bean不太一样。scope 里自定义了一个scope范围来隔离不同的scope,在获取bean时会优先从实现了Scope接口的类中获取,简单来说,scope模式的bean的获取方式通过scope接口实现类获取,是一种代理模式。

//org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 
// Create bean instance.
   if (mbd.isSingleton()) {
      sharedInstance = getSingleton(beanName, () -> {
         try {
            return createBean(beanName, mbd, args);
         }
          //......
   }
​
   else if (mbd.isPrototype()) {
      // It's a prototype -> create a new instance.
      Object prototypeInstance = null;
      try {
         beforePrototypeCreation(beanName);
         prototypeInstance = createBean(beanName, mbd, args);
      }
       //......
   }
​
   else {
      String scopeName = mbd.getScope();
      if (!StringUtils.hasLength(scopeName)) {
         throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
      }
      Scope scope = this.scopes.get(scopeName);
      if (scope == null) {
         throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
      }
      try {
         Object scopedInstance = scope.get(beanName, () -> {
            beforePrototypeCreation(beanName);
            try {
               return createBean(beanName, mbd, args);
            }
            finally {
               afterPrototypeCreation(beanName);
            }
         });
         beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
      }
      catch (IllegalStateException ex) {
         throw new ScopeNotActiveException(beanName, scopeName, ex);
      }
   }
}
catch (BeansException ex) {
    //......
}
finally {
    //......
}

RefreshScope 就是其中一个代理类实现,scope.get由genericScope实现。

image.png
GenericScope是一个beanFactoryPostprocess,在bean实列化前就会执行postProcessBeanFactory将自己注册放scopes中去,这样前面的Bean获取单例就会从对应scopeName中获取scope,而scope是一个保存了代理类的map,最终执行的代理类的Invoke方法。

//postProcessBeanFactory会在实例化执行
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        beanFactory.registerScope(this.name, this);
        setSerializationId(beanFactory);
    }
​
 
public void registerScope(String scopeName, Scope scope) {
        Assert.notNull(scopeName, "Scope identifier must not be null");
        Assert.notNull(scope, "Scope must not be null");
        if (SCOPE_SINGLETON.equals(scopeName) || SCOPE_PROTOTYPE.equals(scopeName)) {
            throw new IllegalArgumentException("Cannot replace existing scopes 'singleton' and 'prototype'");
        }
        Scope previous = this.scopes.put(scopeName, scope);
        if (previous != null && previous != scope) {
            if (logger.isDebugEnabled()) {
                logger.debug("Replacing scope '" + scopeName + "' from [" + previous + "] to [" + scope + "]");
            }
        }
        else {
            if (logger.isTraceEnabled()) {
                logger.trace("Registering scope '" + scopeName + "' with implementation [" + scope + "]");
            }
        }
    }

这个代理类RefreshScope 注解生成的是LockedScopedProxyFactoryBean,下面看看代理类是怎么生成的以及invoke代理的方法。

RefreshScope 注解类代理类注册容器

RefreshScope 注解的类 在生成Bean 定义时会生成两份定义,一份是原来的类的定义,另一份时代理类LockedScopedProxyFactoryBean的定义,且LockedScopedProxyFactoryBean时一个factorybean,通过getObject方法获取实际的proxy对象,实际的proxy对象通过getBeanFactory().getBean(getTargetBeanName()),即ioc容器中获取原始bean。

代理类定义

springboot bean 加载过程是利用@ComponentScan会扫描并注册包下带有@Componet注解的类为Bean(@Configuration 实际上也是一个@Componet),达到一个注解驱动注册Bean的效果。扫描Bean并注册为BeanDefinition这一过程是由ClassPathBeanDefinitionScanner类的doScan方法去做的。

protected Set doScan(String... basePackages) {
  Assert.notEmpty(basePackages, "At least one base package must be specified");
  Set beanDefinitions = new LinkedHashSet();
  // 扫描basePackages所在的包下的所有的类,带@Componet的类都会被注册为BeanDefinition
  for (String basePackage : basePackages) {
        //...
      if (checkCandidate(beanName, candidate)) {
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
        // 这里就是Scope的Bean的统一处理,是一个改变BeanDefinition的回调机会
          // 调用createScopedProxy创建一个scope 代理
        definitionHolder =
          AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        beanDefinitions.add(definitionHolder);
        // 注册BeanDefinition到IOC容器中
        registerBeanDefinition(definitionHolder, this.registry);
      }
    }
  }
  return beanDefinitions;
}
​

注意AnnotationConfigUtils.applyScopedProxyMode方法,这里提供了一个代理bean定义的方式,若具体是在 创建bean时会判断类是否带@refreshScope注解,然后创建ScopedProxyFactoryBean bean工厂用来创建代理bean。

public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
                                                     BeanDefinitionRegistry registry, boolean proxyTargetClass) {
​
  // ...
​
  // Create a scoped proxy definition for the original bean name,
  // "hiding" the target bean in an internal target definition.
  // 重点,这里构造函数中将beanClass设置为了ScopedProxyFactoryBean.class
  RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
  // targetDefinition是被代理的原生Bean
  proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
  
  // ...
​
  // Return the scoped proxy definition as primary bean definition
  // (potentially an inner bean).
  return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}
​
​

postProcessBeanDefinitionRegistry回调方法中会针对刚刚那个beanClass为ScopedProxyFactoryBean.class的BeanDefinition进行一个增强处理,最终生成的代理类定义的是LockedScopedProxyFactoryBean。

// GenericScope.class
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
  throws BeansException {
  // 获取所有BeanDefinition的名称
  for (String name : registry.getBeanDefinitionNames()) {
    BeanDefinition definition = registry.getBeanDefinition(name);
    // 针对RootBeanDefinition这个BeanDefinition来做,这和上面的逻辑吻合
    if (definition instanceof RootBeanDefinition) {
      RootBeanDefinition root = (RootBeanDefinition) definition;
      // 判断BeanClass == ScopedProxyFactoryBean.class
      if (root.getDecoratedDefinition() != null && root.hasBeanClass()
          && root.getBeanClass() == ScopedProxyFactoryBean.class) {
        if (getName().equals(root.getDecoratedDefinition().getBeanDefinition()
                             .getScope())) {
          // 将BeanClass换为LockedScopedProxyFactoryBean
          root.setBeanClass(LockedScopedProxyFactoryBean.class);
          root.getConstructorArgumentValues().addGenericArgumentValue(this);
          // surprising that a scoped proxy bean definition is not already
          // marked as synthetic?
          root.setSynthetic(true);
        }
      }
    }
  }
}
​

代理类创建

LockedScopedProxyFactoryBean的父类ScopedProxyFactoryBean 实现了FactoryBean,FactoryBean在Spring中最为典型的一个应用就是用来创建AOP的代理对象。proxy就是生成的实际代理对象。

​
public Object getObject() {
  if (this.proxy == null) {
    throw new FactoryBeanNotInitializedException();
  }
  return this.proxy;
}
​

image.png

proxy是通过 ScopedProxyFactoryBean 生成的,该类实现了 BeanFactoryAware(该接口用于知道创建Bean的beanfactory,即ioc容器),因此setBeanFactory会在比较早的时机被回调用来生成proxy.

public void setBeanFactory(BeanFactory beanFactory) {
  
  // ...
  
    // 这里是一个比较关键的点,scopedTargetSource变量是一个SimpleBeanTargetSource
  // scopedTargetSource中保存了IOC容器
  this.scopedTargetSource.setBeanFactory(beanFactory);
​
  // 创建动态代理前,将动态代理的信息都保存到ProxyFactory中
  ProxyFactory pf = new ProxyFactory();
  pf.copyFrom(this);
  // 注意,这里的TargetSource就是刚刚说的scopedTargetSource
  pf.setTargetSource(this.scopedTargetSource);
  
    // ...
​
  this.proxy = pf.getProxy(cbf.getBeanClassLoader());
}
​

proxy的代理方法如下, 代理实现是CGLib动态代理都会实现一个MethodInterceptor,被代理的类的每一个方法调用实质上都是在调用MethodInterceptor的intercept方法。

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
  // 反复强调的TargetSource
  TargetSource targetSource = this.advised.getTargetSource();
  try {
    // ...
    // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
    // 重点,这里调用了targetSource的getTarget
    // target变量就是被代理的类,调用实际方法的时候反射调用其对应方法
    target = targetSource.getTarget();
   // ...
    if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
      // ...
      // target变量就是被代理的类,调用实际方法的时候反射调用其对应方法
      retVal = methodProxy.invoke(target, argsToUse);
    }
    else {
      // We need to create a method invocation...
      // target变量就是被代理的类,调用实际方法的时候反射调用其对应方法
      retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
    }
    // target变量就是被代理的类,调用实际方法的时候反射调用其对应方法
    retVal = processReturnType(proxy, target, method, retVal);
    return retVal;
  }
  // ...
}
​

前面讲到TargetSource的实现类是SimpleBeanTargetSource:通过bean容器获取代理的bean.

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
​
  @Override
  public Object getTarget() throws Exception {
    // 从IOC中getBean
    return getBeanFactory().getBean(getTargetBeanName());
  }
}
​

bean注册

this.registry 的类是实现了BeanDefinitionRegistry 是一个接口,它定义了关于 BeanDefinition 的注册、移除、查询等一系列的操作。该接口有三个实现类:DefaultListableBeanFactory、GenericApplicationContext、SimpleBeanDefinitionRegistry。BeanDefinitionRegistry 集成了SingletonBeanRegistry ,接口的核心实现类是 DefaultSingletonBeanRegistry(该类存储 Bean 之间的依赖关系、存储 Bean 的包含关系(外部类包含内部类)、获取 Bean 所处的状态(正在创建、创建完毕等)、回调销毁 Bean 时触发的 destroy 方法等。)用来注册bean到ioc容器。

执行完 registerBeanDefinition 方法后,Bean 的名称和对应的 BeanDefinition 就被放入了容器中,后续获取 Bean 也是从这个容器中获取。

image.png

    /**
     * 注册Bean基于给定的bean factory.
     * @param definitionHolder bean定义类
     * @param registry bean factory注册
     * @throws BeanDefinitionStoreException if registration failed
     */
    public static void registerBeanDefinition(
            BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
            throws BeanDefinitionStoreException {
​
        // 注册bean 定义
        String beanName = definitionHolder.getBeanName();
        registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
​
        // 证据Bean 别名
        String[] aliases = definitionHolder.getAliases();
        if (aliases != null) {
            for (String alias : aliases) {
                registry.registerAlias(beanName, alias);
            }
        }
    }
​
    @Override
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
            throws BeanDefinitionStoreException {
        //......
​
            if (hasBeanCreationStarted()) {
                // 双重判断,将bean定义放入beanDefinitionMap (ioc容器)
                synchronized (this.beanDefinitionMap) {
                    this.beanDefinitionMap.put(beanName, beanDefinition);
                    List updatedDefinitions = new ArrayList(this.beanDefinitionNames.size() + 1);
                    updatedDefinitions.addAll(this.beanDefinitionNames);
                    updatedDefinitions.add(beanName);
                    this.beanDefinitionNames = updatedDefinitions;
                    removeManualSingletonName(beanName);
                }
            }
               //......

动态配置刷新

前面讲到通过beanfactory会创建以及获取bean, 这里scope.get获取的Scope对象为RefreshScope,创建Bean还是由IOC来做(createBean方法),但是获取Bean,都由RefreshScope对象的get方法去获取,其get方法在父类GenericScope中实现。scop利用hash map管理的ioc创建的bean

​
Object scopedInstance = scope.get(beanName, () -> {
                            beforePrototypeCreation(beanName);
                            try {
                                return createBean(beanName, mbd, args);
                            }
                            finally {
                                afterPrototypeCreation(beanName);
                            }
                        });
​
public Object get(String name, ObjectFactory objectFactory) {
  // 将原始Bean缓存下来
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 获取原始类bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}
#BeanLifecycleWrapper getbean
public Object getBean() {
  // 因爲是新的BeanLifecycleWrapper實例,這裏必定爲null
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        // 調用IOC容器的createBean,再建立一個Bean出來
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

BeanLifecycleWrapper生成了一个原始的包装类,其中getBean是返回前面说到的原始类的bean。那么原始类的beab和代理类的bean的关系就出来了。

代理类bean会被其他bean引用,代理原始bean进行执行,并且原始bean可以看出是由GenericScope来管理生命周期。所以当配置发生变化时,只需要将scop缓存的bean移除且将原始bean值设置为null,既可以让代理类重新生成新的原始bean,此时获取的配置也是最新的。

// 配置发生变化时调用

public synchronized Set refresh() {
    Set keys = refreshEnvironment();
    this.scope.refreshAll();
    return keys;
}
@ManagedOperation(description = "Dispose of the current instance of all beans in this scope and force a refresh on next method execution.")
public void refreshAll() {
    super.destroy();
    this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
//进行对象获取,如果没有就创建并放入缓存
@Override
public Object get(String name, ObjectFactory objectFactory) {
    BeanLifecycleWrapper value = this.cache.put(name,
            new BeanLifecycleWrapper(name, objectFactory));
    locks.putIfAbsent(name, new ReentrantReadWriteLock());
    try {
        return value.getBean();
    }
    catch (RuntimeException e) {
        this.errors.put(name, e);
        throw e;
    }
}
//进行缓存的数据清理
@Override
public void destroy() {
    List errors = new ArrayList();
    Collection wrappers = this.cache.clear();
    for (BeanLifecycleWrapper wrapper : wrappers) {
        try {
            Lock lock = locks.get(wrapper.getName()).writeLock();
            lock.lock();
            try {
                wrapper.destroy();
            }
            finally {
                lock.unlock();
            }
        }
        catch (RuntimeException e) {
            errors.add(e);
        }
    }
    if (!errors.isEmpty()) {
        throw wrapIfNecessary(errors.get(0));
    }
    this.errors.clear();
}

总结

@refreshScope注解用了两种代理的模式去实现动态刷新bean,一个是scope代理原始bean的生成和销毁,另一个是aop模式代理原始bean,去获取修改配置后新配置值生成的bean。值得注意的是@RefreshScope会使得bean自动销毁和生成,可能会导致一些莫名其妙的问题,例如常见的@scheduled注解失效、线程池bean不断被创建等等,因此实际生产中最好控制动态变量用ConfigurationProperties 注解生产,该注解是通过设置变量值的方式进行配置的修改,更加的安全。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论