07 Spring Events Common Errors

07 Spring Events Common Errors #

Hello, I’m Fu Jian. In this lesson, we will talk about common errors in Spring events.

In the previous few lessons, we introduced common errors in core features of Spring, such as dependency injection and AOP. As a key feature of Spring, events stand out as a relatively independent aspect. You may have never used Spring events in your own projects, but you have probably come across related log messages. Furthermore, in your future programming practice, you will discover that once you start using Spring events, you often accomplish interesting and powerful functionalities, such as dynamic configuration. Now, let’s move on to discuss the common errors in Spring events.

Case 1: Handling events that are not thrown #

The design of Spring events is relatively simple. Simply put, it is an implementation of the listener design pattern in Spring, as shown in the following figure:

From the diagram, we can see that Spring events consist of the following three components:

  1. Event: Used to distinguish and define different events. In Spring, common examples include ApplicationEvent and AutoConfigurationImportEvent, which both inherit from java.util.EventObject.
  2. Event multicaster: Responsible for publishing the defined events. For example, the ApplicationEventMulticaster is a commonly used multicaster that publishes ApplicationEvents in Spring.
  3. Event listener: Responsible for listening to and processing events that are broadcast by the multicaster. For example, the ApplicationListener is used to handle ApplicationEvents published by the ApplicationEventMulticaster. It inherits from JDK’s EventListener. We can check its definition to verify this conclusion:
public interface ApplicationListener extends EventListener {
    void onApplicationEvent(E event);
}

Of course, although each of these components is indispensable, the naming of the functional modules may not fully correspond to the keywords mentioned above. For example, the multicaster that publishes AutoConfigurationImportEvent does not include the word “Multicaster” in its name. Its publication is done by AutoConfigurationImportSelector.

After understanding these basic concepts and implementations to a certain extent, we can start analyzing common mistakes. Without further ado, let’s take a look at the following code based on the Spring Boot technology stack:

@Slf4j
@Component
public class MyContextStartedEventListener implements ApplicationListener<ContextStartedEvent> {
  
  public void onApplicationEvent(final ContextStartedEvent event) {
    log.info("{} received: {}", this.toString(), event);
  }
  
}

It is obvious that this code defines a listener, MyContextStartedEventListener, which attempts to intercept the ContextStartedEvent. Because in the eyes of many novice Spring developers, the core of Spring is the maintenance of a Context, so starting Spring naturally means starting the Context. Therefore, they expect to see log messages like the following:

2021-03-07 07:08:21.197 INFO 2624 — [nio-8080-exec-1] c.s.p.l.e.MyContextStartedEventListener : com.spring.puzzle.class7.example1.MyContextStartedEventListener@d33d5a received: org.springframework.context.event.ContextStartedEvent [source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@19b56c0, started on Sun Mar 07 07:07:57 CST 2021]

However, when we start Spring Boot, we find that this event is not intercepted. How should we understand this error?

Case Analysis #

This is a common mistake in the use of Spring events, which is to blindly assume that if a framework defines an event, it will definitely be thrown. For example, in this case, ContextStartedEvent is a built-in event defined by Spring, and Spring Boot itself creates and maintains the Context, so it seems that the throwing of this event is inevitable. But will this event be thrown when Spring Boot starts up?

The answer is obviously no. Let’s first see what method needs to be called to throw this event. In Spring Boot, the throwing of this event only occurs in one place, which is in the method AbstractApplicationContext#start.

@Override
public void start() {
   getLifecycleProcessor().start();
   publishEvent(new ContextStartedEvent(this));
}

In other words, the ContextStartedEvent will only be thrown when the above method is called. But will this method be called when Spring Boot starts up? We can check the key method calls related to the Context and Spring Boot in the Spring startup method. The code is as follows:

public ConfigurableApplicationContext run(String... args) {
    // Omitted non-essential code
    context = createApplicationContext();
    // Omitted non-essential code
    prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    refreshContext(context);
    // Omitted non-essential code
    return context;
}

