How to resolve circular dependency for Spring context?

Viewed 920

I have three classes:

open class RedirectProcessor(
    private val adProcessor: AdProcessor
) {

    fun run(depth: Int): String = 
        if (depth < 3) adProcessor.run(depth + 1) else "redirect"
}
open class FallbackProcessor(
    private val adProcessor: AdProcessor
) {

    fun run(depth: Int): String = 
        if (depth < 3) adProcessor.run(depth + 1) else "fallback"
}
open class AdProcessor(
    private val redirectProcessor: RedirectProcessor,
    private val fallbackProcessor: FallbackProcessor
) {

    fun run(depth: Int): String = 
        depth.toString() + 
        redirectProcessor.run(depth) +
        fallbackProcessor.run(depth)
}

So, they depends on each other. I try to configure spring context as below:

@Configuration
class Config {

    @Bean
    @Lazy
    fun redirectProcessor(): RedirectProcessor = RedirectProcessor(adProcessor())

    @Bean
    @Lazy
    fun fallbackProcessor(): FallbackProcessor = FallbackProcessor(adProcessor())

    @Bean
    fun adProcessor() = AdProcessor(
        redirectProcessor = redirectProcessor(),
        fallbackProcessor = fallbackProcessor()
    )
}

I known that I have to use @Lazy annotation. If I mark my services with @Component annotation and use @Lazy in constructor it works fine. But I need to define beans using @Bean annotation and it causes problems. Is there any way to solve it?

2 Answers

I can't say for Kotlin (my knowledge of kotlin is rather limited at this point), but in Java with the last available spring version (5.2.6.RELEASE)

I've got it working with the following "kotlin to java" translation of your example:

public class RedirectProcessor {
    private final AdProcessor adProcessor;

    public RedirectProcessor(AdProcessor adProcessor) {
        this.adProcessor = adProcessor;
    }

    public String run(int depth) {
        if(depth < 3) {
            return adProcessor.run(depth + 1);
        }
        else {
            return "redirect";
        }
    }
}


public class FallbackProcessor {
    private final AdProcessor adProcessor;

    public FallbackProcessor(AdProcessor adProcessor) {
        this.adProcessor = adProcessor;
    }

    public String run(int depth) {
        if(depth < 3) {
            return adProcessor.run(depth + 1);
        }
        else {
            return "fallback";
        }
    }
}

public class AdProcessor {
    private RedirectProcessor redirectProcessor;
    private FallbackProcessor fallbackProcessor;

    public AdProcessor(RedirectProcessor redirectProcessor, FallbackProcessor fallbackProcessor) {
        this.redirectProcessor = redirectProcessor;
        this.fallbackProcessor = fallbackProcessor;
    }

    public String run (int depth) {
        return depth + redirectProcessor.run(depth) + fallbackProcessor.run(depth);
    }
}

Then The trick was to use the configuration in a different (yet totally "legitimate" way from Java Configuration rules's standpoint):

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }


    @Bean
    public RedirectProcessor redirectProcessor (@Lazy AdProcessor adProcessor) {
        return new RedirectProcessor(adProcessor);
    }

    @Bean
    public FallbackProcessor fallbackProcessor (@Lazy AdProcessor adProcessor) {
        return new FallbackProcessor(adProcessor);
    }

    @Bean
    public AdProcessor adProcessor (RedirectProcessor redirectProcessor, FallbackProcessor fallbackProcessor) {
        return new AdProcessor(redirectProcessor, fallbackProcessor);
    }

    @EventListener
    public void onApplicationStarted(ApplicationStartedEvent evt) {
        AdProcessor adProcessor = evt.getApplicationContext().getBean(AdProcessor.class);
        String result = adProcessor.run(2);
        System.out.println(result);
    }
}

Note the usage of @Lazy annotation on a parameter and not on the bean itself.

The listener is done for testing purposes only. Running the application prints 23redirectfallback3redirectfallback

Now why does it work?

When spring sees such a @Lazy annotated parameter - it creates a runtime generated proxy (with CGLIB) from the parameter class.

This proxy acts in a way that it wraps the bean and this bean will be fully created only when it's "required" for the first time (read, we'll call methods of this bean in this case).

If you work with @Component its the same as the following declaration:

@Component
public class FallbackProcessor {
    private final AdProcessor adProcessor;

    public FallbackProcessor(@Lazy AdProcessor adProcessor) {
        this.adProcessor = adProcessor;
    }

    public String run(int depth) {
       ...
    }
}

One Side note, I haven't put @Autowired on constructor of FallbackProcessor class in the last example, only because if there is a single constructor spring will "recognize that" and use it to inject all the dependencies.

The following tutorial and this somewhat old thread of SO can be relevant as well (worth reading).

I ran into same issue, and @Autowire annotation doesn't work for some reason I don't know.

So I used another workaround:

  • inject ApplicationContext instead of bean itself
  • to retrieve bean instance from ApplicationContext

code like:

class ServiceA(
    private val serviceB: ServiceB
) {
......
}


class ServiceB(
    private val applicationContext: ApplicationContext
) {
    private val serviceA: ServiceA by lazy {
        // we need this logic for only once
        // so "property delegated by lazy ..." is perfect for this purpose
        applicationContext.getBean(ServiceA::class.java)
    }
......
}

Related