刘耀文

刘耀文

java开发者
github

springcloud[boot]配置センターの自動更新メカニズム

実現メカニズム:#

bean 属性の自動更新原理:#

spring2 の時に、カスタムスコープが追加されました。つまり、シングルトンとプロトタイプの他に、スコープ注釈とインターフェースが追加され、bean のストレージライフサイクルを向上させるために関連するインターフェースとクラスは

ConfigurableBeanFactory.registerScope, 
CustomScopeConfigurer, 
org.springframework.aop.scope.ScopedProxyFactoryBean, org.springframework.web.context.request.RequestScope,
org.springframework.web.context.request.SessionScope

実装の基本原理は

  • パッケージスキャンの際に、@scope インターフェースを認識すると、beandefinition を ScopedProxyFactoryBean に変更します。具体的には
	protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "少なくとも1つの基本パッケージを指定する必要があります");
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				candidate.setScope(scopeMetadata.getScopeName());
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
				if (candidate instanceof AbstractBeanDefinition abstractBeanDefinition) {
					postProcessBeanDefinition(abstractBeanDefinition, beanName);
				}
				if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
					AnnotationConfigUtils.processCommonDefinitionAnnotations(annotatedBeanDefinition);
				}
                // ここで、すべてのscope注釈のクラスを見つけ、定義をScopedProxyFactoryBeanに変更します
				if (checkCandidate(beanName, candidate)) {
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
					definitionHolder =
							AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
		return beanDefinitions;
	}

コードを追っていくと

    public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition, BeanDefinitionRegistry registry, boolean proxyTargetClass) {
        String originalBeanName = definition.getBeanName();
        BeanDefinition targetDefinition = definition.getBeanDefinition();
        String targetBeanName = getTargetBeanName(originalBeanName);
        // ここでScopedProxyFactoryBeanのbean定義が作成されます
        RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
        // 元のクラスを設定します。つまり、scope注釈の付いたクラスです
        proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
        proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
        proxyDefinition.setSource(definition.getSource());
        proxyDefinition.setRole(targetDefinition.getRole());
        proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
        if (proxyTargetClass) {
            targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
        } else {
            proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
        }

        proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
        proxyDefinition.setPrimary(targetDefinition.isPrimary());
        if (targetDefinition instanceof AbstractBeanDefinition abd) {
            proxyDefinition.copyQualifiersFrom(abd);
        }

        targetDefinition.setAutowireCandidate(false);
        targetDefinition.setPrimary(false);
        registry.registerBeanDefinition(targetBeanName, targetDefinition);
        return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
    }

ScopedProxyFactoryBean は何をするのでしょうか?なぜこのクラスに変更する必要があるのか、構造を見てみましょう

// FactoryBeanを実装していることがわかります(そうでなければbeanを取得できませんし、元のbeanをさらにラップしています)、またBeanFactoryAwareも実装しており、BeanFactoryを取得してインスタンスbeanを取得するためです。AopInfrastructureBeanインターフェースは、このクラスがaopのロジックを実装できることを示しており、aop自身によってラップされないことを示しています。このインターフェースを実装しているのは、非常に重要なInfrastructureAdvisorAutoProxyCreatorとAspectJAwareAdvisorAutoProxyCreatorであり、一つはspringに組み込まれているaop、もう一つはAspectJを有効にしたaopです。この部分も非常に興味深いので、時間があればspringaopに関する記事を更新します。
public class ScopedProxyFactoryBean extends ProxyConfig implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean

FactoryBean を実装しているので、ここで取得した bean がどのように処理されるのかを見てみましょう。非常に簡単です。まず、BeanFactory を設定する際にプロキシを生成します

    public void setBeanFactory(BeanFactory beanFactory) {
        if (beanFactory instanceof ConfigurableBeanFactory cbf) {
            this.scopedTargetSource.setBeanFactory(beanFactory);
            ProxyFactory pf = new ProxyFactory();
            pf.copyFrom(this);
            pf.setTargetSource(this.scopedTargetSource);
            Assert.notNull(this.targetBeanName, "プロパティ'targetBeanName'は必須です");
            Class beanType = beanFactory.getType(this.targetBeanName);
            if (beanType == null) {
                throw new IllegalStateException("bean '" + this.targetBeanName + "' のスコーププロキシを作成できません: プロキシ作成時にターゲットタイプを特定できませんでした。");
            } else {
                if (!this.isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) {
                    pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader()));
                }

                ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName());
                pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));
                pf.addInterface(AopInfrastructureBean.class);
                this.proxy = pf.getProxy(cbf.getBeanClassLoader());
            }
        } else {
            throw new IllegalStateException("ConfigurableBeanFactoryで実行されていません: " + beanFactory);
        }
    }

