Search code examples
javaspringspring-bootjunit4lombok

Load Spring Boot component before Lombok instantation in unit test


I developed a kind of wrapper to make it work as a custom logger. I'm instantiating this class using @CustomLog Lombok annotation just to make it easier and cleaner. The tricky thing comes next: the idea behind this wrapper is to use a common logger (as org.slf4j.Logger) along with a custom monitor class that each time I call log.error(), the proper message gets logged in the terminal and the event is sent to my monitoring tool (Prometheus in this case).

To achieve this I did the following classes:

  1. CustomLoggerFactory the factory called by Lombok to instantiate my custom logger.
public final class CustomLoggerFactory {

     public static CustomLogger getLogger(String className) {

        return new CustomLogger(className);
     }
}
  1. CustomLogger will receive the class name just to then call org.slf4j.LoggerFactory.
public class CustomLogger {

    private org.slf4j.Logger logger;
    private PrometheusMonitor prometheusMonitor;
    private String className;

    public CustomLogger(String className) {

        this.logger = org.slf4j.LoggerFactory.getLogger(className);
        this.className = className;
        this.monitor = SpringContext.getBean(PrometheusMonitor.class);
    }
}
  1. PrometheusMonitor class is the one in charge of creating the metrics and that kind of things. The most important thing here is that it's being managed by Spring Boot.
@Component
public class PrometheusMonitor {

    private MeterRegistry meterRegistry;

    public PrometheusMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }
}
  1. As you may noticed, to access PrometheusMonitor from CustomLogger I need an additional class in order to get the Bean / access the context from a non Spring managed class. This is the SpringContext class which has an static method to get the bean by the class supplied.
@Component
public class SpringContext implements ApplicationContextAware {

    private static ApplicationContext context;

    public static <T extends Object> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        SpringContext.context = context;
    }
}

So all this works just fine when running the application. I ensure to load SpringContext class before anything else, so once each CustomLogger gets instantiated it just works.

But the BIG issue comes here: this is not working while unit testing my app. I tried many things and I saw some solutions that may help me but that I'm trying to avoid (e.g. using PowerMockito). Lombok is processing @CustomLog annotation before any @Before method I add to my test class. Once getBean() method is called I get an exception cause context is null.

My guesses are that I could solve it if I can force the SpringContext to be loaded before Lombok does its magic, but I'm not sure that's even possible. Many thanks for taking your time to read this. Any more info I can provide just let me know.


Solution

  • Well I managed to solve this issue changing a little how the CustomLogger works. Meaning that instead of instantiating monitor field along with the logger, you can do it the first time you'll use it. E.g.:

    public class CustomLogger {
    
        private org.slf4j.Logger logger;
        private Monitor monitor;
    
        public CustomLogger(String className) {
            this.logger = org.slf4j.LoggerFactory.getLogger(className);
        }
    
        public void info(String message) {
            this.logger.info(message);
        }
    
        public void error(String message) {
            this.logger.error(message);
            if (this.monitor == null) {
                this.monitor = SpringContext.getBean(PrometheusMonitor.class);
            }
            this.monitor.send(message);
        }
    }
    

    But after all I decided to not follow this approach because I don't think it's the best one possible and worth it.