A catalyst for change

Be a Catalyst for Change

You can’t force people to change. Instead, show them what the future might be like and help them participate in creating it.

— Tip 5 “How to Become a Programmer — From a Worker to an Expert”

Writing in the front

Some time ago, I found that Spring Event is super easy to use, so I have gradually added the function of Spring Event into the project.

Development environment:

Java 1.8 Spring Boot 2.1.6.RELEASE Spring 5.1.8.release

The basic development

Events are a Spring concept, not all of Spring Events. Spring events can be introduced simply by adding the Spring-context dependency.

To use Event, prepare three parts:

  • Event class: Defines events and inheritsApplicationEventClass becomes an event class.
  • Publisher: Publishes the eventApplicationEventPublisherPublish events.
  • Listener: Listens for and handles events, implementationApplicationListenerInterface or use@EventListenerAnnotation.

Event classes

As long as inherit org. Springframework. Context. ApplicationEvent, is a Spring Event class. Typically, we write an abstract Event class for an Event of that type as a parent class for all events of that type.

/** * Account related events */
public abstract class AccountEvent extends ApplicationEvent {

	/** * Information carried by this type of event */
	private AccountEventData eventData;

	/ * * * *@paramSource Specifies the object that originally triggered the event@paramEventData Information carried by an event of this type */
	public AccountEvent(Object source, AccountEventData eventData) {
		super(source);
		this.eventData = eventData;
	}

	public AccountEventData getEventData(a) {
		returneventData; }}Copy the code

Then define specific publishing events. Instead of using a private String eventType in an event to define the eventType, it is recommended to use a class implementation to publish specific events. With a specific class representing a specific event, the listener can simply listen to the specific event class without making any judgments, and there is no need to maintain a separate list of event types.

public class AccountCreatedEvent extends AccountEvent {
    public AccountCreatedEvent(Object source, AccountEventData eventData) {
        super(source, eventData); }}Copy the code

Another practice is to use generics to define a unified parent class.

public abstract class BaseEvent<T> extends ApplicationEvent {

	/** * Information carried by this type of event */
	private T eventData;

	/ * * * *@paramSource Specifies the object that originally triggered the event@paramEventData Information carried by an event of this type */
	public BaseEvent(Object source, T eventData) {
		super(source);
		this.eventData = eventData;
	}

	public T getEventData(a) {
		returneventData; }}Copy the code

You can then specify the generic type of the event.

public class AccountCreatedEvent extends BaseEvent<AccountEventData> {
    public AccountCreatedEvent(Object source, AccountEventData eventData) {
        super(source, eventData); }}public class TodoCreatedEvent extends BaseEvent<TodoEventData> {
    public TodoCreatedEvent(Object source, TodoEventData eventData) {
        super(source, eventData); }}Copy the code

AccountEventData is used for expansion purposes. If you need to add new fields to the event, you can add them directly to this class without modifying all the child event classes.

The publisher

Publishers are responsible for publishing messages, and there are three ways to do this. In the Spring container is the default ApplicationEventPublisher AbstractApplicationContext, at the same time AbstractApplicationContext ApplicationContext a subclass, That is to say, the Spring default AbstractApplicationContext publishing events.

Method 1: Use it directlyApplicationEventPublisher(recommended)

import org.springframework.context.ApplicationEventPublisher;

public class AccountsController {

	@PostMapping("")
	public Account createAccount(@RequestBody Account account) {... publisher.publishEvent(new AccountCreatedEvent(this.new AccountEventData()));
		returnaccount; }}Copy the code

Mode 2: ImplementationApplicationEventPublisherAwareInterface (recommended)

public interface ApplicationEventPublisherAware extends Aware {

	void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher);
}
Copy the code

This way is injected into a ApplicationEventPublisher, next reoccupy ApplicationEventPublisher# publisheEvent publishing events (ApplicationEvent) method.

package org.springframework.data.rest.webmvc;

