Home > Software design >  Recommended way to create an array of Spring Beans
Recommended way to create an array of Spring Beans

Time:08-18

I'm trying to find the most elegant and Spring way to create an (unlimited) of Spring managed beans.

One of the questions is why I'd need to do it. The most common use-case I have is when using Vaadin I have to create a list of layout depending on a collection fetched from database. Based on that I'll create a list of layouts that might require access to Spring services.

There are 2 methods that I'm using at the moment:

//Spring ApplicationContext
@Component
public class MainLayout {
   
   @Autowired ApplicationContext ctx;

   public void init() {
      items.forEach(i -> this.add(ctx.getBean(ChildLayout.class, i));
   } 

}

//This works well if ChildLayout has autowired dependencies and no arguments
@Component
@Scope("prototype")
public class ChildLayout {
    @Autowired ServiceA serviceA;
    @Autowired ServiceB serviceB;

    public ChildLayout(MyDTO item) { 
        //IDE Interpreter will complain that MyDTO is not a Spring Component (because it's not)
        ...
    }
}

Then I figured out that I could create use a factory pattern and move to a Constructor Based bean injection as it's recommended.

@Component
public class ChildLayoutFactory {
    private final ServiceA serviceA;
    private final ServiceB serviceB;

    public ChildLayoutFactory(ServiceA a, Service b) {...}

    public ChildLayout create(MyDTO dto) {
        return new ChildLayout(a, b, dto)
    }
}

public class ChildLayout {
    ServiceA serviceA;
    ServiceB serviceB;

    public ChildLayout(ServiceA a, ServiceB b, MyDTO item) { 
        this.serviceA = a;
        this.serviceB = b;
        ...
    }
}


@Component
public class MainLayout {
   
   @Autowired ChildLayoutFactory childLayoutFactory; //Or constructor based

   public void init() {
      items.forEach(i -> this.add(childLayoutFactory.create(i));
   } 

}


Both methods work fine. The problem with the second is that I need an extra class and that if I have also some extra dependency that I need to create on the fly I'll need to pass the dependency on the constructors upstream.

Is there any way what I could combine the advantages of both? Is it recommended to do it as shown?

CodePudding user response:

That is bit unclear what problem you are trying to solve: your first code snippet looks good, except the "modern way" is to create factory bean and write something like:


public class MainLayout {

    @Autowired
    private ObjectProvider<ChildLayout> layoutProvider;

    public void init() {
        items.forEach(i -> this.add(layoutProvider.getObject(i));
    }

}

@Configuration
public class ChildLayoutConfiguration {

    @Bean
    @Scope("prototype")
    public ChildLayout childLayout(MyDTO item) {
        return new ChildLayout(item);
    }

}

However, some guys advocate some controversial opinions like:

  • using @Autowired is code smell because you get coupled with spring framework
  • you "should prefer" to use constructor DI over other options

and the problem is those guys do not tell you the whole truth:

  • even if you are working hard to not get coupled with spring framework, the only way to prove that is to perform integration/manual testing against every DI/IoC framework
  • constructor DI assumes that framework must somehow resolve dependency graph, however it sometimes does and sometimes does not, practically that means if you even perform integration tests it means nothing: at any moment you PROD env may not start

CodePudding user response:

So here actually the issue is how to inject prototype into a singleton bean (the class marked with @Component annotation in this case).

There are ways to implement this so that your code won't be coupled with spring framework.

Option 1

Use javax.inject.Provider<T>: add to your pom.xml (or gradle build file) the following dependency:

 <dependency>
      <groupId>javax.inject</groupId>
      <artifactId>javax.inject</artifactId>
      <version>1</version>
 </dependency>

Its a small jar with only some interfaces and is not related to spring Then you can create a singleton like this:

  @Component
public class MainLayout {
    private final ServiceA serviceA;
    private final ServiceB serviceB;
    private final Provider<ChildLayout> childLayoutProvider;


    public MainLayout(ServiceA a, Service b, Provider<ChildLayout> childLayoutProvider) {...}

    public void init(MyDTO dto) {
        List<MyDTO> dtos = ...
        for(MyDTO dto : dtos) { 
           var childLayout = provier.get();
           childLayout.setDto(dto);
           // child Layout is ready   
        } 

    }
}

While in some cases this method works well, one significant drawback is that you break encapsulation of the childLayout and have to set the dto with a setter.

So, you need something similar but in a way that can accept a parameter in constructor (unlike provider.get())

Spring allows this style by using java.util.Function as a bean.

Option 2

@Configuration
public class MyConfig {
  
   @Bean
   public Function<MyDTO, ChildLayout> beanFactory() {
        return dto -> childLayout(dto);
   } 

   @Bean
   public MainLayout(ServiceA serviceA, ServiceB serviceB, Function<MyDTO, 
          ChildLayout> beanFactory) {
       return new MainLayout(serviceA, ServiceB, beanFactory);
   }
   
   @Bean
   @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
   public ChildLayout childLayout(MyDTO dto
                                  ... /**possibly other parameters **/) {
       return new ChildLayout(dto);
   }
}

@AllArgsConstructor
public class MainLayout {
    private final ServiceA serviceA;
    private final ServiceB serviceB;
    private final Function<MyDTO, ChildLayout> beanFactory;

    public void init () {
       List<MyDTO> dtos = ... 
       for(MyDTO dto : dtos) {
         ChildLayout layout = beanFactory.apply(dto);
         ...
       }

    }
}

In general there are many ways to inject prototypes into singleton beans. You might want to read This Article for more details (it also describes the way I've written about here). In general these are my favorite ones because they're not coupled to spring in any case, so you can easily unit test your singleton bean.

  • Related