[spring-rabbit]自動配置原理

Xianhuii發表於2021-10-17

1 一個簡單的示例

Spring Boot專案中使用spring-rabbit時,需要經過以下幾個步驟:

  1. 引入依賴。
  2. 配置基本連線資訊。
  3. 建立訊息釋出者,併傳送訊息。
  4. 建立訊息消費者,監聽訊息並處理。

我們以一個簡單的例子開始,展示這個基本過程。

1.1 引入依賴

如果是Maven專案,需要在pom.xml檔案中引入基本依賴如下:

<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
    <version>2.3.10</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.5.5</version>
</dependency>

其中:

  • spring-rabbit用於與RabbitMQ伺服器互動的工具包
  • spring-boot-autoconfigure用於自動配置RabbitMQ客戶端伺服器連線等基本資訊。

1.2 配置連線資訊

由於spring-boot-autoconfigure的自動配置功能,我們僅需要在application.yml檔案中配置連線資訊即可。以下是一個例子:

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /

其中:

  • host:伺服器地址。
  • port:伺服器埠。
  • username:使用者名稱。
  • password:密碼。
  • virtual-host:交換機/佇列所屬虛擬主機。

1.3 訊息釋出者&消費者

我們直接在Spring Boot主程式中簡單編寫一個釋出&接收訊息的示例:

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

    @Bean
    public Queue myQueue() {
        return new Queue("myQueue");
    }

    @RabbitListener(queues = "myQueue")
    public void listen(String in) {
        System.out.println(in);
    }

    @Bean
    public ApplicationRunner runner(AmqpTemplate template) {
        return args -> template.convertAndSend("myQueue", "Hello World!");
    }
}

我們在這段程式碼中做了如下工作:

  1. 宣告佇列myQueue
  2. 建立訊息消費者,監聽佇列myQueue
  3. 使用AmqpTemplate物件,向訊息佇列myQueue傳送訊息:Hello World!

1.4 啟動專案

如果我們在本地啟動了RabbitMQ伺服器,並且埠、使用者名稱和密碼都沒有問題。

那麼啟動專案,可以從控制檯得到如下輸出:

Hello World!

1.5 提出問題

不知道大家會不會有這些疑問:

  1. 為什麼在application.yml檔案中寫入這些字元就可以連線到RabbitMQ伺服器
  2. AmqpTemplate物件為什麼不用宣告就可以直接使用?

其實,這一切的功勞都歸因於我們引入了spring-boot-autoconfigure。它為我們做了以下基本工作:

  1. application.yml檔案中讀取基本配置資訊。
  2. 使用基本配置資訊為我們建立出AmqpTemplate等物件,存放到Spring容器中。

接下來,由我來給大家揭開spring-boot-autoconfigurespring-rabbit自動配置的面紗。

2 RabbitProperties

2.1 看看原始碼

spring-boot-autoconfigure依賴的org.springframework.boot.autoconfigure.amqp包下,有個RabbitProperties類。

它的作用是:從application.yml檔案中讀取到spring-rabbit相關配置資訊。

IDEA中,我們可以簡單使用以下方法進入到這個類。

方法一,從application.yml檔案進入:

  • application.yml檔案中,按住Ctrl鍵,滑鼠左鍵點選某個配置資訊。

方法二,搜尋:

  • 快速連續按兩下Shift鍵,跳出搜尋框進行搜尋。

RabbitProperties原始碼簡要如下:

package org.springframework.boot.autoconfigure.amqp;

@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitProperties {
   private static final int DEFAULT_PORT = 5672;
   private static final int DEFAULT_PORT_SECURE = 5671;
   private String host = "localhost";
   private Integer port;
   private String username = "guest";
   private String password = "guest";
   private final Ssl ssl = new Ssl();
   private String virtualHost;
   private String addresses;
   private AddressShuffleMode addressShuffleMode = AddressShuffleMode.NONE;
   @DurationUnit(ChronoUnit.SECONDS)
   private Duration requestedHeartbeat;
   private int requestedChannelMax = 2047;
   private boolean publisherReturns;
   private ConfirmType publisherConfirmType;
   private Duration connectionTimeout;
   private Duration channelRpcTimeout = Duration.ofMinutes(10);
   private final Cache cache = new Cache();
   private final Listener listener = new Listener();
   private final Template template = new Template();
   private List<Address> parsedAddresses;
}

2.2 功能講解

通過簡單閱讀RabbitProperties,我們可以發現兩點重要資訊:

  • 該類新增了@ConfigurationProperties(prefix = "spring.rabbitmq")註解。
  • 該類的部分成員變數名與application.yml中配置資訊名一致,例如hostportusernamepasswordvirtual-host