@RepositoryRestController
class RepositoryEntityController extends AbstractRepositoryRestController implements ApplicationEventPublisherAware {

	private ApplicationEventPublisher publisher;
    
    @Override
	public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
		this.publisher = publisher;
	}

    private ResponseEntity<ResourceSupport> saveAndReturn(Object domainObject, RepositoryInvoker invoker,
    			HttpMethod httpMethod, PersistentEntityResourceAssembler assembler, boolean returnBody) {

        publisher.publishEvent(new BeforeSaveEvent(domainObject));
        Object obj = invoker.invokeSave(domainObject);
        publisher.publishEvent(newAfterSaveEvent(obj)); . }... }Copy the code

If you want your Service class can publish event, ApplicationEventPublisherAware can implement this interface.

Mode 3: UseApplicationContext#publishEvent(ApplicationEvent)Release.

public class AccountEventPublisher {

    private final ApplicationContext applicationContext;

    public AccountEventPublisher(ApplicationContext context) {
        this.applicationContext = context;
    }

    public void publish(TodoEvent ev) { applicationContext.publishEvent(ev); }}Copy the code

ApplicationContext ApplicationEventPublisher is an implementation, in front of the two kinds of schemes, actually there is no need for this repeat packaging scheme. Of course, you can also use ApplicationContext directly.

The listener

Listeners are responsible for receiving and handling events.

Basic usage

There are two ways to do this: implement the ApplicationListener interface or use the @EventListener annotation.

implementationApplicationListenerInterface:

public class TodoFinishedListener implements ApplicationListener<TodoEvent.TodoFinishedEvent> {    @Override    public void onApplicationEvent(TodoEvent.TodoFinishedEvent event) {        // do something }}
Copy the code

use@EventListenerNotes (recommended)

@Slf4j@Componentpublic class SyncAccountListener {	/** * Asynchronously send mail *@param event	 */	@EventListener	public void doOnNormalEvent(NormalAccountEvent event) {		try {			log.debug("befor");			Thread.sleep(1000);			log.debug("after");		} catch(InterruptedException e) { log.error(e.getMessage(), e); }}}Copy the code

You can use @EventListener to listen for multiple different events in the same class with different methods. Using @EventListener is more flexible than implementing the ApplicationListener interface.

@EventListenerExplanatory note

@EventListener(value = {AccountCreatedEvent.class, AccountUpdatedEvent.class}, condition = "#event.account.age > 10")public void doSomethingOnAccountEvent(AccountEvent event) {	// TODO}
Copy the code
  • value: A listening event (group) that supports events of the same parent class
  • classWith the value:
  • condition: SpEL, which makes Event Handlers conditional
    • #root.event, Application reference
    • #root.args, represents the method parameter, and #root.args[0] represents the 0th method parameter
    • #<name>, as in the code above, #event associates parameters with parameter names

ApplicationEventMulticaster event broadcast

Event broadcast device responsible for events that will be released ApplicationEventPublisher broadcast to all listeners. If there is no provide event broadcast, Spring will automatically use SimpleApplicationEventMulticaster radio device as the default event.

Building the Event Base

AbstractApplicationContext. Java the refresh () method to construct the complete event. AbstractApplicationContext# initApplicationEventMulticaster () to initialize the event broadcast, AbstractApplicationContext# registerListeners () is responsible for adding event listeners in the Spring container.

/**
 * Initialize the ApplicationEventMulticaster.
 * Uses SimpleApplicationEventMulticaster if none defined in the context.
 * @see org.springframework.context.event.SimpleApplicationEventMulticaster
 */
protected void initApplicationEventMulticaster(a) {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        this.applicationEventMulticaster =
                beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
        if (logger.isTraceEnabled()) {
            logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]"); }}else {
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
        if (logger.isTraceEnabled()) {
            logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
                    "[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]"); }}}Copy the code
/** * Add beans that implement ApplicationListener as listeners. * Doesn't affect other listeners, which can be added without being beans. */
protected void registerListeners(a) {
    // Register statically specified listeners first.
    for(ApplicationListener<? > listener : getApplicationListeners()) { getApplicationEventMulticaster().addApplicationListener(listener); }// Do not initialize FactoryBeans here: We need to leave all regular beans
    // uninitialized to let post-processors apply to them!
    String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true.false);
    for (String listenerBeanName : listenerBeanNames) {
        getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    }

