Zeichnen Sie den Analyseprozess eines verlustbehafteten RPC-Dienstes online auf

1. Problemhintergrund

Nachdem eine Anwendung mit der Bereitstellung von JSF-Diensten begann, kam es innerhalb kurzer Zeit zu einer großen Anzahl von Nullzeigerausnahmen.

Nach der Analyse des Protokolls wurde festgestellt, dass die Konfigurationsdaten des Zangjing Pavilion, von denen der Dienst abhängt, nicht geladen wurden. Dies ist die sogenannte verlustbehaftete Online- oder Direktfreigabe . Wenn die Anwendung gestartet wird, beginnt der Dienst, externe Dienste bereitzustellen, bevor er geladen wird, was zu einem fehlgeschlagenen Aufruf führt .

Der Schlüsselcode lautet wie folgt

Das anfängliche Laden der Daten wird durch die Implementierung der CommandLineRunner-Schnittstelle abgeschlossen.

@Component
public class LoadSystemArgsListener implements CommandLineRunner {

    @Resource
    private CacheLoader cjgConfigCacheLoader;

    @Override
    public void run(String... args) {
        // 加载藏经阁配置
        cjgConfigCacheLoader.refresh();

    }
}

Die Methode cjgConfigCacheLoader.refresh() lädt Daten intern in den Speicher

/** 藏经阁配置数据 key:租户 value:配置数据 */
public static Map<String, CjgRuleConfig> cjgRuleConfigMap = new HashMap<>();

Wenn die Daten zu diesem Zeitpunkt noch nicht geladen wurden und cjgRuleConfigMap.get("301").getXX() aufgerufen wird, wird eine Nullzeiger-Ausnahme gemeldet.

Um die Grundursache zusammenzufassen: Der JSF-Anbieter gibt das anfängliche Laden der Daten früher frei als Dienstabhängigkeiten, was zu fehlgeschlagenen Aufrufen führt.



2. Problemlösung

Bevor wir dieses Problem lösen, müssen wir uns an den Startvorgang von Spring Boot und den Veröffentlichungsprozess von JSF-Diensten erinnern und uns mit ihnen vertraut machen.

1) Spring Boot-Startvorgang (Version 2.0.7.RELEASE)

Die Run-Methode konzentriert sich hauptsächlich auf den Aktualisierungskontext von restartContext (context).

public ConfigurableApplicationContext run(String... args) {
    // 创建 StopWatch 实例:用于计算启动时间
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();

    // 获取SpringApplicationRunListeners:这些监听器会在启动过程的各个阶段发送对应的事件
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);

        // 创建并配置Environment:包括准备好对应的`Environment`,以及将`application.properties`或`application.yml`中的配置项加载到`Environment`中
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);

        // 打印Banner:如果 spring.main.banner-mode 不为 off,则打印 banner
        Banner printedBanner = printBanner(environment);

        // 创建应用上下文:根据用户的配置和classpath下的配置,创建合适的`ApplicationContext`
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);

        // 准备上下文:主要是将`Environment`、`ApplicationArguments`等关键属性设置到`ApplicationContext`中,以及加载`ApplicationListener`、`ApplicationRunner`、`CommandLineRunner`等。
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);

        // 刷新上下文:这是Spring IoC容器启动的关键,包括Bean的创建、依赖注入、初始化,发布事件等
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        // 打印启动信息:如果 spring.main.log-startup-info 为 true,则打印启动信息
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        // 发布 ApplicationStartedEvent:通知所有的 SpringApplicationRunListeners 应用已经启动
        listeners.started(context);
        
        // 调用 Runner:调用所有的ApplicationRunner和CommandLineRunner
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // 运行中:通知所有的 SpringApplicationRunListeners 应用正在运行
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