We found that there are only two key tasks related to the Context and Spring Boot startup: creating the Context and refreshing the Context. Among them, the key code for refreshing is as follows:

protected void refresh(ApplicationContext applicationContext) {
   Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
   ((AbstractApplicationContext) applicationContext).refresh();
}

It is obvious that the Spring startup ultimately calls AbstractApplicationContext#refresh, not AbstractApplicationContext#start. In the face of this cruel reality, the ContextStartedEvent will naturally not be thrown, and if it is not thrown, it cannot be captured. This is how such an error naturally occurs.

Problem Fix #

For this case, after analyzing the source code, we can quickly find the reason for the problem. However, to fix this problem, we need to trace back to what we actually want. We can consider two scenarios.

1. Suppose we misunderstood the ContextStartedEvent.

In this case, it is often because we really want to intercept a startup event when Spring Boot starts up, but after roughly scanning the relevant events, we mistakenly believe that ContextStartedEvent is what we want. In this case, we only need to modify the type of the listening event to the actual event that occurs. For example, in this case, we can fix it as follows:

@Component
public class MyContextRefreshedEventListener implements ApplicationListener<ContextRefreshedEvent> {

  public void onApplicationEvent(final ContextRefreshedEvent event) {
    log.info("{} received: {}", this.toString(), event);
  }

}

We listen to ContextRefreshedEvent instead of ContextStartedEvent. The throwing of ContextRefreshedEvent can be found in the method AbstractApplicationContext#finishRefresh, which is exactly one step of the Refresh operation.

protected void finishRefresh() {
   // Omit non-essential code
   initLifecycleProcessor();
   // Propagate refresh to lifecycle processor first.
   getLifecycleProcessor().onRefresh();
   // Publish the final event.
   publishEvent(new ContextRefreshedEvent(this));
   // Omit non-essential code
}

2. Suppose we want to handle the ContextStartedEvent.

In this case, we do need to call the AbstractApplicationContext#start method. For example, we can use the following code to trigger this event:

@RestController
public class HelloWorldController {

    @Autowired
    private AbstractApplicationContext applicationContext;

    @RequestMapping(path = "publishEvent", method = RequestMethod.GET)
    public String notifyEvent(){
        applicationContext.start();       
        return "ok";
    };
}

We can Autowire an AbstractApplicationContext instance anywhere and directly call its start() method to trigger the event.

Clearly, triggering the event is not difficult, but as an aside, why do we need to call start()? What does start() do in Spring Boot?

If we look up this method, we will find that start() is a method defined in org.springframework.context.Lifecycle, and in the default implementation of Spring Boot, it executes the start method of all Lifecycle Beans. This can be verified by looking at the startBeans method in DefaultLifecycleProcessor:

private void startBeans(boolean autoStartupOnly) {
   Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
   Map<Integer, LifecycleGroup> phases = new HashMap<>();
   lifecycleBeans.forEach((beanName, bean) -> {
      if (!autoStartupOnly || (bean instanceof SmartLifecycle && ((SmartLifecycle) bean).isAutoStartup())) {
         int phase = getPhase(bean);
         LifecycleGroup group = phases.get(phase);
         if (group == null) {
            group = new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly);
            phases.put(phase, group);
         }
         group.add(beanName, bean);
      }
   });
   if (!phases.isEmpty()) {
      List<Integer> keys = new ArrayList<>(phases.keySet());
      Collections.sort(keys);
      for (Integer key : keys) {
         phases.get(key).start();
      }
   }
}

This may sound abstract, so let’s write a Lifecycle Bean, like this:

@Component
@Slf4j
public class MyLifeCycle implements Lifecycle {

    private volatile boolean running = false;

    @Override
    public void start() {
       log.info("lifecycle start");
       running = true;
    }

    @Override
    public void stop() {
       log.info("lifecycle stop");
       running = false;
    }

    @Override
    public boolean isRunning() {
        return running;
    }

}

When we run Spring Boot again, as long as AbstractApplicationContext’s start() is executed, it will output the behavior defined in the above code: outputting the “lifecycle start” log.