    // Publish early application events now that we finally have a multicaster...
    Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
    this.earlyApplicationEvents = null;
    if(earlyEventsToProcess ! =null) {
        for(ApplicationEvent earlyEvent : earlyEventsToProcess) { getApplicationEventMulticaster().multicastEvent(earlyEvent); }}}Copy the code

Event publishing

We can in AbstractApplicationContext. Found in Java event publishing method directly. AbstractApplicationContext# publish (Object, ResolvableType), the event is published through broadcasting ApplicationEventMulticaster.

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
    Assert.notNull(event, "Event must not be null");

    // Decorate event as an ApplicationEvent if necessary
    ApplicationEvent applicationEvent;
    if (event instanceof ApplicationEvent) {
        applicationEvent = (ApplicationEvent) event;
    }
    else {
        applicationEvent = new PayloadApplicationEvent<>(this, event);
        if (eventType == null) {
            eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
        }
    }

    // Multicast right now if possible - or lazily once the multicaster is initialized
    if (this.earlyApplicationEvents ! =null) {
        this.earlyApplicationEvents.add(applicationEvent);
    }
    else {
        getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
    }

    // Publish event via parent context as well...
    if (this.parent ! =null) {
        if (this.parent instanceof AbstractApplicationContext) {
            ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
        }
        else {
            this.parent.publishEvent(event); }}}Copy the code

We can know, in front is SimpleApplicationEventMulticaster ApplicationEventMulticaster default implementation in the Spring container. Of course you can find the real way events are published. The multicastEvent method finds all listeners listening for the current event and then executes the execution listener method.

Has two attributes in the SimpleApplicationEventMulticaster, Executor taskExecutor and ErrorHandler ErrorHandler. The former defines whether all listeners are executed asynchronously. The default is null, which is equivalent to SyncTaskExecutor. You can also use SimpleAsyncTaskExecutor to set all listeners to execute asynchronously. But it’s important to note that if you make your Executor asynchronous, then all listeners will execute asynchronously, and listeners and calling classes will be in different contexts, different transactions, unless you have a way for TaskExecutor to support it. Instead of modifying taskExecutor to make listeners asynchronous, we can start Async with @enableAsync and set listeners to execute asynchronously with @async. With @async, you can decide whether any listener is asynchronous or not, rather than nonviolently asynchronizing all listeners.

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { ResolvableType type = (eventType ! =null ? eventType : resolveDefaultEventType(event));    Executor executor = getTaskExecutor();    for(ApplicationListener<? > listener : getApplicationListeners(event, type)) {if(executor ! =null) {            executor.execute(() -> invokeListener(listener, event));        }        else{ invokeListener(listener, event); }}}Copy the code

ErrorHandler The ErrorHandler defines the behavior of a listener when an exception occurs. As you can see here, if an ErrorHandler is not defined, it will be thrown to the next level.