Die Methode „refresh()“ wird intern in „refreshContext(context)“ aufgerufen . Diese Methode konzentriert sich hauptsächlich auf die FinishBeanFactoryInitialization(beanFactory)-Instanziierung der Bean, die früher als „finishRefresh()“ erfolgt.

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // 准备刷新的上下文环境:设置启动日期,激活上下文,清除原有的属性源
        prepareRefresh();

        // 告诉子类启动 'refreshBeanFactory()' 方法,创建一个新的bean工厂。
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // 为 BeanFactory 设置上下文特定的后处理器:主要用于支持@Autowired和@Value注解
        prepareBeanFactory(beanFactory);

        try {
            // 为 BeanFactory 的处理提供在子类中的后处理器。
            postProcessBeanFactory(beanFactory);

            // 调用所有注册的 BeanFactoryPostProcessor Bean 的处理方法。
            invokeBeanFactoryPostProcessors(beanFactory);

            // 注册 BeanPostProcessor 的处理器,拦截 Bean 创建。
            registerBeanPostProcessors(beanFactory);

            // 为此上下文初始化消息源。
            initMessageSource();

            // 为此上下文初始化事件多播器。
            initApplicationEventMulticaster();

            // 在特定的上下文子类中刷新之前的进一步初始化。
            onRefresh();

            // 检查监听器 Bean 并注册它们:注册所有的ApplicationListenerbeans
            registerListeners();

            // 实例化所有剩余的(非延迟初始化)单例。
            finishBeanFactoryInitialization(beanFactory);

            // 完成刷新:发布ContextRefreshedEvent,启动所有Lifecyclebeans,初始化所有剩余的单例(lazy-init 单例和非延迟初始化的工厂 beans)。
            finishRefresh();
        }
        ...
    }


Wenn Sie eine Bean instanziieren, müssen Sie mit dem Lebenszyklus der Bean vertraut sein (wichtig).





 

2) Veröffentlichungsprozess des JSF-Anbieters (Version 1.7.5-HOTFIX-T6)

Die Klasse com.jd.jsf.gd.config.spring.ProviderBean ruft die Methode com.jd.jsf.gd.config.ProviderConfig#export zum Veröffentlichen auf

JSF-Quellcode-Adresse: http://xingyun.jd.com/codingRoot/jsf/jsf-sdk

public class ProviderBean<T> extends ProviderConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {
    
    // 此处代码省略...

    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ContextRefreshedEvent && this.isDelay() && !this.exported && !CommonUtils.isUnitTestMode()) {
            LOGGER.info("JSF export provider with beanName {} after spring context refreshed.", this.beanName);
            if (this.delay < -1) {
                Thread thread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            Thread.sleep((long)(-ProviderBean.this.delay));
                        } catch (Throwable var2) {
                        }

                        ProviderBean.this.export();
                    }
                });
                thread.setDaemon(true);
                thread.setName("DelayExportThread");
                thread.start();
            } else {
                this.export();
            }
        }

    }

    private boolean isDelay() {
        return this.supportedApplicationListener && this.delay < 0;
    }

    public void afterPropertiesSet() throws Exception {
        // 此处代码省略...

        if (!this.isDelay() && !CommonUtils.isUnitTestMode()) {
            LOGGER.info("JSF export provider with beanName {} after properties set.", this.beanName);
            this.export();
        }

    }
}

public synchronized void export() throws InitErrorException {
    if (this.delay > 0) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep((long)ProviderConfig.this.delay);
                } catch (Throwable var2) {
                }

                ProviderConfig.this.doExport();
            }
        });
        thread.setDaemon(true);
        thread.setName("DelayExportThread");
        thread.start();
    } else {
        this.doExport();
    }

}



Es ist ersichtlich, dass es zwei Orte gibt, an denen der Anbieter veröffentlicht wird

Ⅰ. Bean-Initialisierungsprozess (Verzögerung>=0)

Implementieren Sie die InitializingBean-Schnittstelle und überschreiben Sie die Methode afterPropertiesSet. Hier wird beurteilt, ob die Veröffentlichung verzögert werden soll. Wenn sie größer oder gleich 0 ist, wird sie hier freigegeben. Insbesondere wird bei der Exportmethode die Freigabe verzögert, wenn Verzögerung > 0. Wenn beispielsweise 5000 konfiguriert ist, bedeutet dies, dass die Freigabe um 5 Sekunden verzögert wird; wenn Verzögerung = 0, wird die Freigabe sofort freigegeben.



Ⅱ. Hören Sie auf den Ereignisauslöser „ContextRefreshedEvent“ (Verzögerung <0).

Implementieren Sie die ApplicationListener-Schnittstelle und überschreiben Sie die onApplicationEvent-Methode. Es gehört zum Ereignis ContextRefreshedEvent. Wenn die Verzögerung <-1 ist, wird die Veröffentlichung verzögert. Wenn beispielsweise -5000 konfiguriert ist, bedeutet dies eine verzögerte Veröffentlichung um 5 Sekunden; andernfalls wird sie sofort freigegeben.



