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).