在此我們需要先簡單瞭解@ConfigurationProperties註解的功能:

  1. @ConfigurationProperties可以用來獲取外部配置資訊,預設是application.ymlSpring Boot配置檔案。
  2. 將該註解新增到類上,會通過setter(預設)或constructor方法的方式,將外部配置資訊賦值給對應成員變數。
  3. prefix可以指定配置檔案中的字首,用來將外部配置資訊與成員變數進行匹配。

回到RabbitProperties原始碼,我們應該很容易理解RabbitProperties的功能:

  1. application.yml檔案中讀取字首為spring.rabbitmq的配置資訊。
  2. setter方法將配置資訊賦值給對應的成員變數。

通過上述過程,完成了將配置資訊從檔案讀取到快取(RabbitProperties物件)的過程,以便於後續使用。

2.3 動手實戰

我們也可以編寫一個類似的MyRabbitProperties,用來從application.yml檔案中讀取RabbitMQ配置資訊。

2.3.1 MyRabbitProperties

程式碼如下:

@ConfigurationProperties(prefix = "spring.rabbitmq")
public class MyRabbitProperties {
    private String host;
    private Integer port;
    private String username;
    private String password;
    private String virtualHost;

    public void setHost(String host) {
        this.host = host;
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setVirtualHost(String virtualHost) {
        this.virtualHost = virtualHost;
    }

    @Override
    public String toString() {
        return "MyRabbitProperties{" +
                "host='" + host + '\'' +
                ", port=" + port +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", virtualHost='" + virtualHost + '\'' +
                '}';
    }
}

簡要說明:

  • 新增@ConfigurationProperties(prefix = "spring.rabbitmq"),用來從application.yml檔案中讀取字首為spring.rabbitmq的配置資訊。
  • 新增setter方法,用來為成員變數注入配置資訊。
  • 新增toString()方法,便於後續列印資訊。

2.3.2 application.yml

我們在application.yml檔案中寫入如下配置資訊:

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /

2.3.3 啟動類

我們簡單編寫如下啟動類:

@EnableConfigurationProperties(MyRabbitProperties.class)
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    @Bean
    public ApplicationRunner runner(MyRabbitProperties properties) {
        return args -> {
            System.out.println(properties);
        };
    }
}

簡要說明:

  • 新增SpringBootApplication註解,表明這是一個Spring Boot啟動類。
  • 新增@EnableConfigurationProperties(MyRabbitProperties.class)註解,可以將@ConfigurationProperties標註的類宣告為Bean。這樣Spring容器才能為MyRabbitProperties注入配置資訊。
  • 新增main()函式,用來啟動Spring Boot專案。
  • 宣告ApplicationRunnerBean,專案啟動後會執行其中的程式碼。

需要注意的是:聯合@EnableConfigurationProperties只是@ConfigurationProperties註解使用方式的一種。我們也可以直接在MyRabbitProperties類上標註@Configuration@Component等註解,直接宣告為Bean

2.3.4 啟動專案

啟動專案後,可以從控制檯中得到如下輸出,說明我們成功將配置資訊注入到MyRabbitProperteis物件中:

MyRabbitProperties{host='localhost', port=5672, username='guest', password='guest', virtualHost='/'}

3 RabbitAutoConfiguration

通過RabbitProperteis我們已經從application.yml檔案中獲取到了連線RabbitMQ伺服器的配置資訊,接下來我們繼續揭祕:

  • spring-boot-autoconfigure為我們預先建立了哪些Bean
  • 它是如何建立這些Bean的?

預先小結:這些功能都在RabbitAutoConfiguration中。

3.1 看看原始碼

spring-boot-autoconfigure依賴的org.springframework.boot.autoconfigure.amqp包下,有個RabbitAutoConfiguration類。

它的作用是,當類路徑中存在RabbitMQSpring AMQP客戶端類庫時,可能會為我們自動建立如下Bean

  • org.springframework.amqp.rabbit.connection.CachingConnectionFactory:建立客戶端與RabbitMQ伺服器連線的工廠。
  • org.springframework.amqp.core.AmqpAdmin:封裝了宣告交換機/訊息佇列/繫結等模板方法。
  • org.springframework.amqp.rabbit.core.RabbitTemplate:封裝了與RabbitMQ伺服器互動的模板方法,例如:傳送訊息和接收訊息等。
  • org.springframework.amqp.rabbit.core.RabbitMessagingTemplate:功能與RabbitTemplate相同,但底層使用org.springframework.messaging.Message作為訊息抽象,較少使用。

IDEA中,我們可以簡單使用以下方法進入到這個類:

  • 快速連續按兩下Shift鍵,跳出搜尋框進行搜尋。

其原始碼結構如下:

package org.springframework.boot.autoconfigure.amqp;

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {

    // 建立連線工廠Bean
   protected static class RabbitConnectionFactoryCreator {}

    // 建立RabbitTemplate和AmqpAdmin
   protected static class RabbitTemplateConfiguration {}

    // 建立RabbitMessagingTemplate
   protected static class MessagingTemplateConfiguration {}
}