protected void invokeListener(ApplicationListener
        listener, ApplicationEvent event) {    ErrorHandler errorHandler = getErrorHandler();    if(errorHandler ! =null) {        try {            doInvokeListener(listener, event);        }        catch(Throwable err) { errorHandler.handleError(err); }}else {        doInvokeListener(listener, event);    }}private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {    try {        listener.onApplicationEvent(event);    }    catch (ClassCastException ex) {        String msg = ex.getMessage();        if (msg == null || matchesClassCastMessage(msg, event.getClass())) {            // Possibly a lambda-defined listener which we could not resolve the generic event type for // -> let's suppress the exception and just log a debug message. Log logger = LogFactory.getLog(getClass()); if (logger.isTraceEnabled()) { logger.trace("Non-matching event type for listener: " + listener, ex); } } else { throw ex; }}}
Copy the code

You can modify the default ApplicationEventMulticaster, or inheritance/realize AbstractApplicationEventMulticaster ApplicationEventMulticaster directly.

// Who made all my listeners program asynchronously? - Someone changed the taskExecutor of the event broadcaster to be asynchronous. public class AsyncConfig { @Bean public ApplicationEventMulticaster simpleApplicationEventMulticaster() { SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor()); return eventMulticaster; }}
Copy the code
  • @EnableAspectJAutoProxy(proxyTargetClass = true)role

synchronous

The Listener is synchronized by default

public Account createAccount(Account account) {...try {        publisher.publishEvent(new ThrowExceptionAccountEvent(this.new AccountEventData()));    } catch (Exception e) {        Throw new ServiceException(LLDB etMessage(), e); }... return account; }
Copy the code

In the above method, publishEvent returns after all synchronization listeners have been executed. Since it is synchronous, you can specify the Order of execution via the @order annotation. Using synchronous listeners, you can allow events to participate in the publisher’s transaction. Can know from the face in the interpretation of ApplicationEventMulticaster, synchronous execution is a simple method call.

Exceptions and transactions

As mentioned above, the synchronized listener is thrown upwards if an exception occurs and is not intercepted by ErrorHandler and can be caught directly on the publishEvent method call.

In synchronous scenarios, the execution of the listener is essentially a call to a normal method. Transaction control is the same as normal method calls. If you want the listener to be in a transaction, you can simply add the transaction annotation @transational to the listener method. See Spring’s transaction propagation behavior for a detailed analysis.

asynchronous

Since the execution of a listener is actually the execution of a normal method, a method that declares a listener asynchronous will use @async just like a method that declares a normal method asynchronous.

To be clear, when a listener is set to asynchronous, this causes the listener method to be in a different transaction from the publishEvent method. In fact, there is not much difference with ordinary asynchronous.

Implement Async with @async

Start the asynchronous

@EnableAsync@Configurationpublic class AsyncConfig implements AsyncConfigurer {    @Override    public Executor getAsyncExecutor(a) {    	ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();    	executor.setCorePoolSize(10);    	executor.setMaxPoolSize(20);    	executor.setQueueCapacity(1000);    	executor.setKeepAliveSeconds(300);    	executor.setThreadNamePrefix("dspk-Executor-");    	/ / rejection policies executor. SetRejectedExecutionHandler (new ThreadPoolExecutor. CallerRunsPolicy ()); executor.initialize(); return executor; } / exception handler * / * * * @ Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler () {return new SimpleAsyncUncaughtExceptionHandler(); }}
Copy the code

The default AsyncConfigurer is AsyncConfigurerSupport, and both methods return NULL.

Set a listener to asynchronous

@Componentpublic class AccountListener {    @Async    @EventListener    public void sendEmailOnAccountCreatedEvent(AccountCreateEvent event) {        // do something else}}
Copy the code

Using asynchronous ApplicationEventMulticaster implementation

To specify a asynchronous ApplicationEventMulticaster taskExecutor, will make all the listeners are executed asynchronously. I really don’t recommend this.

public class AsyncConfig {    @Bean    public ApplicationEventMulticaster simpleApplicationEventMulticaster(a) {        SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());        return eventMulticaster;    }}
Copy the code

@ TransactionalEventListener affair management

define

Using the @ TransactionalEventListener is @ EventListener to expand, you can specify the listener and publish event method of transaction isolation level. Isolation levels ensure the validity of data. @ TransactionalEventListener annotation listener will be declared as a transaction manager (this part of the organic meet in other articles).

When a listener method is @ TransactionalEventListener annotation, the listener will only in the caller for the transaction method, if the caller is a transaction method, cannot the listener will not be notified. It is important to note that although @ TransactionalEventListener keyword with the Transaction, but this method did not declare the listener for Transactional.

@TransactionalEventListenerpublic void afterRegisterSendMail(MessageEvent event) { mailService.send(event); }Copy the code

Configuration @ TransactionalEventListener

In addition to having the same attributes as @EventListener (classes, condition), this annotation provides two other attributes, fallbackExecution and Phase.

@ TransactionalEventListener annotation attributes: fallbackExecution

Definition: fallbackExecution sets the Listener to process an event without a transaction.

The default is false, which means that the publishEvent listener does not listen for events when the publishEvent method has no transaction control. By setting fallbackExecution=true, the Listener can be executed in any case.

@Transactional
public void txMethod(a) {
    publisher.publishEvent(new MessageEvent());
}

public void nonTxMethod(a) {
    publisher.publishEvent(new MessageEvent());
}

/ / txMethod: execution
// nonTxMethod: no execution
@TransactionalEventListener
public void afterRegisterSendMail(MessageEvent event) {
    mailService.send(event);
}

/ / txMethod: execution
/ / nonTxMethod: execution
@TransactionalEventListener(fallbackExecution = true)
public void afterRegisterSendMail(MessageEvent event) {
    mailService.send(event);
}
Copy the code

@ TransactionalEventListener annotation attributes: phase

  • AFTER_COMMIT– (default) Triggers the event after the transaction completes. At this point the transaction has ended and the listener method cannot find the previous transaction
  • AFTER_ROLLBACK– Raises the event after the transaction rollback
  • AFTER_COMPLETION— Events are raised after the transaction completes (containsAFTER_COMMITandAFTER_ROLLBACK)
  • BEFORE_COMMIT– Raises the event before the transaction commits, while the transaction that called the square method still exists and can be found by the listener method

When you try to execute JPA’s save method in the @transactionEventListener method, you get the following error:

@EventListener
@TransactionalEventListener
public void doOnNormalEvent3(NormalAccountEvent event) {
    Account account = new Account();
    account.setPassword("33333");
    account.setFirstName("333");
    account.setLastName("333");
    account.setEmail("[email protected]");

    accountRepository.save(account);
}
Copy the code
Participating transaction failed - marking existing transaction as rollback-only
Setting JPA transaction on EntityManager [SessionImpl(1991443937<open>)] rollback-only
...
org.springframework.dao.InvalidDataAccessApiUsageException: no transaction is in progress; nested exception is javax.persistence.TransactionRequiredException: no transaction is in progress
Copy the code

Reason is @ TransactionalEventListener default is AFTER_COMMIT, that is, the current transaction is over, so I can’t find the transaction, can only perform a rollback, and therefore cannot succeed to save data in the database. But if you want to execute the findAll() method, you get the data that the caller committed to the database, but not the data saved in that Listener. You may be thinking, shouldn’t I just @Transactional on this method? So far, the test results are negative. The specific reasons will be explained in the article about writing transactions, which will not be expanded here. This can be resolved by setting phase to transactionPhase.before_commit so that the caller’s transaction has not yet finished.

@Transactional
@TransactionalEventListener(fallbackExecution = true, phase = TransactionPhase.BEFORE_COMMIT)
public void doOnNormalEvent3(NormalAccountEvent event) {... }Copy the code

This does not mean AFTER_COMMIT should not be used, but should be used in appropriate situations. For example, you need to send emails. In the following method, method B triggers the sending of the message, but because there are actions in method A that could cause method B to fall back, to prevent the message from being sent early (the action cannot be undone), you can have the sending of the message execute after the transaction commits.

methodA() {
  methodB();
  // do something
}

methodB() {
	publisher.publishEvent(new EventB());
}

@TransactionalEventListener
sendEmailOnEventB() {
  // send Email
}
Copy the code

reference

  • Using asynchronous events in Spring to implement the race conditions that synchronous transaction asynchrony can introduce
  • Spring Events @www.baeldung.com
  • Spring event architecture @blog.csdn.net
  • Spring transaction event monitoring
  • SpringBoot series – Asynchronous thread pools