How to create a custom Spring Boot FailureAnalyzer


Today, I was working with a Spring Boot application that does local JVM cache warming on the server start up. Application was calling a global Redis cache and storing state that does not change often in an in-memory JVM cache. It is a common pattern that many applications use. In our case, application not only just warm the cache but it also first process some data and then cache the result in the local JVM cache.

Many times junior developers forget to start redis or any other depending service and then application fails to start on their local machine. Then, they need to spend few minutes reading the long Java stack trace to find the problem. These stack trace can be quite long. And, it is difficult to find needle in this haystack.

Recently, I learnt about a Spring Boot feature called FailureAnalyzer. FailureAnalyzer allows you to intercept exceptions that occur at the start-up of an application causing an application startup failure. Using FailureAnalyzer you can replace the stack trace of the exception with a more human readable message. The best example of this is when your code has cyclic dependencies. A common example of cyclic dependency is a bean A depending on bean B and vice versa as shown below.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {
  private B b;

  @Autowired
  public A(B b) {
    this.b = b;
  }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class B {

  private A a;

  @Autowired
  public B(A a) {
    this.a = a;
  }
}

When you will start a Spring Boot application with these two classes then you will not get a long stack trace but a meaningful message as shown below.

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

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  a defined in file [~/failure-analyzer/target/classes/com/example/failureanalyzer/A.class]
↑     ↓
|  b defined in file [~/failure-analyzer/target/classes/com/example/failureanalyzer/B.class]
└─────┘

You will agree it is much easier for a developer to find what is wrong with their application and fix it.

Writing a custom FailureAnalyzer

Let’s revisit the example that I mentioned at the start of the blog. I want to help developer figure out quickly that one of the service that they depend on is not running on their machine.

Our application on start up calls the CacheService to load the cache. We listen to ContextRefreshEvent and then load the cache as shown below. This is the standard way to perform some logic on server startup in Spring applications.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {

  @Autowired
  private CacheService cacheService;

  @Override
  public void onApplicationEvent(ContextRefreshedEvent event) {
    cacheService.load();
  }
}

For this blog, CacheService just throws an exception. In a real-world application, the exception will be deeply nested.

import org.springframework.stereotype.Component;

@Component
public class CacheService {

  public void load() {
    throw new CacheLoadFailedException(ServiceType.REDIS, "Application failed to start");
  }
}

In the load method, I am throwing CacheLoadFailedException passing it the service that was unavailable.

To create a custom FailureAnalyzer, we will extend AbstractFailureAnalyer as shown below. There is only method that you need to override analyze . It gets both the root exception and the cause. You then have to build an object of type FailureAnalysis and return that back as shown below.

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

public class CacheLoadFailureAnalyzer extends AbstractFailureAnalyzer<CacheLoadFailedException> {

  @Override
  protected FailureAnalysis analyze(Throwable rootFailure, CacheLoadFailedException cause) {
    ServiceType serviceType = cause.getServiceType();
    if (serviceType == ServiceType.REDIS) {
      return new FailureAnalysis(
        cause.getMessage(),
        "Please ensure redis is running on port 6379",
        cause
      );
    } else if (serviceType == ServiceType.POSTGRES) {
      return new FailureAnalysis(
        cause.getMessage(),
        "Please ensure postgres is running on port 5432",
        cause
      );
    }
    throw cause;
  }

}

In the code shown above, we are checking the service type and based on the service type we are building FailureAnalysis object. If we don’t understand the type then we just rethrow the cause.

Registering the custom FailureAnalyzer

To register custom FailureAnalyzer, you need to create a file named spring.factories in the resources/META-INF directory. The file needs to contain org.springframework.boot.diagnostics.FailureAnalyzer key with a value of the full name of our custom FailureAnalyzer class as shown below.

org.springframework.boot.diagnostics.FailureAnalyzer=com.example.failureanalyzer.CacheLoadFailureAnalyzer

CacheLoadFailureAnalyzer in Action

To see CacheLoadFailureAnalyzer in action just start the application and you will be greeted by following message.

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

Description:

Application failed to start

Action:

Please ensure redis is running on port 6379

You can add more meaning to these messages by capturing information in your custom exception object and then using that in your custom FailureAnalyzer.

Github repo

You can read the complete code at following location Github repository spring-boot-failure-analyzer-example

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s