yeuk0
yeuk0

Reputation: 43

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.

Upvotes: 0

Views: 654

Answers (2)

yeuk0
yeuk0

Reputation: 43

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.

Upvotes: 0

rzwitserloot
rzwitserloot

Reputation: 103018

NOTE: It sounds like your custom logging needs are better served by logging to slf4j as normal, and registering an additional handler with the slf4j framework so that slf4j will forward any logs to you (in addition to the other handlers, such as the one making the log files).

Lombok is processing @CustomLog

The generated log field is static. If an annotation is going to help at all, you'd need @BeforeClass, but that probably also isn't in time. Lombok's magic doesn't seem relevant here. Check out what delombok tells you lombok is doing: It's just.. a static field, being initialized on declaration.

Upvotes: 1

Related Questions