Spring Boot: implementing a startup failure analyzer

If you have been using Spring Boot for a while, sometimes you may have encountered a failure when launching an application. One common and frequent case is when the application is already running, and you try to re-run the application (or another application that uses the same server port). In this case, Spring Boot logs a clean and informative message about the server port that is already in use. Would you like to produce a similar failure message? This article shows how to implement a custom startup failure analyzer.

Introduction

First of all, I want to show you what is a typical startup failure message. I’m sure you’ve seen such a log at least once in your developments with Spring Boot.

   ……… other logs ………
2023-02-06 18:09:52.511  WARN 7076 --- [  restartedMain] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.PortInUseException: Port 8080 is already in use
2023-02-06 18:09:52.514  INFO 7076 --- [  restartedMain] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2023-02-06 18:09:52.526  INFO 7076 --- [  restartedMain] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-02-06 18:09:52.542 ERROR 7076 --- [  restartedMain] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

The above log shows a specific message with a description and an action that should help solve the problem. It is not a trivial stack trace log, which is only sometimes useful! This error message is produced by a startup failure analyzer, represented in Spring Boot by the FailureAnalyzer interface described in the next section.

The FailureAnalyzer interface

FailureAnalyzer is a simple, functional interface that has the following definition:

@FunctionalInterface
public interface FailureAnalyzer {
    FailureAnalysis analyze(Throwable failure);
}

The analyze method receives an exception object and must decide whether the exception can be handled. If the exception cannot be handled, the method must return null. Otherwise, it must analyze the failure and return an object of type FailureAnalysis.

FailureAnalysis is a simple bean class that contains only three pieces of information: a description (String), an action (String), and a cause (Throwable). Description and action are the two information reported in the startup failure message.

Spring Boot already provides several implementations of a failure analyzer. For example, the message “Web server failed to start. Port nnnn was already in use.” is produced by the PortInUseFailureAnalyzer that handles exceptions of type PortInUseException. And clearly, you can also implement your startup failure analyzers.

When you implement a failure analyzer, you have mainly two options:

  1. you can implement the FailureAnalyzer interface directly

    public class MyFailureAnalyzer implements FailureAnalyzer { /*...*/ }
  2. you can extend the AbstractFailureAnalyzer<T extends Throwable> abstract class

    public class MyFailureAnalyzer extends AbstractFailureAnalyzer<SomeException> { /*...*/ }

Both approaches are straightforward in general. However, the second has the added benefit of not needing to do any checks or type cast. The second option is greatly explained in Step 3 of my example case, where you can see a more concrete and realistic implementation of a failure analyzer.

Example case

I used a simple scenario to show a concrete example of a custom startup failure. You certainly know that the java.lang.Runtime class has the maxMemory() method to retrieve the maximum amount of memory (heap space) that the JVM can use. This amount is a value in bytes; for example, 134217728 means 128 Megabytes of maximum heap space.

This example case shows how to check the max memory at startup and generate a custom startup failure message if the max memory value is lower than a configured minimum threshold.

I don’t know if this scenario could be applicable in real applications, but at least it is reasonable for this article.

Step 1: Choosing the exception

As you have seen before, the goal of a failure analyzer is to check if it can handle an exception and, if so, to provide a more meaningful description of the problem. Therefore, an exception must be thrown somewhere during the application startup if you want to cause a startup failure with a description/action message. This exception can be any that already exists (e.g., from a framework or a library) or an exception developed by you.

For this article, I created the following custom exception named InsufficientMaxMemoryException:

package your.pkgname;

public class InsufficientMaxMemoryException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    private final long actualMaxMemory;
    private final long minimumMaxMemory;

    public InsufficientMaxMemoryException(long actualMaxMemory, long minimumMaxMemory) {
        super("Insufficient max memory: " + actualMaxMemory);
        this.actualMaxMemory = actualMaxMemory;
        this.minimumMaxMemory = minimumMaxMemory;
    }

    public long getActualMaxMemory() {
        return actualMaxMemory;
    }

    public long getMinimumMaxMemory() {
        return minimumMaxMemory;
    }
}