Through the use of this Lifecycle Bean, we can understand what the start method of AbstractApplicationContext does. It is different from Refresh(). Refresh() initializes and loads all managed beans, while start is only valuable when there are Lifecycle Beans. So what do we usually use custom Lifecycle Beans for? For example, we can use them for runtime startup and shutdown. I won’t go into further detail here; you can explore it further on your own.

Through this example, we have addressed the first type of error. And from this error, we also come to the realization that when an event cannot be intercepted, the first thing to check is whether the intercepted event type is correct and whether the executing code can throw it. By grasping this point, we can achieve more with less effort.

Case 2: Incorrect Event Monitoring System #

Through the study of Case 1, we can ensure that an event is thrown. But can we always listen to the thrown event? Let’s take a look at the following case and start with the code:

@Slf4j
@Component
public class MyApplicationEnvironmentPreparedEventListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    public void onApplicationEvent(final ApplicationEnvironmentPreparedEvent event) {
        log.info("{} received: {}", this.toString(), event);
    }

}

Here we attempt to handle the ApplicationEnvironmentPreparedEvent. We expect the log to show the intercepted event as follows:

2021-03-07 09:12:08.886 INFO 27064 — [ restartedMain] MyApplicationEnvironmentPreparedEventListener: com.spring.puzzle.class7.example2.MyApplicationEnvironmentPreparedEventListener@2b093d received: org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent[source=org.springframework.boot.SpringApplication@122b9e6]

With the experience from Case 1, we can first check if there are any issues with the event being thrown. This event is thrown in Spring by the EventPublishingRunListener#environmentPrepared method, as shown below:

@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
   this.initialMulticaster
         .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}

Now let’s debug the code. You will find that this method is called when Spring starts up and is invoked through the SpringApplication#prepareEnvironment method. Here is a screenshot of the debug:

Debug Screenshot

Superficially, since the code is being called and the event is being thrown, our defined listener should be able to handle it. However, when we actually run the program, we find that the effect is the same as in Case 1 - the listener’s processing is not executed, i.e., the interception does not occur. Why is this happening?

Case Analysis #

In fact, this is a very common mistake in Spring event handling - inconsistent listening systems. In simple terms, it means “the donkey’s head doesn’t match the horse’s mouth.” First, let’s take a look at the components related to handling ApplicationEnvironmentPreparedEvent. What are they?

  1. Broadcaster: The broadcaster for this event is initialMulticaster in EventPublishingRunListener. The code references are as follows:
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
   // Omitted non-essential code
   private final SimpleApplicationEventMulticaster initialMulticaster;

   public EventPublishingRunListener(SpringApplication application, String[] args) {
      // Omitted non-essential code
      // Store listener in initialMulticaster
      this.initialMulticaster = new SimpleApplicationEventMulticaster();
      for (ApplicationListener<?> listener : application.getListeners()) {
         this.initialMulticaster.addApplicationListener(listener);
      }
   }
}
  1. Listener: The listener for this event is also within EventPublishingRunListener, and the way the listener is obtained is via the following code:
this.initialMulticaster.addApplicationListener(listener);

If we continue to look at the code, we will find that the listener for this event is stored in SpringApplication#Listeners. By debugging, we can find all the listeners, as shown in the screenshot below:

Debug Screenshot

From here, we can see that our defined MyApplicationEnvironmentPreparedEventListener does not exist. Why is this the case?

By looking at the code again, when Spring Boot is built, the following method is used to search for the aforementioned listeners:

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

Ultimately, the code searches for the candidates for listeners, referencing the crucial line in SpringFactoriesLoader#loadSpringFactories:

// The FACTORIES_RESOURCE_LOCATION is defined as "META-INF/spring.factories" - classLoader.getResources(FACTORIES_RESOURCE_LOCATION)

We can search for a file like this (spring.factories), and we can indeed find a similar definition:

org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
// Omitted other listeners