3.2 功能講解

通過簡單閱讀原始碼,我們可以將其分成四個部分進行介紹:

  1. RabbitAutoConfiguration標註註解:自動配置主類。
  2. RabbitConnectionFactoryCreator內部類:建立連線工廠,預設為CachingConnectionFactory
  3. RabbitTemplateConfiguration內部類:建立RabbitTemplateAmqpAdmin
  4. MessagingTemplateConfiguration內部類:建立RabbitMessagingTemplate

接下來,我們分別來介紹它們的功能。

3.2.1 配置主類

RabbitAutoConfiguration配置主類標註如以下四個註解:

  • @Configuration(proxyBeanMethods = false)
  • @ConditionalOnClass({ RabbitTemplate.class, Channel.class })
  • @EnableConfigurationProperties(RabbitProperties.class)
  • @Import(RabbitAnnotationDrivenConfiguration.class)

1、@Configuration(proxyBeanMethods = false)

@ConfigurationRabbitAutoConfiguration標註成配置類,可以在其內部宣告Bean

proxyBeanMethods = false表示Spring容器不會動態代理內部用@Bean標註的方法,可以提高效能。

2、@ConditionalOnClass({ RabbitTemplate.class, Channel.class })

@ConditionalOnClass註解表示只有當類路徑中存在以下類時,才會將RabbitAutoConfiguration註冊成Bean

  • org.springframework.amqp.rabbit.core.RabbitTemplate
  • com.rabbitmq.client.Channel

也就是說,只有在類路徑中存在RabbitMQSpring AMQP客戶端類庫,Spring容器才會為我們對RabbitMQ進行自動配置。

4、@EnableConfigurationProperties(RabbitProperties.class)

@EnableConfigurationProperties@ConfigurationProperties註解聯用,可以將RabbitProperties註冊成Bean,從而將配置資訊從application.yml讀取到記憶體中。

5、@Import(RabbitAnnotationDrivenConfiguration.class)

@Import註解可以引入另外的配置類——RabbitAnnotationDrivenConfiguration:用於配置Spring AMQP註解驅動斷點

簡單來說,它為我們註冊瞭如下Bean

  • org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory:建立SimpleMessageListenerContainer的工廠。
  • DirectRabbitListenerContainerFactory:建立DirectMessageListenerContainer的工廠。

這兩個XxxListenerContainer主要用來監聽RabbitMQ伺服器傳送的訊息。

因此,RabbitAnnotationDrivenConfiguration配置類主要與監聽訊息有關。由於篇幅限制,這裡就不進行深入講解了。其原始碼結構如下:

package org.springframework.boot.autoconfigure.amqp;

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnableRabbit.class)
class RabbitAnnotationDrivenConfiguration {

   @Bean
   @ConditionalOnMissingBean
   SimpleRabbitListenerContainerFactoryConfigurer simpleRabbitListenerContainerFactoryConfigurer() {}

   @Bean(name = "rabbitListenerContainerFactory")
   @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory")
   @ConditionalOnProperty(prefix = "spring.rabbitmq.listener", name = "type", havingValue = "simple",
         matchIfMissing = true)
   SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(}

   @Bean
   @ConditionalOnMissingBean
   DirectRabbitListenerContainerFactoryConfigurer directRabbitListenerContainerFactoryConfigurer() {}

   @Bean(name = "rabbitListenerContainerFactory")
   @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory")
   @ConditionalOnProperty(prefix = "spring.rabbitmq.listener", name = "type", havingValue = "direct")
   DirectRabbitListenerContainerFactory directRabbitListenerContainerFactory(}

   @Configuration(proxyBeanMethods = false)
   @EnableRabbit
   @ConditionalOnMissingBean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
   static class EnableRabbitConfiguration {}

}

3.2.2 RabbitConnectionFactoryCreator內部類

RabbitConnectionFactoryCreator內部類的作用是:註冊CachingConnectionFactory作為連線工廠Bean

其頭部標註了以下兩個註解:

  • @Configuration(proxyBeanMethods = false):宣告為配置類。
  • @ConditionalOnMissingBean(ConnectionFactory.class):只有org.springframework.amqp.rabbit.connection.ConnectionFactory類存在時才會生效,即只有類路徑中新增了spring-rabbit依賴時才會生效。

其內部預設將CachingConnectionFactory註冊為連線工廠Bean,步驟如下:

  1. 例項化CachingConnectionFactory物件。
  2. 將配置檔案中的配置資訊注入到CachingConnectionFactory物件中。

其原始碼簡要如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ConnectionFactory.class)
protected static class RabbitConnectionFactoryCreator {

