Я использовал приложение Spring Boot 3.2.4 на Win 10 и Win Server 2019, и оно работало нормально. Я знаю, что архитектура моего приложения не очень хороша, но она сработала. После того, как мне пришлось перейти на Ubuntu 24.04, при запуске того же приложения выбрасывается NullPointerException
в ApplicationContextProvider.getApplicationContext()
. JDK на всех машинах один и тот же — openjdk-18.0.1.
Я использую «простой» способ получить applicationContext
, который мне показался удобным, его можно найти здесь:
@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 не вызывается
Я думаю, что ошибка связана с порядком инициализации компонентов, когда Spring строит контекст.
Нет никакой гарантии, что ApplicationContextProvider
будет создан раньше MainBuilderUtil
. Порядок построения bean-компонентов может меняться в зависимости от множества разных факторов и может меняться, казалось бы, случайно, когда Spring не знает о необходимой зависимости.
Простое решение этой проблемы — использовать @DependsOn на MainBuilderUtil
, чтобы убедиться, что ApplicationContextProvider
создается первым.
Более чистое решение состоит в том, чтобы признать, что MainChannelCredentialsUtil уже автоматически подключается к конструктору MainBuilderUtil
. Конструктор может инициализировать credentialMain
и clientMain
и вообще не использовать ApplicationContextProvider
.
В общем, пытаться использовать applicationContext для получения bean-компонентов во время создания экземпляра компонента — плохая идея, поскольку это нарушает обычные механизмы разрешения зависимостей Spring и требует использования таких хаков, как @DependsOn.
Я также хотел бы отметить, что если бы MainChannelCredentialsUtil
не подключался автоматически к MainBuilderUtil
, то не было бы никакой гарантии, что он будет создан раньше MainBuilderUtil
. В этом случае он также не будет доступен через ApplicationContextProvider
.
Спасибо! На данный момент @DependsOn({"applicationContextProvider"})
на MainBuilderUtil
решает проблему. Также спасибо за идею более чистого исправления, буду использовать ее для переписывания класса.
Такой класс, как 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
, является хаком, и его следует избегать.
Ваш
ApplicationContextProvider
— это хак, и его следует избегать, насколько это возможно. ВашMainBuilderUtil
— это Spring bean-компонент (с аннотацией@Component
), поэтому просто внедрите зависимости, а не полагайтесь на этот хак для получения bean-компонентов.