Note that the exception has an extra state to keep the “actual” and the “minimum” max memory value. These pieces of information are not strictly required for the exception object itself, but they will come in handy for the failure analyzer implementation.

Step 2: Checking the max memory at startup

Now we have to check the actual max memory at startup. Where? How? A Spring Boot application has a complex life cycle, and the Spring Framework can emit several types of events to registered listeners, especially during the startup phase. The Spring Boot reference documents all the predefined events: Application Events and Listeners.

My main goal is to check the max memory as soon as possible but not too soon to avoid problems or restrictions. Therefore, I used ApplicationEnvironmentPreparedEvent. This event is fired after the Environment has been defined but before the context is created. The Environment is very useful because I want to read the “minimum” max memory value from the application.properties configuration file.

In Spring Framework/Boot, any Spring bean can receive events by implementing the ApplicationListener<E extends ApplicationEvent> interface, where E stands for the specific event type you want to handle. For example, the following class handles the ApplicationEnvironmentPreparedEvent with the necessary logic to check the max memory value.

package your.pkgname;

import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;

public class MaxMemoryChecker implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        long actualMaxMemory = Runtime.getRuntime().maxMemory();
        Long minimumMaxMemory = event.getEnvironment().getProperty("app.minimumMaxMemory", Long.class);

        if (minimumMaxMemory != null && actualMaxMemory < minimumMaxMemory) {
            throw new InsufficientMaxMemoryException(actualMaxMemory, minimumMaxMemory);
        }
    }
}
Configuration

There is, however, an important issue to consider. The ApplicationEnvironmentPreparedEvent is fired before the context is created. It means that when the event is fired, there is no Spring bean, dependency injection, autowiring, etc. Therefore, we cannot use @Component to make MaxMemoryChecker a Spring bean!

This type of event listener (like some few others) can only be registered in either of the following two ways:

  • Using SpringApplication.addListeners() or alternatively SpringApplicationBuilder.listeners().
  • Using an entry in the META-INF/spring.factories file. Spring reads this file to know which particular extensions need to be registered.

Since the latter option is more generic and also independent of how the application is built, packaged, and run, this is the solution I generally choose. Thus, in a Maven/Gradle project, create the file <your-project>/src/main/resources/META-INF/spring.factories with the following content:

File: spring.factories

org.springframework.context.ApplicationListener = your.pkgname.MaxMemoryChecker

The part before “=” is fixed to org.springframework.context.ApplicationListener for registering application listeners. The part after “=” is the fully qualified name of your listener implementation class.

Since MaxMemoryChecker uses a configuration property, we must also configure the app.minimumMaxMemory value in application.properties (<your-project>/src/main/resources/application.properties in Maven/Gradle projects).

File: application.properties

# requires minimum 512 MB of max memory
app.minimumMaxMemory = 536870912

You can use a .yaml format instead of the .properties format (see Working With YAML).

Partial test

At this point, if you try to start the application using -Xmx128m as the JVM option (see Extra Options for Java), you should obtain a similar result:

18:13:41.172 [Thread-0] DEBUG org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created RestartClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@7ecf6d3
2023-02-06 18:13:41.662 ERROR 11320 --- [  restartedMain] o.s.boot.SpringApplication               : Application run failed

your.pkgname.InsufficientMaxMemoryException: Insufficient max memory: 134217728
    at your.pkgname.MaxMemoryChecker.onApplicationEvent(MaxMemoryChecker.java:13) ~[classes/:na]
    at your.pkgname.MaxMemoryChecker.onApplicationEvent(MaxMemoryChecker.java:1) ~[classes/:na]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176) ~[spring-context-5.3.25.jar:5.3.25]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169) ~[spring-context-5.3.25.jar:5.3.25]
       ……… long stack trace ………

