Talk about Spring Cloud Bus

Keywords: Java Spring RabbitMQ network

Spring event driven model

Because the operating mechanism of Spring Cloud Bus is also Spring event-driven model, it is necessary to understand the relevant knowledge points first.

The diagram above is a schematic implementation of Spring event-driven model. The following is a supplement to the implementation details not mentioned in the diagram: The abstract class AbstractApplication Event Multicaster obtains the corresponding observers according to the event and event types by:

    protected Collection<ApplicationListener<?>> getApplicationListeners(
            ApplicationEvent event, ResolvableType eventType)  

In this method, the specific retrieval listener (the observer's method) is:

private Collection<ApplicationListener<?>> retrieveApplicationListeners(
            ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever)
            
            .....
        // Add programmatically registered listeners, including ones coming
        // from ApplicationListenerDetector (singleton beans and inner beans).
        for (ApplicationListener<?> listener : listeners) {
            if (supportsEvent(listener, eventType, sourceType)) {
                if (retriever != null) {
                    retriever.applicationListeners.add(listener);
                }
                allListeners.add(listener);
            }
        }
            .....

In this method, all corresponding (subscribed) listeners are traversed according to the event object of the incoming parameter. One important method is Boolean support Event, which is used to determine whether the listener is a subscriber:

    protected boolean supportsEvent(
            ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) {

        GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
                (GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
        return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
    }

The Generic Application Listener and Generic Application Listener Adapter classes are used to define or implement the supportsEventType method and the supportsSourceType method, through which to determine whether they are event listeners (observers, subscribers).

public interface GenericApplicationListener extends ApplicationListener<ApplicationEvent>, Ordered {

    /**
     * Determine whether this listener actually supports the given event type.
     * @param eventType the event type (never {@code null})
     */
    boolean supportsEventType(ResolvableType eventType);

    /**
     * Determine whether this listener actually supports the given source type.
     * <p>The default implementation always returns {@code true}.
     * @param sourceType the source type, or {@code null} if no source
     */
    default boolean supportsSourceType(@Nullable Class<?> sourceType) {
        return true;
    }

    /**
     * Determine this listener's order in a set of listeners for the same event.
     * <p>The default implementation returns {@link #LOWEST_PRECEDENCE}.
     */
    @Override
    default int getOrder() {
        return LOWEST_PRECEDENCE;
    }

}

The support SourceType method, which determines the source object of the publishing event, returns true by default, which means that if the interface method is not rewritten, whether the listener subscribing to the event is not judged by the source object of the event, but only filtered according to the event type. The specific implementation of this method can refer to the support SourceType method wrapped in the Generic Application Listener Adapter class.

public boolean supportsSourceType(@Nullable Class<?> sourceType) {
        return !(this.delegate instanceof SmartApplicationListener) ||
                ((SmartApplicationListener) this.delegate).supportsSourceType(sourceType);
    }

Events, Publications, Subscriptions of Spring Cloud Bus

Events of Spring Cloud Bus are inherited from the RemoteApplication Event class. The RemoteApplication Event class is inherited from the event abstract class ApplicationEvent of Spring event-driven model, which means that events, publications and subscriptions of Spring Cloud Bus are also Spring-based event-driven models, such as Spring Cloud Bus configuration refresh event RefreshRemoteApplication Event:

Similarly, subscription events are also standard Spring event-driven models, such as configuring refreshed listener source code that inherits the interface ApplicationListener <E extends Application Event> in Spring event-driven model:

public class RefreshListener
        implements ApplicationListener<RefreshRemoteApplicationEvent> {

    private static Log log = LogFactory.getLog(RefreshListener.class);

    private ContextRefresher contextRefresher;

    public RefreshListener(ContextRefresher contextRefresher) {
        this.contextRefresher = contextRefresher;
    }

    @Override
    public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
        Set<String> keys = this.contextRefresher.refresh();
        log.info("Received remote refresh request. Keys refreshed " + keys);
    }

}

RefreshListener objects are registered in Spring's BeanFactory in the BusRefreshAutoConfiguration class (refresh events cannot be handled using Spring's event-driven model without registering the listener class in Spring's BeanFactory).

    @Bean
    @ConditionalOnProperty(value = "spring.cloud.bus.refresh.enabled",matchIfMissing = true)
    @ConditionalOnBean(ContextRefresher.class)
    public RefreshListener refreshListener(ContextRefresher contextRefresher) {
        return new RefreshListener(contextRefresher);
    }

You can also use @EventListener to create listeners, such as the TraceListener class:

    @EventListener
    public void onAck(AckRemoteApplicationEvent event) {
        Map<String, Object> trace = getReceivedTrace(event);
        // FIXME boot 2 this.repository.add(trace);
    }

    @EventListener
    public void onSend(SentApplicationEvent event) {
        Map<String, Object> trace = getSentTrace(event);
        // FIXME boot 2 this.repository.add(trace);
    }

Publishing events also uses application context for event publishing, such as configuration refresh implementation code:

@Endpoint(id = "bus-refresh") // TODO: document new id
public class RefreshBusEndpoint extends AbstractBusEndpoint {

    public RefreshBusEndpoint(ApplicationEventPublisher context, String id) {
        super(context, id);
    }

    @WriteOperation
    public void busRefreshWithDestination(@Selector String destination) { // TODO:
                                                                            // document
                                                                            // destination
        publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), destination));
    }

    @WriteOperation
    public void busRefresh() {
        publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), null));
    }

}