3) Lösung



Szenario 1: Anbieter automatisch im XML-Modus veröffentlichen (häufig verwendet)

Aus der obigen Einführung kennen wir die Ausführungssequenz : 1. Bean-Initialisierung > 2. ContextRefreshedEvent-Ereignisauslöser > 3. ApplicationRunner oder CommandLineRunner aufrufen;

Es wurde oben bekannt, dass die Veröffentlichung durch den Anbieter in den Prozessen 1 und 2 erfolgt , und es ist notwendig, die Verwendung von Methode 3 zum Initialisieren von Daten zu vermeiden.

Voraussetzungsvorschlag: Die Standardkonfiguration der Verzögerung ist -1, die unkonfiguriert bleiben oder eine negative Zahl sein kann. Dann befindet sich die JSF-Provider-Veröffentlichung in Prozess 2, das heißt, sie lauscht auf das auszulösende ContextRefreshedEvent-Ereignis



Methode 1: Während des Initialisierungsprozesses von Bean

Lösung: Verwenden Sie die Annotation @PostConstruct, implementieren Sie die InitializingBean-Schnittstelle und konfigurieren Sie die Methode init-method.

@Component
public class DataLoader {

    @PostConstruct
    @Scheduled(cron = "${cron.config}")
    public void loadData() {
        // 数据加载
        System.out.println("数据加载工作");
    }

}

Hinweis: Wenn die Bean von anderen Beans abhängt, müssen Sie sicherstellen, dass die abhängigen Beans instanziiert wurden. Andernfalls wird eine Nullzeiger-Ausnahme gemeldet.



Methode 2: ContextRefreshedEvent-Ereignis ausgelöst

Wie das ContextRefreshedEvent-Ereignis veröffentlicht wird

Verwenden Sie den Eintrag AbstractApplicationContext#finishRefresh -> AbstractApplicationContext#publishEvent -> SimpleApplicationEventMulticaster#multicastEvent

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
   ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
   for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
      Executor executor = getTaskExecutor();
      if (executor != null) {
         executor.execute(() -> invokeListener(listener, event));
      }
      else {
         invokeListener(listener, event);
      }
   }
}

Rufen Sie invokeListener() in der multicastEvent-Methode von SimpleApplicationEventMulticaster auf, um Ereignisse zu veröffentlichen . Der Standardwert von getTaskExecutor() ist null (außer für benutzerdefinierte Executor-Objekte). Alle ApplicationListener-Implementierungsklassen führen die onApplicationEvent-Methode seriell aus.

getApplicationListeners(event, type) ruft alle Implementierungsklassen ab. Wenn Sie weiter nach unten schauen, wird AnnotationAwareOrderComparator.sort(allListeners) aufgerufen, um alle ApplicationListeners zu sortieren . allListeners ist eine Liste der zu sortierenden Objekte. Diese Methode bestimmt die Sortierreihenfolge basierend auf der Sortieranmerkung oder Schnittstelle des Objekts und gibt eine Liste von Objekten zurück, die in der angegebenen Reihenfolge sortiert sind. Im Einzelnen lauten die Sortierregeln wie folgt:

1. Sortieren Sie zunächst nach dem Wert der @Order- Annotation für das Objekt . Je kleiner der Wert der @Order- Annotation ist, desto höher ist die Sortierpriorität .
2. Wenn für das Objekt keine @Order- Annotation vorhanden ist oder der @Order- Annotationswert mehrerer Objekte gleich ist, werden die Objekte danach sortiert, ob sie die Ordered-Schnittstelle implementieren. Objekte, die die Ordered-Schnittstelle implementieren, können über die Methode getOrder() einen Sortierwert zurückgeben.
3. Wenn das Objekt weder die Annotation @Order noch die Schnittstelle Ordered hat, wird der Standardsortierwert LOWEST_PRECEDENCE (Integer.MAX_VALUE) verwendet . Besonderheit: Wenn die Sortierwerte von BeanA und BeanB beide Standardwerte sind, bleibt die ursprüngliche Reihenfolge, also die Ladereihenfolge der Beans, erhalten.



