0

I have a spring boot base abstract config class that creates a bean. If I then inherit from it, the bean will be created later than my controller (which needs to auto-wire it and thus fails). Note: it does get created, just after the controller. So it can't be auto-wired but has to be found via appContext.getBean( BeanType.class )

If I instead override the bean method in the child class, then it's created before the controller and it can be auto-wired.

How can i fix this and make the super-class bean definition load at the same time as the child class?

@SpringBootApplication
public class ChildConfig extends ParentConfig<PCTestBean>
{
    public ChildConfig()
    {
        super();
    }

    @Override
    public PCTestBean getT()
    {
        return new PCTestBean();
    }
}

public abstract class ParentConfig<T>
{
    public ParentConfig() {}

    @Bean
    public T createTestBean()
    {
        return getT();
    }

    public abstract T getT();
}

public class PCTestBean
{
}

@RestController
@RequestMapping( "/client" )
public class MyController
{
    @Autowired
    private PCTestBean pcTestBean;

    @RequestMapping( "/get" )
    @ResponseBody
    public String getClient(HttpServletRequest request) throws Exception
    {
        return pcTestBean.toString();
    }
}

@RunWith( SpringJUnit4ClassRunner.class )
@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@ContextConfiguration(
    classes = {
        ChildConfig.class
    }
)
public class TestConfigs
{
    @LocalServerPort
    private String port;

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Before
    public void setUp() throws Exception
    {
        mockMvc = MockMvcBuilders
            .webAppContextSetup( context )
            .build();
    }

    @Test
    public void testValidCall() throws Exception
    {
        MvcResult result = mockMvc.perform(
            MockMvcRequestBuilders.get( new URI( "http://localhost:" + port + "/client/get" ) )
        )
            .andExpect( MockMvcResultMatchers.status().isOk() ).andReturn();

        System.out.println( result.getResponse().getContentAsString() );
    }
}
Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
Don Rhummy
  • 24,730
  • 42
  • 175
  • 330
  • Please provide a [mcve] that shows that Spring fails to inject the `TestBean` bean into the `@Autowired` annotated field. – Sotirios Delimanolis Jan 31 '18 at 00:41
  • @SotiriosDelimanolis I'm doing it now, and thank you, it showed it's not just being a super-class, the super class method uses generics. and THAT is what leads to the issue (it *does* create it, just not till after the controller). But it's still odd that overriding it eliminates the issue. – Don Rhummy Jan 31 '18 at 00:52
  • @SotiriosDelimanolis I updated the example – Don Rhummy Jan 31 '18 at 01:35

2 Answers2

1

When Spring scans your configuration class, ChildConfig, it discovers this inherited method

@Bean
public T createTestBean() {
    return getT();
}

and registers a bean definition for it. That bean definition contains metadata about the type of the bean. That type is inferred from the return type of the method. In this case, it's resolved to Object because the type variable T has no bounds in its declaration and because Spring doesn't try to resolve it based on the type argument provided in ChildConfig's extends ParentConfig<PCTestBean> clause.

When Spring then tries to process the

@Autowired
private PCTestBean pcTestBean;

injection target, it looks for a PCTestBean bean, which it doesn't think it has, because the metadata is lacking. IF the bean hasn't been initialized through some other forced order, then Spring has no other information to go on and thinks the bean doesn't exist.

When you change your code to

instead override the bean method in the child class

the return type of the method is PCTestBean which Spring can then match to the @Autowired injection requirement, find (and initialize) the bean, and inject it.

By the time you use ApplicationContext#getBean(Class), the PCTestBean has been initialized. Spring can therefore rely on the actual type of the instance. It'll more or less loop through all beans and check whether beanClass.isInstance(eachBean), returning the one that matches (or failing if more than one does).

Pankaj, in their answer, suggests using @DependsOn (it was wrong when they suggested it, before you edited your question). That can help establish the order I mentioned earlier.


I don't how extensive your configuration class is that you think you need generics to abstract some behavior away, but I would suggest just dropping the generic behavior and be explicit in each class.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • Your explanation (`it looks for a PCTestBean bean, which it doesn't think it has, because the metadata is lacking.`) doesn't seem right because it *does* find it. I can search for the bean by type `appContext.getBean( BeanType.class )` and it does find it, but not until later. – Don Rhummy Jan 31 '18 at 05:21
  • You mention the bean could be initialized via some forced manner. How would I do that? – Don Rhummy Jan 31 '18 at 07:12
  • @DonRhummy The `@DependsOn` is one way. I can't think of others at the moment. You could also qualify your `@Autowired` with a bean name, ie. annotate it with `@Resource(name = "createTestBean")`. – Sotirios Delimanolis Jan 31 '18 at 16:09
0

Try DependsOn annotation, it guarantees that the child bean should be created after the parent bean

@Configuration
public class ChildConfig extends ParentConfig
{
    public ChildConfig()
    {
        super();
    }


    @DependsOn("parentConfig")
    @Override
    public TestBean createTestBean()
    {
        return super.createTestBean();
    }*/
}

public abstract class ParentConfig
{
    public ParentConfig() {}

    @Bean (name ="parentConfig")
    public TestBean createTestBean()
    {
        return new TestBean();
    }
}
Pankaj Gadge
  • 2,748
  • 3
  • 18
  • 25