Annotation @WriteOperation implements POST operation, @Endpoint combines management.endpoints.web.exposure.include=* configuration item to achieve an access point whose URL is: / actuator/bus-refresh.
The publication of events is implemented in the application context within the parent AbstractBusEndpoint:

public class AbstractBusEndpoint {

    private ApplicationEventPublisher context;

    private String appId;

    public AbstractBusEndpoint(ApplicationEventPublisher context, String appId) {
        this.context = context;
        this.appId = appId;
    }

    protected String getInstanceId() {
        return this.appId;
    }

    protected void publish(ApplicationEvent event) {
        this.context.publishEvent(event);
    }

}

The underlying communication implementation of Spring Cloud Bus (transparent to users)

The underlying communication foundation of Spring Cloud Bus is Spring Cloud Stream. Bus AutoConfiguration (sending and receiving event messages from other nodes on the network) is defined as the class that sends and receives bus events listeners. Because it inherits Application Event Publisher Aware, this class also has the function of publishing local events (can query Aware interface), and publishing network. The method of network event message is:

@EventListener(classes = RemoteApplicationEvent.class)
    public void acceptLocal(RemoteApplicationEvent event) {
        if (this.serviceMatcher.isFromSelf(event)
                && !(event instanceof AckRemoteApplicationEvent)) {
            this.cloudBusOutboundChannel.send(MessageBuilder.withPayload(event).build());
        }
    }

If you listen to RemoteApplication Event events, first check whether they are self-published and not ACK events, and if they are self-published non-ACK events, send this event message on the bus. Sending AckRemoteApplication Event (ACK Event) has been triggered when receiving event messages from other nodes, so you don't have to worry about sending ACK events here.

Receive event messages:

@StreamListener(SpringCloudBusClient.INPUT)
    public void acceptRemote(RemoteApplicationEvent event) {
        if (event instanceof AckRemoteApplicationEvent) {
            if (this.bus.getTrace().isEnabled() && !this.serviceMatcher.isFromSelf(event)
                    && this.applicationEventPublisher != null) {
                this.applicationEventPublisher.publishEvent(event);
            }
            // If it's an ACK we are finished processing at this point
            return;
        }
        if (this.serviceMatcher.isForSelf(event)
                && this.applicationEventPublisher != null) {
            if (!this.serviceMatcher.isFromSelf(event)) {
                this.applicationEventPublisher.publishEvent(event);
            }
            if (this.bus.getAck().isEnabled()) {
                AckRemoteApplicationEvent ack = new AckRemoteApplicationEvent(this,
                        this.serviceMatcher.getServiceId(),
                        this.bus.getAck().getDestinationService(),
                        event.getDestinationService(), event.getId(), event.getClass());
                this.cloudBusOutboundChannel
                        .send(MessageBuilder.withPayload(ack).build());
                this.applicationEventPublisher.publishEvent(ack);
            }
        }
        if (this.bus.getTrace().isEnabled() && this.applicationEventPublisher != null) {
            // We are set to register sent events so publish it for local consumption,
            // irrespective of the origin
            this.applicationEventPublisher.publishEvent(new SentApplicationEvent(this,
                    event.getOriginService(), event.getDestinationService(),
                    event.getId(), event.getClass()));
        }
    }

Upon receiving event messages from other nodes, the event will be published to the local application context (this. application Event Publisher), and the subscribers listening for this event type will process it accordingly.

Two trace events, AckRemoteApplication Event and Set Application Event

From their inheritance relationship, we can see that AckRemoteApplication Event can be sent to other network nodes (inherited from RemoteApplication Event), SentApplication Event is only a local event (inherited from Application Event), SentApplication Event event can show the type of received event message, AckRemoteApplication Event event only shows the ID of received event message, TraceL The istener class is responsible for listening and recording their content (the configuration item opens spring.cloud.bus.trace.enabled=true):

public class TraceListener {

    @EventListener
    public void onAck(AckRemoteApplicationEvent event) {
        Map<String, Object> trace = getReceivedTrace(event);
        // FIXME boot 2 this.repository.add(trace);
    }

    @EventListener
    public void onSend(SentApplicationEvent event) {
        Map<String, Object> trace = getSentTrace(event);
        // FIXME boot 2 this.repository.add(trace);
    }

    protected Map<String, Object> getSentTrace(SentApplicationEvent event) {
        .....
    }

    protected Map<String, Object> getReceivedTrace(AckRemoteApplicationEvent event) {
        .....
    }

}

The process of recording logs at bus event sender and bus event receiver is as follows:

Test application A and application B for "chat"

First prepare the environment:
Create three projects: spring-cloud-bus-shared-library, spring-cloud-bus-a, spring-cloud-bus-b

  • spring-cloud-bus-shared-library: responsible for defining events and listeners and configuration classes
  • spring-cloud-bus-a: Acting as an A application is responsible for referencing shared-library and sending a message to B application using BUS (this message is actually a broadcast message)
  • spring-cloud-bus-b: Acting as a B application is responsible for referencing shared-library and using BUS to reply to messages sent by A application (this message is not broadcast)

The POM dependencies of spring-cloud-bus-shared-library:

<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>

Delete the built Maven plug-in node or other projects will not be referenced after the build (in incorrect format):

<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Start a rabbitmq:

docker pull rabbitmq:3-management

docker run -d --hostname my-rabbit --name rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management

application.properties configuration definition:

spring.application.name=spring-cloud-bus-shared-library
server.port=9007
# Open message tracking
spring.cloud.bus.trace.enabled=true
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

#Exposed Access Points Displayed
management.endpoints.web.exposure.include=*

The configuration information of spring-cloud-bus-a and spring-cloud-bus-b is the same except that spring.application.name and server.port are different.

Customize a chat event class:

/**
 * Chat Events
 */
public class ChatRemoteApplicationEvent extends RemoteApplicationEvent {

    private String message;

    //for serializers
    private ChatRemoteApplicationEvent(){}

    public ChatRemoteApplicationEvent(Object source, String originService,
            String destinationService,String message){
        super(source, originService, destinationService);

        this.message = message;
    }

    public void setMessage(String message){
        this.message = message;
    }

    public String getMessage(){
        return this.message;
    }
}