Zusammenfassung: Standardmäßig führen alle ApplicationListener-Implementierungsklassen die onApplicationEvent-Methode seriell aus, und die Reihenfolge hängt von AnnotationAwareOrderComparator.sort(allListeners) ab. Je kleiner der Wert der @Order-Annotation ist, desto höher ist die Sortierpriorität.

Lösung: Verwenden Sie die Annotation @Order, um sicherzustellen, dass die Ausführungsreihenfolge vor ProviderBean liegt

@Component
@Order(1)
public class DataLoader implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 数据准备
        System.out.println("初始化工作");
        
    }
}

Darüber hinaus kann es auch in der Startup-Klasse mit @SpringBootApplication implementiert werden ( Spring Boot verwendet standardmäßig annotationsbasierte Methoden zum Konfigurieren und Verwalten von Beans, sodass durch Annotationen definierte Beans vor durch XML definierten Beans geladen werden).

@SpringBootApplication
public class DemoApplication implements ApplicationListener<ContextRefreshedEvent> {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("初始化工作");
    }
}

Szenario 2: Veröffentlichungsanbieter über API (seltener verwendet)

Nach dem Start der Anwendung wird zunächst die Initialisierungsaktion durchgeführt und nach Abschluss der Provider manuell freigegeben. Auf diese Weise kann die Initialisierung durch Implementierung der ApplicationRunner-Schnittstelle oder der CommandLineRunner-Schnittstelle durchgeführt werden.

@Component
public class DataLoader implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 数据准备
        System.out.println("初始化工作");

        // 发布provider
        // 参考:https://cf.jd.com/pages/viewpage.action?pageId=296129902
    }
}

Szenario 3: Manuelle Veröffentlichung in XML (nicht häufig verwendet)

Das dynamische Attribut des Anbieters ist auf „false“ gesetzt

Etikett Attribute Typ Ist es erforderlich? Standardwert beschreiben
Anbieter dynamisch Boolescher Wert NEIN WAHR Gibt an, ob der Anbieter dynamisch registriert werden soll. Der Standardwert ist „true“. Bei der Konfiguration „false“ bedeutet dies, dass keine aktive Veröffentlichung erfolgt. Für Online-Vorgänge müssen Sie zum Verwaltungsterminal gehen.



3. Zusammenfassung

RPC-Dienste (wie JSF, Dubbo) werden elegant gestartet. Es gibt zwei häufig verwendete Methoden: 1. Verzögerte Veröffentlichung 2. Manueller Start

Wenn Ihr Dienst einige Initialisierungsvorgänge erfordert, bevor er Dienste für die Außenwelt bereitstellen kann, z. B. die Initialisierung des Caches (nicht beschränkt auf Zangjing Pavilion, DCCC, MySQL oder sogar das Aufrufen anderer JSF-Dienste), den Redis-Verbindungspool und andere verwandte Ressourcen, Sie können auf verschiedene Arten auf die Einleitung in diesem Artikel verweisen.

Dieser Artikel ist die Schlussfolgerung des Autors durch Lesen des Quellcodes + lokale Überprüfung. Wenn es Fehler oder Auslassungen oder bessere Lösungen gibt, weisen Sie bitte darauf hin und machen Sie gemeinsam Fortschritte!



Autor: JD Retail Guo Hongyu

Quelle: JD Cloud Developer Community Bitte geben Sie beim Nachdruck die Quelle an

Broadcom kündigt die Beendigung des bestehenden Deepin-IDE-Versionsupdates des VMware-Partnerprogramms an und ersetzt das alte Erscheinungsbild durch ein neues Erscheinungsbild Zhou Hongyi: Der gebürtige Hongmeng wird definitiv erfolgreich sein WAVE SUMMIT begrüßt seine zehnte Sitzung, Wen Xinyiyan wird die neueste Enthüllung haben! Yakult Company bestätigt, dass 95 G-Daten durchgesickert sind Die beliebteste Lizenz unter den Programmiersprachen im Jahr 2023 „2023 China Open Source Developer Report“ offiziell veröffentlicht Julia 1.10 offiziell veröffentlicht Fedora 40 plant die Vereinheitlichung von /usr/bin und /usr/sbin Rust 1.75 .0-Version
{{o.name}}
{{m.name}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/10451996
Recomendado
Clasificación