次に、bean を取得する際にプロキシを返します

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

生成されたプロキシは何に役立つのでしょうか?プロキシクラスのロジックをさらに見てみると、実際にはメソッドを呼び出すたびに、最初に aop コールバックメソッドに入って元のオブジェクトを取得します


		public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
			Object oldProxy = null;
			boolean setProxyContext = false;
			Object target = null;
			TargetSource targetSource = this.advised.getTargetSource();
			try {
				if (this.advised.exposeProxy) {
					// 必要に応じて呼び出しを利用可能にします。
					oldProxy = AopContext.setCurrentProxy(proxy);
					setProxyContext = true;
				}
                // ここで、beanfactoryから元のオブジェクトを取得します
				target = targetSource.getTarget();
				Class<?> targetClass = (target != null ? target.getClass() : null);
				List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
				Object retVal;

				if (chain.isEmpty()) {

					Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
					retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
				}
				else {
					// メソッド呼び出しを作成する必要があります...
					retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
				}
				return processReturnType(proxy, target, method, args, retVal);
			}
			finally {
				if (target != null && !targetSource.isStatic()) {
					targetSource.releaseTarget(target);
				}
				if (setProxyContext) {
					// 古いプロキシを復元します。
					AopContext.setCurrentProxy(oldProxy);
				}
			}
		}

では、最初の問題に戻りましょう。どこで動的に更新が行われるのでしょうか?実際には、設定センターの設定が変更されたときに bean を破棄し、次回呼び出すと最新の bean を取得できるのです。確かに、springcloud はこれを行っています。springcloud は refreshscope 注釈を使用して RefreshScope クラスと組み合わせて、refreshscope 注釈は ScopedProxyFactoryBean にラップされ、RefreshScope クラスは bean のライフサイクルを処理します。つまり、取得した bean は元の beanfactory から取得されるのではなく、RefreshScope から取得されます。ソースコードは以下の通りです。

if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
        try {
            return createBean(beanName, mbd, args);
        }
        catch (BeansException ex) {
            // シングルトンキャッシュからインスタンスを明示的に削除します: 作成プロセスによって早期に追加された可能性があり、循環参照の解決を許可するためです。
            // 一時的にbeanへの参照を受け取ったbeanも削除します。
            destroySingleton(beanName);
            throw ex;
        }
    });
    beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

else if (mbd.isPrototype()) {
    // プロトタイプです -> 新しいインスタンスを作成します。
    Object prototypeInstance = null;
    try {
        beforePrototypeCreation(beanName);
        prototypeInstance = createBean(beanName, mbd, args);
    }
    finally {
        afterPrototypeCreation(beanName);
    }
    beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

else {
    
    String scopeName = mbd.getScope();
    if (!StringUtils.hasLength(scopeName)) {
        throw new IllegalStateException("bean '" + beanName + "' のスコープ名が定義されていません");
    }
    Scope scope = this.scopes.get(scopeName);
    if (scope == null) {
        throw new IllegalStateException("スコープ名 '" + 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);
    }
}

スコープは RefreshScope の中で登録されており、BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor を実装しています。

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    this.beanFactory = beanFactory;
    // RefreshScopeを登録して、上記でスコープを取得して呼び出すのを容易にします
    beanFactory.registerScope(this.name, this);
    setSerializationId(beanFactory);
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    for (String name : registry.getBeanDefinitionNames()) {
        BeanDefinition definition = registry.getBeanDefinition(name);
        if (definition instanceof RootBeanDefinition root) {
            if (root.getDecoratedDefinition() != null && root.hasBeanClass()
                    && root.getBeanClass() == ScopedProxyFactoryBean.class) {
                if (getName().equals(root.getDecoratedDefinition().getBeanDefinition().getScope())) {
                    // ここで、パッケージスキャンされたScopedProxyFactoryBeanをさらにLockedScopedProxyFactoryBeanに変更します。もちろん、refreshの場合はロックを追加するためです。このロジックは重要ではありません。
                    root.setBeanClass(LockedScopedProxyFactoryBean.class);
                    root.getConstructorArgumentValues().addGenericArgumentValue(this);
                    // スコーププロキシbean定義がすでに合成としてマークされていないのは驚きですか?
                    root.setSynthetic(true);
                }
            }
        }
    }
}