Custom chat event listener:

/**
 * Chat Event Monitor
 */
public class ChatListener implements ApplicationListener<ChatRemoteApplicationEvent> {

    private static Log log = LogFactory.getLog(ChatListener.class);

    public ChatListener(){}

    @Override
    public void onApplicationEvent(ChatRemoteApplicationEvent event){
        log.info(String.format("application%s Application%s Quietly say:\"%s\"",
                event.getOriginService(),
                event.getDestinationService(),
                event.getMessage()));
    }
}

The configuration class registers the listener in BeanFactory and needs to show Spring Cloud Bus that we have a custom event: @RemoteApplication EventScan (base Package Classes = ChatRemoteApplication Event. class), otherwise BUS cannot recognize the event type after receiving the message.

@Configuration
@ConditionalOnClass(ChatListener.class)
@RemoteApplicationEventScan(basePackageClasses=ChatRemoteApplicationEvent.class)
public class BusChatConfiguration {

    @Bean
    public ChatListener ChatListener(){
        return new ChatListener();
    }
}

Published to local Maven warehouse:

mvn install

POM dependencies of spring-cloud-bus-a and spring-cloud-bus-b:

<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-bus</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.bluersw</groupId>
            <artifactId>spring-cloud-bus-shared-library</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Add the @ComponentScan (value = com. bluersw) annotation to the spring-cloud-bus-a and spring-cloud-bus-b startup Main functions, otherwise the configuration class that references the spring-cloud-bus-shared-library project will not be scanned (and custom event and listener types will not be loaded).

spring-cloud-bus-a:

@SpringBootApplication
@ComponentScan(value = "com.bluersw")
public class SpringCloudBusAApplication {

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

}

spring-cloud-bus-b:

@SpringBootApplication
@ComponentScan(value = "com.bluersw")
public class SpringCloudBusBApplication {

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

}

Spring-cloud-bus-a sends messages to spring-cloud-bus-b (start spring-cloud-bus-a program and spring-cloud-bus-b program):

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringCloudBusAApplicationTests {

    @Autowired
    private ApplicationEventPublisher context;

    @Autowired
    private BusProperties bp;

    @Test
    public void AChat() {
        context.publishEvent(new ChatRemoteApplicationEvent(this,bp.getId(),null,"hi!B Application, I am A Application."));
    }

}

After executing AChat(), the console of spring-cloud-bus-b will output:
” Applying spring-cloud-bus-a_33b6374cba32e6a3e7e2c8e7631de8c0 whispers to the application ** "hi!B application, I am A application." Explain that spring-cloud-bus-b received the message and parsed and executed the event handler correctly, but the message was distributed in groups, because the destination service parameter we gave a null, which can be received by all projects that reference the spring-cloud-bus-shared-library project registration listener.

spring-cloud-bus-b replies to the message to spring-cloud-bus-a:

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringCloudBusBApplicationTests {

    @Autowired
    private ApplicationEventPublisher context;

    @Autowired
    private BusProperties bp;

    @Test
    public void BChat() {
        context.publishEvent(new ChatRemoteApplicationEvent(this,bp.getId(),"spring-cloud-bus-a:9008","hi!I am B application,Only in this way can it not be received by other applications."));
    }
}

Spring-cloud-bus-a is the name of the project and 9008 is the port number of the spring-cloud-bus-a project. When the destination service parameter is specified, other applications will not receive this message. After executing BChat(), the spring-cloud-bus-a console displays:
"Apply spring-cloud-bus-b_d577ac1ab28f0fc465a1e4700e7f538a to spring-cloud-bus-a:9008:** whisper:" hi! I am a B application, so that I can not be received by other applications."
This message is now received only by the spring-cloud-bus-a project.

Source code

Github warehouse: https://github.com/sunweisheng/spring-cloud-example

Posted by JakeTheSnake3.0 on Wed, 09 Oct 2019 11:09:12 -0700