4

In my code there is a Bean Admin. One of the operations of Admin is creating Tasks and starting them.

Operation performed by Task is fairly complex. So it is split into to a few different Step classes.

This is kind of how my code looks

public interface Admin{
    void start();

    //other methods
}

public class AdminImpl implements Admin{
    private BeanA bean1;
    private BeanB bean2;
    //other fields

    public Admin(BeanA bean1,
        BeanB bean2,
        BeanC bean3
        //Lot more parameters
        BeanO bean15
    ){
        this.bean1 = bean1;
        this.bean2 = bean2;
        //and so on
    }

    public void start(){
        return new Task(bean1, bean2, bean3,...).start()
    }

    //other methods
}

public class Task{
    private BeanA bean1;
    private BeanB bean2;
    //other fields
    public Task(BeanA bean1,
        BeanB bean2,
        BeanC bean3
        //Lot more parameters
    ){
        //bind parameters to fields
    }

    public void start(){
        new Step1(bean1, bean2,.. other parameters).do();
        new Step2(bean3, bean7,.. other parameters).do();
        //more steps
    }
}

@Configuration
public class MyConfiguration{
    @Bean
    public Admin admin(BeanA bean1, BeanB bean2....){
        return new AdminImpl(bean1, bean2...);
    }
}

As you can see each of the Step classes have 2 or three Bean dependencies. Step classes are not Spring Beans, so they are handed down the dependencies from Task. Task is also not a Spring Bean so it gets the dependencies from Admin. This lead to Admin having way too many dependencies (~15).

I tried this: https://dzone.com/articles/autowiring-spring-beans-into-classes-not-managed-by-spring.

Basically you create a service Bean called BeanUtil which is ApplicationContextAware. A static method getBean gets beans using the ApplicationContext.

Step class now look like this:

class Step{
    public Step(){
        BeanA bean1 = BeanUtil.getBean(BeanA.class);
        BeanB bean2 = BeanUtil.getBean(BeanB.class);
    }

    public void do(){
        //do stuff 
    }
}

This solves the initial problem, but then I had difficulty with testing. This is how the test class looks now.

 @ContextConfiguration(loader = AnnotationConfigContextLoader.class)
    public class Step1Test extends AbstractTestNGSpringContextTests {

        @Test
        public void test(){
            Step1 step = new Step().do();
        }

        @Configuration
        static class MockConfiguration {

            @Bean
            public BeanA beanA() {
                BeanA mockBeanA=Mockito.mock(BeanA.class);
                // set behavior of mock
                return mockBeanA;
            }

            @Bean
            public BeanUtil beanUtil(){
                return new BeanUtil();
            }
        }
    }

You can't change the behaviour of mocks for different test cases without creating different Configuration classes. This feels like solving one problem by creating another.

Is this a common problem faced by Spring developers, where classes at higher level of abstraction end up having too many dependencies? Or is there something wrong with my design? What is the right way to handle or avoid this?

Other questions that seemed similar but aren't

Edit: One suggestion I got from user7 is I group the BeanXs and pass the abstraction to Admin. The beans don't have any logical grouping that I could leverage on. Moreover, many of the Steps need complete access (access to all the methods in the interface) to the beans. This would lead to the abstraction getting bloated.

M. Jaseem
  • 43
  • 1
  • 7
  • Is there a way to group individual `BeanXs` and inject the grouped abstraction into the AdminImpl? – Thiyagu Mar 10 '18 at 09:37
  • This is a valid suggestion, but I can't unfortunately use it. I have responded in detail in the post. – M. Jaseem Mar 10 '18 at 09:52
  • What do you mean by `many of the Steps need complete access (access to all the methods in the interface) to the beans`? Each Step only needs a few Beans to be passed to it right? – Thiyagu Mar 10 '18 at 09:56
  • Why can't you make Step as Beans? – Thiyagu Mar 10 '18 at 09:57
  • Is there a reason why you don't want your steps to be spring beans? That would be an obvious solution. That way your steps could use constructor injection and you could test them. – Peter Borbas Mar 10 '18 at 10:00
  • Steps take other non-bean parameters that can't be injected by the container. I want to pass them dynamically to different instances of StepX. I don't know how to achieve that. – M. Jaseem Mar 10 '18 at 10:10

1 Answers1

1

You can make your steps Spring beans as prototype beans (since they are stateful, and you want a different instance every time), and inject Provider<Step> in your Task beans (which, if I understand correctly, can be singleton beans).

For example:

public class Step1 {
    private Bean1 bean1;
    private Bean2 bean2;

    private final String someValue;
    private final String someOtherValue;

    public Step(String someValue, String someOtherValue) {
        this.someValue = someValue;
        this.someOtherValue = someOtherValue;
    }

    @Autowired
    public void setBean1(Bean1 bean1) {
        this.bean1 = bean1;
    }

    @Autowired
    public void setBean2(Bean2 bean2) {
        this.bean2 = bean2;
    }

    do() {
        // ...
    }
}

In your configuration class, you then define The various Steps as beans, with methods expecting the needed arguments:

@Bean
@Scope("prototype")
public Step1 step1(String someValue, String someOtherValue) {
    return new Step(someValue, someOtherValue);
}

And in the Task bean, you inject an ObjectProvider<Step1>:

private ObjectProvider<Step1> stepProvider;

public Service(ObjectProvider<Step1> step1Provider) {
    this.stepProvider = stepProvider;
}

public void start() {
    Step1 step1 = step1Provider.getObject("a", "b");
    step1.do();
}
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • Using prototype works. In my case Task also needs to be a prototype bean, but that's fine. – M. Jaseem Mar 10 '18 at 12:36
  • In your sample Step class you have used setter for bean dependencies and constructor for runtime parameters. Is it possible for all those to be in constructor? – M. Jaseem Mar 10 '18 at 12:37
  • Yes, but then you would need to declare, or autowire the BeanXxx beans in your configuration class in order to be able to pass them as argument to the StepXxx constructors. – JB Nizet Mar 10 '18 at 12:46
  • I have autowired the BeanXs in my configuration class. So that's ok. I am not able to figure out how to get spring to pass some constructor arguments (Beans) while I pass the rest (runtime parameters). I guess this question should be already answered somewhere on SO, I am not able to find it though. – M. Jaseem Mar 10 '18 at 13:04
  • Well, `new Step1(bean1, bean2, someValue, someOtherValue)`. You have access to bean1 and bean2 since they're autowired, and you have access to someValue and someOtherValue, since those are the arguments of the `@Bean`-annotated method. – JB Nizet Mar 10 '18 at 13:07