By now, I believe you have realized the problem in this case. Our defined listener is not placed in META-INF/spring.factories. In fact, the events our listener listens to belong to a different system with the following crucial components:

  1. Broadcaster: AbstractApplicationContext#applicationEventMulticaster.
  2. Listener: The listeners loaded from META-INF/spring.factories mentioned above and the ApplicationListener type beans scanned by the system, which form a common group.

After comparing the two, we can conclude that our defined listener cannot listen to the ApplicationEnvironmentPreparedEvent broadcasted by initialMulticaster.

Problem Resolution #

Now it’s time to solve the problem. We can register the custom listener into the initialMulticaster broadcasting system. Here are two methods to fix the issue:

  1. Add MyApplicationEnvironmentPreparedEventListener when building Spring Boot:
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        MyApplicationEnvironmentPreparedEventListener myApplicationEnvironmentPreparedEventListener = new MyApplicationEnvironmentPreparedEventListener();
        SpringApplication springApplication = new SpringApplicationBuilder(Application.class).listeners(myApplicationEnvironmentPreparedEventListener).build();
        springApplication.run(args);
    }
}
  1. Use META-INF/spring.factories. Create a META-INF directory under /src/main/resources and then create a corresponding spring.factories file:
org.springframework.context.ApplicationListener=\
com.spring.puzzle.listener.example2.MyApplicationEnvironmentPreparedEventListener

By using either of the above modification methods, we can successfully listen to the event. It is evident that the second method is better than the first, at least by completely using the native way to solve the problem, rather than manually instantiating a MyApplicationEnvironmentPreparedEventListener. This point is quite important.

In summary, the conclusion of this case’s mistake is: we must pay attention to “the donkey’s head” (the listener) matching “the horse’s mouth” (the broadcaster) for events.

Case 3: Partial Event Listeners Failure #

From the analysis of the previous cases, we can ensure that events are captured by the appropriate listeners at the appropriate time. However, ideals often differ from reality, and sometimes we may find that some event listeners are consistently or occasionally ineffective. Here we can write a piece of code to simulate the scenario of occasional failures. First, let’s create a custom event and two listeners:

public class MyEvent extends ApplicationEvent {
    public MyEvent(Object source) {
        super(source);
    }
}

@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener<MyEvent> {

    Random random = new Random();

    @Override
    public void onApplicationEvent(MyEvent event) {
        log.info("{} received: {}", this.toString(), event);
        // Simulate partial failure
        if (random.nextInt(10) % 2 == 1)
            throw new RuntimeException("exception happen on first listener");
    }
}

@Component
@Order(2)
public class MySecondEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        log.info("{} received: {}", this.toString(), event);
    }
}

In this code, MyFirstEventListener has a slightly higher priority and a 50% chance of throwing an exception during execution. Next, let’s write a controller to trigger the event:

@RestController
@Slf4j
public class HelloWorldController {

    @Autowired
    private AbstractApplicationContext applicationContext;

    @RequestMapping(path = "publishEvent", method = RequestMethod.GET)
    public String notifyEvent(){
        log.info("start to publish event");
        applicationContext.publishEvent(new MyEvent(UUID.randomUUID()));
        return "ok";
    };
}

Once you have completed this code, you can test the reception and execution of the listeners using http://localhost:8080/publishEvent. By observing the test results, you will find that MySecondEventListener has a 50% chance of not receiving any events. In other words, we have simulated the scenario of occasional failure of some event listeners with the most simplified code. Of course, in a real project, the root cause of an exception’s occurrence will not be so obvious, but we can still take this opportunity to take a hint. So how do we understand this problem?

Case Analysis #

This case is very simple, and if you have some development experience, you can probably infer the reason: the execution of listeners is sequential, and if one listener throws an exception during execution, the subsequent listeners will not have the opportunity to be executed. Let’s take a look at how events are executed through the Spring source code.

Specifically, when broadcasting an event, the method SimpleApplicationEventMulticaster#multicastEvent(ApplicationEvent) is called:

@Override
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);
      }
   }
}

