Search code examples
javaspringspring-bootmultithreadingunit-testing

Get different instance of autowired class bean in for loop


I am new to Spring Boot, learning autowire annotation, dependency injection and unit testing

I have a code in Spring Boot something like below

@Component
class A {
  public void classAMethod() {
    for(int i=0; i<10; i++) {
      B b = new B(dynamicStirngValue,dynamicIntValue, i);
      Object xyz = b.execute();
    }
  }
}

@Component
@Scope("prototype")
class B {

  public B(String str1, int int1, int int2) {
    // setting up the passed parameters
  }
  public Object execute() {
    // some code to use the class variables
  }
}

I want to achieve 3 things with the code:

  1. Instead of creating an object of B using the new keyword I want it to be using @autowired (Dependency injection), this will be helpful so that I can mock B and mock the return of execute() method while unit testing.

  2. Since B has a parameterized constructor, I want the ability to get bean by passing the parameters, the parameter values will be dynamic, i.e., for each iteration of the loop the values passed will be different. The examples I found online are using values from the properties files.

  3. The classAMethod() is being called in multi-threaded environment, that's why in for loop I am every time creating a new instance of class B, so as to avoid concurrency issues, so using autowire I want to get a new instance of class B. I cannot use synchronized keyword inside B because it internally performs lots of operations, so creating a new instance every time is more simpler than locking and unlocking blocks of code and it will lead to a decrease in performance issues as only one thread will have access to that block of code.

Can you provided an example for this scenario? Or is there any other way.

I want to do this for 2 reasons:

  1. Unit test
  2. Make the application completely with Spring Boot concepts (DI) and avoid new keyword

I can do the unit tests by constructor mocking, but the problem with this approach is that we need to do @PrepareForTest(A.class), which has its own problem like code coverage which shows as 0%.

Thanks


Solution

  • Let see how we can solve this:

    • Configure a bean to supply dynamicStirngValue, dynamicIntValue and we can use tread safety list using CopyOnWriteArrayList
    @Configuration
    public class ParamsKeyVal  {
        @Bean
        public List<HashMap<String, Integer>>  getParams() {
            HashMap<String, Integer> params = new HashMap<>();
            List<HashMap<String, Integer>> list = new CopyOnWriteArrayList<>();
    
            list.add(Map.of("key1", 10));
            list.add(Map.of("key2", 13));
    
            // this params can found form other source like properties file/database
            return list;
        }
    }
    
    
    • update component A so that it can be test friendly
    @Component
    class A {
        private final List<HashMap<String, Integer>> params;
        private final B b;
    
        @Autowired
        public A(List<HashMap<String, Integer>> params, B b) {
    
            this.params = params;
            this.b = b;
        }
    
        public void classAMethod() {
            for(int i=0; i<params.size(); i++) {
    
                HashMap<String, Integer> entrySet = params.get(i);
                String dynamicStirngValue = null;
                Integer dynamicIntValue = null;
                for (Map.Entry<String, Integer> entry : entrySet) {
    
                    dynamicStirngValue =  entry.getKey();
                    dynamicIntValue = entry.getValue();
                    break;
                }
    
                b.setDynamicStirngValue(dynamicStirngValue);
                b.setDynamicIntValue(dynamicIntValue);
                b.setDynamicInt(i);
    
                Object xyz = b.execute();
            }
        }
    
    }
    
    
    • update component B for no argument constructor
    
    @Component
    @Scope("prototype")
    class B {
    
        public B() {
    
        }
        public B(String str1, int int1, int int2) {
            // setting up the passed parameters
        }
        public Object execute() {
            // some code to use the class variables
        }
    }