Почему ApplicationContextProvider выдает исключение NullPointerException в другой системе

Я использовал приложение Spring Boot 3.2.4 на Win 10 и Win Server 2019, и оно работало нормально. Я знаю, что архитектура моего приложения не очень хороша, но она сработала. После того, как мне пришлось перейти на Ubuntu 24.04, при запуске того же приложения выбрасывается NullPointerException в ApplicationContextProvider.getApplicationContext(). JDK на всех машинах один и тот же — openjdk-18.0.1.

Я использую «простой» способ получить applicationContext, который мне показался удобным, его можно найти здесь:

ApplicationContextProvider

@Component
public class ApplicationContextProvider implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        ApplicationContextProvider.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}

Класс, в котором возникает ошибка:

@Component
public class MainBuilderUtil {
    MainChannelCredentialsUtil mainChannelCredentialsUtil;

    public final OAuth2Credential credentialMain = new OAuth2Credential("provider",
            ApplicationContextProvider.getApplicationContext().getBean(MainChannelCredentialsUtil.class).getMainToken());

    @Autowired
    private MainBuilderUtil(MainChannelCredentialsUtil mainChannelCredentialsUtil) {
        this.mainChannelCredentialsUtil = mainChannelCredentialsUtil;
    }

    public final Client ClientMain =
            ClientBuilder.builder()
                    .withEnableChat(true)
                    .withChatAccount(credentialMain)
                    .build();

    //getters
}
    

ошибка:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.bot.springbootbot.connections.channels.builder_utils.MainBuilderUtil]: Constructor threw exception
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:221) ~[spring-beans-6.1.5.jar!/:6.1.5]
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:111) ~[spring-beans-6.1.5.jar!/:6.1.5]
    at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:315) ~[spring-beans-6.1.5.jar!/:6.1.5]
    ... 24 common frames omitted
Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.context.ApplicationContext.getBean(java.lang.Class)" because the return value of "com.bot.springbootbot.ApplicationContextProvider.getApplicationContext()" is null
    at com.bot.springbootbot.connections.channels.builder_utils.MainBuilderUtil.<init>(MainBuilderUtil.java:18) ~[!/:0.0.1-SNAPSHOT]
    at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486) ~[na:na]
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:208) ~[spring-beans-6.1.5.jar!/:6.1.5]
    ... 26 common frames omitted

Также есть это предупреждение во время запуска tomcat перед ОШИБКОЙ, в основном то же самое:

WARN 6417 --- [SpringBootBot] [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mainBuilderUtil' defined in URL [jar:nested:/usr/SpringBootBot/target/SpringBootBot-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/com/bot/springbootbot/connections/channels/builder_utils/MainBuilderUtil.class]: Failed to instantiate [com.bot.springbootbot.connections.channels.builder_utils.MainBuilderUtil]: Constructor threw exception

Я знаю, что этот класс даже не обязательно должен быть Spring Bean, но я изучал инъекции, и обходной путь ApplicationContextProvider сработал. Поэтому, прежде чем переписывать весь свой код, я просто хочу понять, почему код работает на Win10, а не на Ubuntu. Я пробовал разные jdks, ничего не работает. Во всех случаях запускайте его с помощью java -jar. Возможно, есть очевидная причина и решение, которых я не вижу. Пробовал делать шаги отсюда - никакого эффекта: applicationContextProvider не вызывается

Ваш ApplicationContextProvider — это хак, и его следует избегать, насколько это возможно. Ваш MainBuilderUtil — это Spring bean-компонент (с аннотацией @Component), поэтому просто внедрите зависимости, а не полагайтесь на этот хак для получения bean-компонентов.

M. Deinum 15.08.2024 07:40
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
1
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я думаю, что ошибка связана с порядком инициализации компонентов, когда Spring строит контекст.

Нет никакой гарантии, что ApplicationContextProvider будет создан раньше MainBuilderUtil. Порядок построения bean-компонентов может меняться в зависимости от множества разных факторов и может меняться, казалось бы, случайно, когда Spring не знает о необходимой зависимости.

Простое решение этой проблемы — использовать @DependsOn на MainBuilderUtil, чтобы убедиться, что ApplicationContextProvider создается первым.

Более чистое решение состоит в том, чтобы признать, что MainChannelCredentialsUtil уже автоматически подключается к конструктору MainBuilderUtil. Конструктор может инициализировать credentialMain и clientMain и вообще не использовать ApplicationContextProvider.

В общем, пытаться использовать applicationContext для получения bean-компонентов во время создания экземпляра компонента — плохая идея, поскольку это нарушает обычные механизмы разрешения зависимостей Spring и требует использования таких хаков, как @DependsOn.

Я также хотел бы отметить, что если бы MainChannelCredentialsUtil не подключался автоматически к MainBuilderUtil, то не было бы никакой гарантии, что он будет создан раньше MainBuilderUtil. В этом случае он также не будет доступен через ApplicationContextProvider.

Спасибо! На данный момент @DependsOn({"applicationContextProvider"}) на MainBuilderUtil решает проблему. Также спасибо за идею более чистого исправления, буду использовать ее для переписывания класса.

maximuz666 14.08.2024 21:00

Такой класс, как ApplicationContextProvider, является хаком, и его следует избегать. Проблема в том, что вы используете это в компоненте, который уже управляется Spring и получает зависимости. Чтобы правильно исправить, перепишите свой класс, чтобы использовать внедрение зависимостей вместо взлома.

@Component
public class MainBuilderUtil {
    private MainChannelCredentialsUtil mainChannelCredentialsUtil;

    private final OAuth2Credential credentialMain;
    private final Client ClientMain;

    @Autowired
    private MainBuilderUtil(MainChannelCredentialsUtil mainChannelCredentialsUtil) {
        this.mainChannelCredentialsUtil = mainChannelCredentialsUtil;
        this.credentialMain = new OAuth2Credential("provider", mainChannelCredentialsUtil.getMainToken();
        this.ClientMain = ClientBuilder.builder()
                    .withEnableChat(true)
                    .withChatAccount(credentialMain)
                    .build();
    }
    //getters
}

Если вы напрямую ссылаетесь на эти поля public final из других классов, прекратите это делать и вместо этого внедрите MainBuilderUtil в эти классы и используйте методы доступа (геттеры) для доступа к свойствам (или даже лучше создайте эти bean-компоненты и вместо этого внедрите их).

Короче говоря, использование такого класса, как ApplicationContextProvider, является хаком, и его следует избегать.

Другие вопросы по теме