Я пишу приложение Spring Boot, используя зависимость spring-integration-mqtt, где я использую bean-компонент CommandLineRunner для запуска MQTTSubscriber при запуске приложения.
Однако, когда я запускаю приложение, я получаю следующую ошибку:
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2019-01-26 01:48:40.386 ERROR 59171 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Field MessageListener in im.sma.mqtt.mqttclient.DemoApplication required a single bean, but 2 were found:
- messageListener: defined in file [/Users/sma/sandbox/slidecab/mqtt/mqtt-client/target/classes/im/sma/mqtt/mqttclient/config/MessageListener.class]
- integrationHeaderChannelRegistry: defined in null
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Я заметил, что ошибка исчезает, когда я удаляю из своего кода следующую часть:
@Autowired
Runnable MessageListener;
@Bean
public CommandLineRunner schedulingRunner(TaskExecutor executor) {
return new CommandLineRunner() {
public void run(String... args) throws Exception {
executor.execute(MessageListener);
}
};
}
Это мой класс DemoApplication, где происходит ошибка:
@SpringBootApplication
public class DemoApplication extends SpringBootServletInitializer {
@Autowired
Runnable MessageListener;
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DemoApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public CommandLineRunner schedulingRunner(TaskExecutor executor) {
return new CommandLineRunner() {
public void run(String... args) throws Exception {
executor.execute(MessageListener);
}
};
}
}
Кроме того, у меня также есть следующий класс AppConfig для настройки TaskExecutor:
@Component
public class AppConfig {
@Bean
@Primary
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
}
}
И это bean-компонент MessageListener, который не может быть автоматически подключен:
@Component
public class MessageListener implements Runnable {
@Autowired
MQTTSubscriberBase subscriber;
@Override
public void run() {
while(true) {
subscriber.subscribeMessage("demoTopic2019");
}
}
}
Кроме того, у меня есть следующая конфигурация для настройки MQTTSubscriber:
public abstract class MQTTConfig {
protected final String broker = "localhost";
protected final int qos = 2;
protected Boolean hasSSL = false; /* By default SSL is disabled */
protected Integer port = 1883; /* Default port */
protected final String userName = "guest" ;//"testUserName";
protected final String password = "guest";//"demoPassword";
protected final String TCP = "tcp://";
protected final String SSL = "ssl://";
protected abstract void config(String broker, Integer port, Boolean ssl, Boolean withUserNamePass);
protected abstract void config();
}
public interface MQTTSubscriberBase {
public static final Logger logger = LoggerFactory.getLogger(MQTTSubscriberBase.class);
public void subscribeMessage(String topic);
public void disconnect();
}
@Component
public class MQTTSubscriber extends MQTTConfig implements MqttCallback, MQTTSubscriberBase {
private String brokerUrl = null;
private String colon = ":";
private String clientId = "demoClient2";
private MqttClient client = null;
private MqttConnectOptions options = null;
private MemoryPersistence persistence = null;
private static final Logger logger = LoggerFactory.getLogger(MQTTSubscriber.class);
public MQTTSubscriber() {
this.config();
}
@Override
public void connectionLost(Throwable cause) {
logger.info("Connection lost");
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String time = new Timestamp(System.currentTimeMillis()).toString();
System.out.println();
System.out.println("********************************************************");
System.out.println("Message arrived at " + time + "Topic: " + topic + " Message: " + new String(message.getPayload()));
System.out.println("********************************************************");
System.out.println();
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
// Not required for subscriber
}
@Override
public void subscribeMessage(String topic) {
try {
this.client.subscribe(topic, this.qos);
} catch (MqttException exception) {
logger.error("ERROR", exception);
}
}
@Override
public void disconnect() {
try {
this.client.disconnect();
} catch (MqttException exception) {
logger.error("ERROR: ", exception);
}
}
@Override
public void config(String broker, Integer port, Boolean ssl, Boolean withUserNamePass) {
String protocol = this.TCP;
if (true == ssl) {
protocol = this.SSL;
}
this.brokerUrl = protocol + broker + this.colon + port;
this.persistence = new MemoryPersistence();
this.options = new MqttConnectOptions();
try {
this.client = new MqttClient(this.brokerUrl, clientId, persistence);
this.options.setCleanSession(true);
if (true == withUserNamePass) {
if (this.password != null) {
this.options.setPassword(this.password.toCharArray());
}
if (this.userName != null) {
this.options.setUserName(this.userName);
}
}
this.client.connect(this.options);
this.client.setCallback(this);
}
catch(MqttException exception) {
this.logger.error("ERROR ", exception);
}
}
@Override
public void config() {
this.brokerUrl = this.TCP + this.broker + this.colon + this.port;
this.persistence = new MemoryPersistence();
this.options = new MqttConnectOptions();
try {
this.client = new MqttClient(brokerUrl, clientId, persistence);
this.options.setCleanSession(true);
this.client.connect(options);
this.client.setCallback(this);
}
catch(MqttException exception) {
logger.error("ERROR", exception);
}
}
}
И это зависимости, которые я использую:
<?xml version = "1.0" encoding = "UTF-8" standalone = "yes"?>
<project xmlns = "http://maven.apache.org/POM/4.0.0" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/>
</parent>
<groupId>im.sma.mqtt</groupId>
<artifactId>mqtt-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
<version>5.1.2.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Почему я не могу ввести бин и что делать?
Одним из возможных решений было объявить один из двух bean-компонентов как @Primary, я пробовал, этот вариант все еще не работает. То, как я использую интерфейс CommandLineRunner, — это выполнение кода команды при запуске приложения и поддержание его работы.
Вы пытались добавить @Qualifier("messageListener") рядом с аннотацией @Autowired для поля messageListener?
@ g00glen00b Да, я пробовал, и это работает, большое спасибо, вы спасаете жизнь.
@ g00glen00b Я также нашел другое решение: если использовать прямую зависимость от клиента paho ieorg.eclipse.paho.client.mqttv3 (из репозитория eclipse в repo.eclipse.org/content/repositories/paho-releases) вместо spring-integration-mqtt, тогда все работает нормально даже без использования @Qualifier ("messageListener") аннотация.
@ g00glen00b g00glen00b будет действительно полезно, если вы сможете объяснить решение «@Qualifier» в контексте упомянутой проблемы.
Я создал подробный ответ, охватывающий объяснение решений, предложенных Spring, и, кроме того, несколько решений, которые также могут работать, например, удаление библиотеки интеграции Spring, как вы упомянули.
@ g00glen00b, да, я прочитал ваш подробный ответ, кстати, почему я получаю отрицательные баллы за этот вопрос?
Я не минусовал вас, но я предполагаю, что люди могут подумать, что это вопрос с небольшими усилиями, потому что (1) решение находится в сообщении об ошибке (2) в вопросе много кода, который на самом деле не связан к фактической причине. Это также причина, по которой я сначала спросил, не было ли что-то неясно в сообщении об ошибке. Если вы не знаете, что означает @Primary/@Qualifier, то сообщение об ошибке не совсем полезно, и, похоже, так оно и было.