The application effectively fails at startup because the JVM was started with 128 MBytes of max memory, which is lower than the minimum expected. However, you see an ugly stack trace. We need to do another final step, the implementation of a failure analyzer.

Step 3: Implementing the startup failure analyzer

You must implement a failure analyzer to see a more descriptive failure message instead of the ugly stack trace shown in the previous section. The following class extends AbstractFailureAnalyzer to handle exceptions of type InsufficientMaxMemoryException:

package your.pkgname;

import java.util.Locale;

import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;

public class MaxMemoryFailureAnalyzer extends AbstractFailureAnalyzer<InsufficientMaxMemoryException> {
    @Override
    protected FailureAnalysis analyze(Throwable rootFailure, InsufficientMaxMemoryException imme) {
        return new FailureAnalysis(
                String.format(Locale.ENGLISH, "The actual max memory is insufficient: %,d bytes", imme.getActualMaxMemory()),
                String.format(Locale.ENGLISH, "Configure the max memory to be at least: %,d bytes", imme.getMinimumMaxMemory()),
                imme);
    }
}

This code, at first glance, might look like black magic. Who checks the exception? How is this check performed? The answer is straightforward: AbstractFailureAnalyzer does some very clever things! First of all, you should notice that the class extends a generic type:

extends AbstractFailureAnalyzer<InsufficientMaxMemoryException>

When a class extends a generic type (or implements a generic interface), the information about the parameterization (which is <InsufficientMaxMemoryException> in our case) is maintained and available at runtime. AbstractFailureAnalyzer uses Java reflection to find out this type, then checks if the exception object (or recursively the exception “cause”) is an instance of that type. If a match exists, AbstractFailureAnalyzer performs an “unchecked” cast of the exception object and calls the specialized method analyze(Throwable, T).

Overall, we do not have to do any checks, loops, or type cast. AbstractFailureAnalyzer already performs these operations. If you want to understand better, look at the source code of AbstractFailureAnalyzer (here on GitHub: AbstractFailureAnalyzer.java) because it is fascinating and instructive.

At this point, there is still one small thing to do. A failure analyzer must be registered using an entry in the META-INF/spring.factories file. Thus, this file should now look like the following:

File: spring.factories

org.springframework.context.ApplicationListener = your.pkgname.MaxMemoryChecker
org.springframework.boot.diagnostics.FailureAnalyzer = your.pkgname.MaxMemoryFailureAnalyzer

The part before “=” in the second line is fixed to org.springframework.boot.diagnostics.FailureAnalyzer for registering failure analyzers.

Final test

If you run the application after the three implementation steps, again with the -Xmx128m JVM option, you should see the following result:

18:17:39.630 [Thread-0] DEBUG org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created RestartClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@6447b747
2023-02-06 18:17:40.082 ERROR 17080 --- [  restartedMain] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

The actual max memory is insufficient: 134,217,728 bytes

Action:

Configure the max memory to be at least: 536,870,912 bytes

The above log is the expected result of the failure analyzer implemented in this article. As you can see, the failure message is much more descriptive and pleasant! I also used the String formatting feature (the String.format method, available since Java 5) to print a “localized” message in English. In particular, I used the %,d specifier to format the number with groupings. This custom formatting is not strictly necessary but makes the message more readable.

Conclusions

In this article, you have seen a particular feature of Spring Boot, how to implement a custom startup failure analyzer. This technique is not a standard and recurrent task; however, this article helps better understand how Spring Boot works.

Here is just a recap of the three steps:

  1. Choose or define an exception that will cause the startup failure.
  2. Throw that exception conditionally during the startup phase. You can do this, for example, in a listener, in a @PostConstruct method of a Spring bean, or any other code executed during startup.
  3. Implement a failure analyzer and register it using the META-INF/spring.factories file.

Similar Posts