    // 註冊CachingConnectionFactory作為連線工廠Bean
   @Bean
   public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties,
         ResourceLoader resourceLoader, ObjectProvider<CredentialsProvider> credentialsProvider,
         ObjectProvider<CredentialsRefreshService> credentialsRefreshService,
         ObjectProvider<ConnectionNameStrategy> connectionNameStrategy,
         ObjectProvider<ConnectionFactoryCustomizer> connectionFactoryCustomizers) throws Exception {
       // 1、例項化CachingConnectionFactory物件
      com.rabbitmq.client.ConnectionFactory connectionFactory = getRabbitConnectionFactoryBean(properties,
            resourceLoader, credentialsProvider, credentialsRefreshService).getObject();
      connectionFactoryCustomizers.orderedStream()
            .forEach((customizer) -> customizer.customize(connectionFactory));
      CachingConnectionFactory factory = new CachingConnectionFactory(connectionFactory);
       // 2、將配置檔案中的配置資訊注入到CachingConnectionFactory物件中
      PropertyMapper map = PropertyMapper.get();
      map.from(properties::determineAddresses).to(factory::setAddresses);
       // 省略其他map.from().to()方法
      return factory;
   }

    // 例項化RabbitConnectionFactoryBean物件
   private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitProperties properties,
         ResourceLoader resourceLoader, ObjectProvider<CredentialsProvider> credentialsProvider,
         ObjectProvider<CredentialsRefreshService> credentialsRefreshService) {
       // 省略
      return factory;
   }

}

3.2.3 RabbitTemplateConfiguration內部類

RabbitTemplateConfiguration內部類的作用是:註冊RabbitTemplateAmqpAdmin作為互動模板Bean

其頭部標註了以下兩個註解:

  • @Configuration(proxyBeanMethods = false):宣告為配置類。
  • @Import(RabbitConnectionFactoryCreator.class):引入RabbitConnectionFactoryCreator配置類。

其內部預設註冊RabbitTemplateAmqpAdmin作為互動模板Bean,本質上就是例項化物件。原始碼簡要如下:

@Configuration(proxyBeanMethods = false)
@Import(RabbitConnectionFactoryCreator.class)
protected static class RabbitTemplateConfiguration {

   @Bean
   @ConditionalOnMissingBean
   public RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties,
         ObjectProvider<MessageConverter> messageConverter,
         ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers) {
      RabbitTemplateConfigurer configurer = new RabbitTemplateConfigurer();
      configurer.setMessageConverter(messageConverter.getIfUnique());
      configurer
         .setRetryTemplateCustomizers(retryTemplateCustomizers.orderedStream().collect(Collectors.toList()));
      configurer.setRabbitProperties(properties);
      return configurer;
   }

    // 註冊RabbitTemplate
   @Bean
   @ConditionalOnSingleCandidate(ConnectionFactory.class)
   @ConditionalOnMissingBean(RabbitOperations.class)
   public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) {
      RabbitTemplate template = new RabbitTemplate();
      configurer.configure(template, connectionFactory);
      return template;
   }

    // 註冊AmqpAdmin
   @Bean
   @ConditionalOnSingleCandidate(ConnectionFactory.class)
   @ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true)
   @ConditionalOnMissingBean
   public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
      return new RabbitAdmin(connectionFactory);
   }

}

3.2.4 MessagingTemplateConfiguration內部類

MessagingTemplateConfiguration內部類的作用是:註冊RabbitMessagingTemplate作為互動模板Bean

RabbitMessagingTemplateRabbitTemplate的功能沒有本質差別,它們的差別在於繼承結構不同:

  • RabbitMessagingTemplate:繼承自org.springframework.messaging.core.AbstractMessageSendingTemplate抽象類。
  • RabbitTemplate:繼承自org.springframework.amqp.rabbit.connection.RabbitAccessor抽象類。

專案中通常使用的是RabbitTemplate

MessagingTemplateConfiguration內部類的原始碼如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RabbitMessagingTemplate.class)
@ConditionalOnMissingBean(RabbitMessagingTemplate.class)
@Import(RabbitTemplateConfiguration.class)
protected static class MessagingTemplateConfiguration {

   @Bean
   @ConditionalOnSingleCandidate(RabbitTemplate.class)
   public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
      return new RabbitMessagingTemplate(rabbitTemplate);
   }
}

4 總結

通過以上的簡單介紹,想必大家對Spring Boot專案spring-rabbit的自動配置有了大概的瞭解。

Spring Boot對其他工具,如:spring-webspring-securityspring-datasourcespring-transactionspring-kafka以及spring.jackson等都採用類似的自動配置方式,大家可以採用本文類似的步驟閱讀相關原始碼。

本篇文章就到這裡了,希望大家身體健康,工作順利!

相關文章