In the above method, getApplicationListeners retrieves all listeners that are eligible for execution (in this case, MyFirstEventListener and MySecondEventListener) based on the event type and other information, and then executes them in order. Finally, the execution of each listener is triggered by calling the interface method ApplicationListener#onApplicationEvent. The logic of execution can be seen in the following code:

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) {
        // Omitted irrelevant code
      }
      else {
         throw ex;
      }
   }
}

We did not set any org.springframework.util.ErrorHandler or bind any Executor to execute tasks here. So, based on the specific scenario in this case, we can see that event execution is ultimately completed by the same thread in order, and any errors will prevent subsequent listeners from being executed.

Problem Fix #

How do we solve this? It’s easy, I’ll give you two solutions.

1. Ensure that listener execution does not throw exceptions.

Since we are using multiple listeners, we certainly want them all to be executed, so we must ensure that the execution of each listener is not affected by other listeners. Based on this idea, we modify the code in the case as follows:

@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        try {
          // Omitted event handling code
        } catch(Throwable throwable){
            // Write error/metric to alert
        }

    }
}

2. Use org.springframework.util.ErrorHandler.

From the analysis of the previous case, assuming we set an ErrorHandler, we can use this ErrorHandler to handle exceptions and ensure that subsequent event listeners are not affected. We can use the following code to fix the problem:

// Get the SimpleApplicationEventMulticaster bean
SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = applicationContext.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, SimpleApplicationEventMulticaster.class);
simpleApplicationEventMulticaster.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);

The implementation of LOG_AND_SUPPRESS_ERROR_HANDLER is as follows:

public static final ErrorHandler LOG_AND_SUPPRESS_ERROR_HANDLER = new LoggingErrorHandler();

private static class LoggingErrorHandler implements ErrorHandler {

   private final Log logger = LogFactory.getLog(LoggingErrorHandler.class);

   @Override
   public void handleError(Throwable t) {
      logger.error("Unexpected error occurred in scheduled task", t);
   }
}

Comparing the two solutions, using an ErrorHandler has a huge advantage: we don’t need to repeatedly write similar code in each listener like this:

try {
    // Omitted event handling process
    } catch(Throwable throwable){
    // Write error/metric to alert
}

From this perspective, Spring’s design is quite comprehensive. It considers various situations. However, Spring users often do not delve into its internal implementation, which can lead to various problems. On the contrary, if you have an understanding of its implementation and awareness of common errors, you are likely to quickly avoid pitfalls, and the project can run more smoothly.

Key Review #

Today we briefly reviewed the basic process of Spring event handling. In fact, when designing a generic event handling framework, we often make three types of mistakes:

  1. Misunderstanding the meaning of the event itself;
  2. Listening to the wrong event propagation system;
  3. Interference between event handlers, resulting in some event handling being unable to complete.

These three mistakes correspond exactly to the three cases we discussed in this lesson.

In addition, in the process of Spring event handling, we also learned about the special way of loading listeners, which is to directly load from the configuration file META-INF/spring.factories using the SPI mechanism. This approach, or idea, is worth learning because it is used in many Java application frameworks. For example, Dubbo uses an enhanced version of SPI to configure encoders and decoders.

Thought Question #

In Case 3, we mentioned that the default event execution is performed in the same thread as the event publisher, which can be evidenced by the following log:

2021-03-09 09:10:33.052 INFO 18104 — [nio-8080-exec-1] c.s.p.listener.HelloWorldController : start to publish event- 2021-03-09 09:10:33.055 INFO 18104 — [nio-8080-exec-1] c.s.p.l.example3.MyFirstEventListener : com.spring.puzzle.class7.example3.MyFirstEventListener@18faf0 received: com.spring.puzzle.class7.example3.MyEvent[source=df42b08f-8ee2-44df-a957-d8464ff50c88]

From the log, we can see that both the event publication and execution use the nio-8080-exec-1 thread. However, when there are multiple events, we often hope that the events can be executed faster or that their execution can be asynchronous without affecting the main thread. What should we do in this case?

Looking forward to seeing your reply in the comments section. See you in the next class!