Проблема в том, что вы используете довольно общий интерфейс для автоподключения, Runnable.
В связи с этим кажется, что существует два bean-компонента, которые соответствуют интерфейсу Runnable:
MessageListener, который вы создали сами.DefaultHeaderChannelRegistry, представленный как bean-компонент integrationHeaderChannelRegistry. Предположительно, это выставлено, потому что у вас есть интеграция Spring в вашем пути к классам.Проблема в том, что из-за этого контейнер Spring IoC не может понять, какой bean-компонент он должен внедрить, и предоставляет некоторые решения.
@PrimaryЭто можно использовать в сценариях, где один из этих компонентов будет использоваться в 99% сценариев. Пометив класс MessageListener как @Primary, он будет иметь приоритет при попытке внедрить его, например:
@Primary // Add this
@Component
public class MessageListener implements Runnable {
// ...
}
Этот сценарий полезен, когда вам нужна ссылка на все bean-компоненты Runnable. В вашем случае это, вероятно, не решение, но в некоторых ситуациях вы можете захотеть получить все компоненты определенного типа. Для этого вы можете сделать:
@Autowired
private List<Runnable> runnables; // Change the type to List<Runnable>
@Qualifier для идентификации bean-компонентаДругая возможность — использовать @Qualifier, чтобы указать точное имя bean-компонента, который вы хотите внедрить. В вашем случае вы можете выбрать messageListener или integrationHeaderChannelRegistry. Например:
@Autowired
@Qualifier("messageListener")
private Runnable mesageListener;
Это, вероятно, лучшее решение в вашем случае, которое предлагается.
Есть также несколько дополнительных решений, которые я хочу предоставить.
Если вы измените тип поля autowired на MessageListener, не будет путаницы, в какой bean-компонент следует ввести, поскольку существует только один bean-компонент типа MessageListener:
@Autowired
private MessageListener mesageListener;
Показанный вами код не имеет ничего общего с интеграцией Spring. Если ваша единственная цель — настроить клиент MQTT, вы можете отказаться от пакета spring-integration-mqtt и вместо этого использовать простую реализацию клиента MQTT, например Затмение Пахо.
Поскольку другой bean-компонент создавался автоматически, потому что вы добавили библиотеку spring-integration-mqtt, ее удаление прекратит создание bean-компонента, и это также решит проблему.
Спасибо @g00glen00b за подробный ответ, это было действительно полезно. Я упомянул последнее решение в своем предыдущем комментарии, т.е. проблема не возникает, если я добавляю прямую зависимость от клиентской библиотеки Eclipse Paho Java. Еще раз большое спасибо за ваш подробный ответ со всеми техническими подробностями.
У меня была эта проблема в моем коде в течение недели, и сегодня я нашел это справедливо, потому что я дал одно и то же имя двум методам:
@Bean(name = "sqlServer")
public DataSource sqlServerDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("spring.ds-sql.driverClassName"));
dataSource.setUrl(env.getProperty("spring.ds-sql.url"));
dataSource.setUsername(env.getProperty("spring.ds-sql.username"));
dataSource.setPassword(env.getProperty("spring.ds-sql.password"));
return dataSource;
}
@Bean(name = "sqlJdbc")
public JdbcTemplate sqlServerDataSource(@Qualifier("sqlServer") DataSource dsSqlServer) {
return new JdbcTemplate(dsSqlServer);
}
Надеюсь, это поможет людям, имеющим такой же случай.
Удачи
В сообщении об ошибке уже сказано, как вы можете ее решить, так что именно вы спрашиваете? Что-то непонятно?