Search code examples
javaspringdependency-injectionrefactoringinversion-of-control

Bean with multiple constructors in Java-based Spring configuration


I am trying to refactor some application to use Spring DI instead of plain java and stuck with the issue.

Basically i have a class with several constructors:

  public MyClass() {
    this(new A());
  }

  public MyClass(A a) {
    this(a, new B()));
  }

  public MyClass(String string) {
    this(new A(string));
  }

  public MyClass(A a, B b) {
    this.a = a;
    this.c = a.getC();
    this.b = b;
    this.d = b.getD();
  }

  public MyClass(A a, B b, D d) {
    this.a = a;
    this.c = a.getC();
    this.b = b;
    this.d = d;
  }

These constructors are used in many places, some of them in code, some of them in tests, etc.

Now, i am introducing spring java-based application config:

@Configuration
public class ApplicationConfiguration {

  @Bean
  MyClass myClass() {
    return null;
  }

}

And trying to rewrite all the places with getting a bean from application context:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfiguration.class);
MyClass myClass = (MyClass) context.getBean("myClass", arg1, arg2);

And the problem is that in some places i have only arg1, in some both arg1 and arg2, in some i have no args. So, how could i express that in application configuration?

Also the bean is singletone, so if i, for example create several beans with different arguments then this requirement will be broken, i.e.

@Configuration
public class ApplicationConfiguration {

  @Bean
  MyClass myClass1() {
    return new MyClass();
  }

  @Bean
  MyClass myClass2(A a) {
    return new MyClass(a);
  }

  //etc
}

is definitely not a solution.

Thanks in advance

Upd. Looks like the +Avi answer is the right one, but i still don't understand how to do things right.

I created a junit4 test:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ApplicationConfiguration.class)
public class MyTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Before
    public void setupMyClass() {
    //    myClass = new MyClass();
        myClass = (MyClass) applicationContext.getBean("myClass");
    }
}

So, here i want to use MyClass in test, so there is no Foo-like bean like in Avi answer.

I modified context for "different scenarios, and in each of them you need to construct MyClass with different arguments - you need to create multiple beans, that each instantiate its own MyClass" (i got the phrase in a wrong way, but here is what i came to):

@Configuration
public class ApplicationConfiguration {

  //Note: beans have the same name

  @Bean
  MyClass myClass() {
    return new MyClass();
  }

  @Bean
  MyClass myClass(A a) {
    return new MyClass(a);
  }

  //etc
}

But now there is another issue: applicationContext.getBean("myClass") returns me random(depending of number of beans with same name and parameters) bean and not a bean without parameters. And when i specify args - applicationContext.getBean("myClass", new Object[]{}); it says me that it is allowed only for prototype scoped beans. But i want a singleton bean.

Looks like i need another advice: how to get rid of several beans with same names in configuration? Maybe i need a clever factory, or maybe @Autowired(required=false) can help here?

Even if i had Foo-like object in my test, how should i use it in test?

@Configuration
@Import(ApplicationConfiguration.class)
public class FooConfiguration {
     @Autowire
     MyClass myClass; //but which one constructor?

     @Bean
     Foo foo() {
         return new Foo(myClass);
     }

}

I don't want to create MyClass in each configuration itself, i want to have only one, which i can import...

Upd2.

Okay, i removed all constructors and leaved only one, which have all parameters

@Configuration
public class ApplicationConfiguration {       

    @Bean
    MyClass myClass(A a, B b, C c, D d) {
      return new MyClass(a, b, c, d);
    }
}

Now in test i do:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MyTest.TestConfiguration.class)
public class MyTest {

  @Configuration
  @Import(ApplicationConfiguration.class)
  static class TestConfiguration {

    @Bean
    A a() {
        return new A();
    }

    @Bean
    B b() {
        return new B();
    }

    @Bean
    C c() {
        return b().getC();
    }

    @Bean
    D d() {
        return c().getD();
    }
  }

  @Autowired
  private MyClass myClass;
}

But now, i don't understand how to avoid writing this for every and every test..


Solution

  • I think you're missing something here. When you move from constructing dependencies inside the classes that use them, to inject them, you have to stop constructing them at all. I guess that's a little vague, let me explain with an example:

    Let's say you have a class Foo that uses the bean you're creating in the context:

    class Foo {
       public void someMethod() {
          MyClass myClass1 = new MyClass();
          // do something with myClass1
       }
    }
    

    And now you want to inject the bean. You don't call directly to AnnotationConfigApplicationContext like you did in your example. You're doing something like this:

    class Foo {
       private MyClass myClass1;
    
       public Foo(MyClass myClass1) {
          this.myClass1 = myClass1;
       }
    
       public void someMethod() {
          // do something with myClass1
       }
    }
    

    In your application context you create Foo as a bean as well. Something like:

    @Configuration
    public class ApplicationConfiguration {
    
        @Bean
        Foo createFooBean() {
          return new Foo(createMyClassBean());
        }
    
        @Bean
        MyClass createMyClassBean() {
          return new MyClass();
        }
    
    }
    
    • If you have arguments to the constructor of MyClass you need to pass them in the @Configuration class, when you create the bean.
    • If you have different scenarios, and in each of them you need to construct MyClass with different arguments - you need to create multiple beans, that each instantiate its own MyClass.