さて、ここまで来ました。少し休憩しましょう。上記で何をしたのかを振り返ってみましょう。

  • @refreshscope で注釈された bean を LockedScopedProxyFactoryBean でラップし、毎回呼び出すたびに RefreshScope から bean オブジェクトを取得します。
  • RefreshScope は springcloud によって登録され、springcloud-context の自動構成クラスの中で beanfactory に登録されます。
    では、なぜ毎回取得する際に最新のオブジェクトが得られるのでしょうか?私たちは自然に、毎回設定を更新する際に RefreshScope が保存した bean を破棄し、次回呼び出す際に最新の bean を取得することを考えます。ここでも同じことが行われています。毎回更新する際には RefreshScope に通知して破棄します。ソースコードは以下の通りです。
public void refreshAll() {
    super.destroy();
    this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
@Override
public void destroy() {
    List<Throwable> errors = new ArrayList<>();
    // refreshAllを呼び出すとすべてのキャッシュがクリアされるので、次回取得する際には最新のものになります
    Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
    for (BeanLifecycleWrapper wrapper : wrappers) {
        try {
            Lock lock = this.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();
}

では、いつ呼び出されるのでしょうか?ConfigDataContextRefresher の中で呼び出されるのが見えます。

public synchronized Set<String> refresh() {
    Set<String> keys = refreshEnvironment();
    this.scope.refreshAll();
    return keys;
}
// springcloudはRefreshScopeを登録します
@Bean
@ConditionalOnMissingBean
@ConditionalOnBootstrapDisabled
public ConfigDataContextRefresher configDataContextRefresher(ConfigurableApplicationContext context,
        RefreshScope scope, RefreshProperties properties) {
    return new ConfigDataContextRefresher(context, scope, properties);
}

ConfigDataContextRefresher は誰が呼び出すのでしょうか?RefreshEventListener の中で見ることができます。

@Override
public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationReadyEvent) {
        handle((ApplicationReadyEvent) event);
    }
    // RefreshEventを受信した後に更新します
    else if (event instanceof RefreshEvent) {
        handle((RefreshEvent) event);
    }
}

public void handle(ApplicationReadyEvent event) {
    this.ready.compareAndSet(false, true);
}

public void handle(RefreshEvent event) {
    if (this.ready.get()) { // アプリが準備できる前にイベントを処理しない
        log.debug("受信したイベント " + event.getEventDesc());
        Set<String> keys = this.refresh.refresh();
        log.info("更新されたキー: " + keys);
    }
}
// springcloudは上記のConfigDataContextRefresherを注入します
@Bean
public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
    return new RefreshEventListener(contextRefresher);
}

最後に RefreshEvent イベントは誰が発行するのでしょうか?nacos-config 依存関係を導入すると、関連する bean が注入されるのが見えます。中を見てみましょう。

@Bean
public NacosContextRefresher nacosContextRefresher(
        NacosConfigManager nacosConfigManager,
        NacosRefreshHistory nacosRefreshHistory) {
    // 以前の設定と互換性を持たせる必要はありません
    // 必要に応じて新しい設定を使用します。
    return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
}

private void registerNacosListener(final String groupKey, final String dataKey) {
    String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
    Listener listener = listenerMap.computeIfAbsent(key,
            lst -> new AbstractSharedListener() {
                @Override
                public void innerReceive(String dataId, String group,
                        String configInfo) {
                    refreshCountIncrement();
                    nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
                    NacosSnapshotConfigManager.putConfigSnapshot(dataId, group,
                            configInfo);
                    // 毎回設定が更新されるとRefreshEventイベントが発行されるのが見えます
                    applicationContext.publishEvent(
                            new RefreshEvent(this, null, "Nacos設定を更新"));
                    if (log.isDebugEnabled()) {
                        log.debug(String.format(
                                "Nacos設定を更新しました group=%s,dataId=%s,configInfo=%s",
                                group, dataId, configInfo));
                    }
                }
            });
    try {
        configService.addListener(dataKey, groupKey, listener);
        log.info("[Nacos Config] 設定をリスニング: dataId={}, group={}", dataKey,
                groupKey);
    }
    catch (NacosException e) {
        log.warn(String.format(
                "nacosリスナーの登録に失敗しました, dataId=[%s], group=[%s]", dataKey,
                groupKey), e);
    }
}

さて、ついにここまで来ました。つまり、nacos が設定を更新するたびに RefreshEvent イベントが発行され、RefreshEventListener がイベントを受信して ConfigDataContextRefresher の refresh を呼び出し、さらに RefreshScope の refresh を呼び出し、キャッシュをクリアします。次回取得する際には最新のものになります。

もう一つのこと、更新プロセスを見てみましょう#

public synchronized Set<String> refresh() {
    Set<String> keys = refreshEnvironment();
    this.scope.refreshAll();
    return keys;
}
// 環境を更新する際にEnvironmentChangeEventイベントも発生します。これはnacosのConfigurationPropertiesRebinderのイベントソースで、すべてのConfigurationPropertiesBeansを再設定します。
public synchronized Set<String> refreshEnvironment() {
    Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
    updateEnvironment();
    Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
    this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
    return keys;
}

// ここで
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
@ConditionalOnNonDefaultBehavior
public ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder(
        ConfigurationPropertiesBeans beans) {
    // デフォルトの動作を使用する場合、SmartConfigurationPropertiesRebinderを使用しません。
    // ミスを最小限に抑えます。
    return new SmartConfigurationPropertiesRebinder(beans);
}
内部では:すべてのConfigurationPropertiesBeansを収集します
private boolean rebind(String name, ApplicationContext appContext) {
    try {
        Object bean = appContext.getBean(name);
        if (AopUtils.isAopProxy(bean)) {
            bean = ProxyUtils.getTargetObject(bean);
        }
        if (bean != null) {
            // TODO: より一般的なアプローチを決定する必要があります。
            // https://github.com/spring-cloud/spring-cloud-commons/issues/571を参照
            if (getNeverRefreshable().contains(bean.getClass().getName())) {
                return false; // 無視します
            }
            appContext.getAutowireCapableBeanFactory().destroyBean(bean);
            appContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
            return true;
        }
    }
    catch (RuntimeException e) {
        this.errors.put(name, e);
        throw e;
    }
    catch (Exception e) {
        this.errors.put(name, e);
        throw new IllegalStateException("Cannot rebind to " + name, e);
    }
    return false;
}

結論#

上記が refreshscope の自動更新プロセスです。実際にはもう一点、nacos がどのように設定の更新とイベントの発行を監視しているのか、これには netty が関与しています。具体的には、nacos には設定の変更を確認するための定期的なタスクがあります。

@Override
public void startInternal() {
    executor.schedule(() -> {
        while (!executor.isShutdown() && !executor.isTerminated()) {
            try {
                listenExecutebell.poll(5L, TimeUnit.SECONDS);
                if (executor.isShutdown() || executor.isTerminated()) {
                    continue;
                }
                // ここで
                executeConfigListen();
            } catch (Throwable e) {
                LOGGER.error("[rpc listen execute] [rpc listen] 例外", e);
                try {
                    Thread.sleep(50L);
                } catch (InterruptedException interruptedException) {
                    // 無視します
                }
                notifyListenConfig();
            }
        }
    }, 0L, TimeUnit.MILLISECONDS);
    
}

netty についてはまだ学んでいませんので、次回さらに見ていきたいと思います。また、bootstrap がどのようにリモート設定を取得するか、EnvironmentPostProcessorApplicationListener で設定を取得する方法についても書きたいと思います。設定の取得にも関連しています。springboot2.4 以降、bootstrap が廃止され、EnvironmentPostProcessorApplicationListener が提案され、設定のインポートがより便利になりました。

この記事は Mix Space によって xLog に同期更新されました。元のリンクは https://me.liuyaowen.club/posts/default/20240816and1

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。