Why @PostConstruct method is not called when autowiring prototype bean with constructor argument
Asked Answered
B

5

22

I have a prototype-scope bean, which I want to be injected by @Autowired annotation. In this bean, there is also @PostConstruct method which is not called by Spring and I don't understand why.

My bean definition:

package somepackage;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;

@Component
@Scope("prototype")
public class SomeBean {

    public SomeBean(String arg) {
        System.out.println("Constructor called, arg: " + arg);
    }

    @PostConstruct
    private void init() {
        System.out.println("Post construct called");
    }

}

JUnit class where I want to inject bean:

package somepackage;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ContextConfiguration("classpath*:applicationContext-test.xml")
public class SomeBeanTest {

    @Autowired
    ApplicationContext ctx;

    @Autowired
    @Value("1")
    private SomeBean someBean;

    private SomeBean someBean2;

    @Before
    public void setUp() throws Exception {
        someBean2 = ctx.getBean(SomeBean.class, "2");
    }

    @Test
    public void test() {
        System.out.println("test");
    }
}

Spring configuration:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="somepackage"/>

</beans>

The output from execution:

Constructor called, arg: 1
Constructor called, arg: 2
Post construct called
test

When I initialize bean by calling getBean from ApplicationContext everything works as expected. My question is why injecting bean by @Autowire and @Value combination is not calling @PostConstruct method

Bedlam answered 27/2, 2018 at 11:25 Comment(11)
Anyone has an idea?Bedlam
#10513667Garrote
@KarlNicholas SpringJunit4ClassRunner's just deprecated version of SpringRunner, and I don't think the link is relevant eitherAttribute
In the link others don't seem to have any problem with @PostConstruct in a test situation, so maybe some clues there, like By default, Spring will not aware of the @PostConstruct and @PreDestroy annotation. To enable it, ...Garrote
@KarlNicholas in this question it is working, like explained, but the question is what is the difference between autowired and setUp() initializationsAttribute
I don't know, perhaps enabling it makes the difference. I was just throwing some suggestions out, unlike others ...Garrote
Maybe the same problem as https://mcmap.net/q/591535/-spring-boot-postconstruct-not-called-on-component?Bowdlerize
@Bowdlerize The @PostConstruct is called, just not in their expected case.Squadron
Using @Value does not seem to be valid for autowired beans - for the second instance there is 'Creating instance of bean 'someBean'' message in the log, for the first - none. I suspect what Spring is doing in that case is converting the string value "1" to SomeBean using converters, never treating it as a bean.Bowdlerize
@Bowdlerize That seems very likely, single constructor of the right type and all. The floor is yours.Squadron
@Value element is not a bean, it's not attached to the application context so lifecycle method is not called because there are no postprocessors it's a value-object created by converter. I see it as a bug that Spring allows you to annotated field both as @autowired and @valuePampa
E
13

Why is @Value used instead of @Autowired?

The @Value annotation is used to inject values and normally has as destination strings, primitives, boxed types and java collections.

Acording to Spring's documentation:

The @Value annotation can be placed on fields, methods and method/constructor parameters to specify a default value.

Value receives a string expression which is used by spring to handle the conversion to the destination object. This conversion can be through the Spring's type conversion, the java bean property editor, and the Spring's SpEL expresions. The resulting object of this conversion, in principle, is not managed by spring (even though you can return an already managed bean from any of this methods).

By the other hand, the AutowiredAnnotationBeanPostProcessor is a

BeanPostProcessor implementation that autowires annotated fields, setter methods and arbitrary config methods. Such members to be injected are detected through a Java 5 annotation: by default, Spring's @Autowired and @Value annotations.

This class handles the field injection, resolves the dependencies, and eventually calls the method doResolveDependency, is in this method where the 'priority' of the injection is resolved, springs checks if a sugested value is present which is normally an expression string, this sugested value is the content of the annotation Value, so in case is present a call to the class SimpleTypeConverter is made, otherwise spring looks for candicate beans and resolves the autowire.

Simply the reason @Autowired is ignored and @Value is used, is because the injection strategy of value is checked first. Obviously always has to be a priority, spring could also throw an exception when multiple conflicting annotations are used, but in this case is determined by that previous check to the sugested value.

I couldn't find anything related to this 'priority' is spring, but simple is because is not intended to use this annotations together, just as for instance, its not intended to use @Autowired and @Resource together either.


Why does @Value creates a new intance of the object

Previously I said that the class SimpleTypeConverter was called when the suggested value was present, the specific call is to the method convertIfNecessary, this is the one that performs the conversion of the string into the destination object, again this can be done with property editor or a custom converter, but none of those are used here. A SpEL expression isn't used either, just a string literal.

Spring checks first if the destination object is a string, or a collection/array (can convert e.g. comma delimited list), then checks if the destination is an enum, if it is, it tries to convert the string, if is not, and is not an interface but a class, it checks the existance of a Constructor(String) to finally create the object (not managed by spring). Basically this converter tries many different ways to convert the string to the final object.

This instantiation will only work using a string as argument, if you use for instance, a SpEL expression to return a long @Value("#{2L}"), and use an object with a Constructor(Long) it will throw an IllegalStateException with a similar message:

Cannot convert value of type 'java.lang.Long' to required type 'com.fiberg.test.springboot.object.Hut': no matching editors or conversion strategy found


Possible Solution

Using a simple @Configuration class as a supplier.

public class MyBean {
    public MyBean(String myArg) { /* ... */ }
    // ...
    @PostConstruct public init() { /* ... */ }
}

@Configuration
public class MyBeanSupplier {
    @Lazy
    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE, 
           proxyMode = ScopedProxyMode.NO)
    public MyBean getMyBean(String myArg) {
        return new MyBean(myArg);
    }
}

You could define MyBean as a static class in MyBeanSupplier class if its the only method it would have. Also you cannot use the proxy mode ScopedProxyMode.TARGET_CLASS, because you'll need to provide the arguments as beans and the arguments passed to getMyBean would be ignored.

With this approach you wouldn't be able to autowire the bean itself, but instead, you would autowire the supplier and then call the get method.

// ...
public class SomeBeanTest {
    @Autowired private MyBeanSupplier supplier;
    // ...
    public void setUp() throws Exception {
        someBean = supplier.getMyBean("2");
    }
}

You can also create the bean using the application context.

someBean = ctx.getBean(SomeBean.class, "2");

And the @PostConstruct method should be called no matter which one you use, but @PreDestroy is not called in prototype beans.

Edgar answered 10/3, 2018 at 2:34 Comment(2)
"Exactly when you use @Autowire()and Value(), autowired is ignored and value creates a new instance of the object not managed by spring." yes, seems like so. But is it a bug or expected behaviour?Attribute
I didn't find anything in the official doc about using both together, but their are used for totally diferent things, even though both perform an injection, Autowired is used for beans only, while Value uses configuration variables and spring's SpEL to initialize objects, is normally used with boxed primitives, primitives or with custom conversionEdgar
C
2

I read through the debug logs and stack trace for both scenarios many times and my observations are as follows:-

  1. When it goes to create the bean in case of @Autowire, it basically ends up injecting value to the constructor via using some converters. See screenshot below:-

converters are used

  1. The @Autowire is ineffective. So, in your code, if you even remove @Autowired it will still work. Hence, supporting #1 when @Value is used on property it basically created the Object.

Solution:-

You should be having a bean with name arg injected with whatever value you want. E.g. I preferred using configuration class(you could create the bean in context file) and did below:-

@Configuration
public class Configurations {

    @Bean
    public String arg() {
        return "20";
    }
}

Then test class would be as below (Note you could use change ContextConfiguration to use classpath to read context file ):-

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SomeBean.class, Configurations.class})
public class SomeBeanTest {

    @Autowired
    ApplicationContext ctx;

    @Autowired
    String arg;

    @Autowired
    private SomeBean someBean;

    private SomeBean someBean2;

    @Before
    public void setUp() throws Exception {
        someBean2 = ctx.getBean(SomeBean.class, "2");
    }

    @Test
    public void test() {
        System.out.println("\n\n\n\ntest" + someBean.getName());
    }
}

So, a learning for me as well to be careful with the usage of @Value as it could be misleading that it helped in autowiring by injecting value from some spring bean that got created in the background and could make applications misbehave.

Campanile answered 10/3, 2018 at 0:8 Comment(0)
B
0

When you run the test, a new bean for the test is created (i.e. not the SomeBean class, the SomeBeanTest class). @Value will be instantiated as a member value (not a bean) and thus default BeanPostProcessor (AutowiredAnnotationBeanPostProcessor) will not attempt initializing it.

To show that I have moved your System.out.println() to log.info() (keep lines in sync). Enabling debug-level logging shows:

DEBUG org.springframework.beans.factory.annotation.InjectionMetadata - Processing injected element of bean 'somepackage.SomeBeanTest': AutowiredFieldElement for org.springframework.context.ApplicationContext somepackage.SomeBeanTest.ctx

DEBUG org.springframework.core.annotation.AnnotationUtils - Failed to meta-introspect annotation [interface org.springframework.beans.factory.annotation.Autowired]: java.lang.NullPointerException

DEBUG org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'somepackage.SomeBeanTest' to bean named 'org.springframework.context.support.GenericApplicationContext@39c0f4a'

DEBUG org.springframework.beans.factory.annotation.InjectionMetadata - Processing injected element of bean 'somepackage.SomeBeanTest': AutowiredFieldElement for private somepackage.SomeBean somepackage.SomeBeanTest.someBean

DEBUG org.springframework.core.annotation.AnnotationUtils - Failed to meta-introspect annotation [interface org.springframework.beans.factory.annotation.Value]: java.lang.NullPointerException

DEBUG org.springframework.beans.BeanUtils - No property editor [somepackage.SomeBeanEditor] found for type somepackage.SomeBean according to 'Editor' suffix convention

INFO somepackage.SomeBean - Constructor called, arg: 0

DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test method: ....

INFO somepackage.SomeBeanTest - test

DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - After test method:

A way to workaround that works is to initialize the bean manually:

@Value("0")
private SomeBean someBean;

@PostConstruct
private void customInit() {
    log.info("Custom Init method called");
    someBean.init();
}

Which will produce:

INFO somepackage.SomeBean - Constructor called, arg: 0

INFO somepackage.SomeBeanTest - Custom Init method called

INFO somepackage.SomeBean - Post construct called

INFO somepackage.SomeBeanTest - test

Breathing answered 15/3, 2018 at 11:57 Comment(0)
S
-1

@Value does not do what you are expecting it to do. It cannot be used to supply a constructor arg to the bean being created.

See this SO Q&A: Spring Java Config: how do you create a prototype-scoped @Bean with runtime arguments?

Sato answered 9/3, 2018 at 19:35 Comment(1)
why does the output say "Constructor called, arg: 1" then? where is 1 coming if not from @Value?Attribute
L
-1

If I am not wrong :- Spring RULE Field injection happens after objects are constructed since obviously the container cannot set a property of something which doesn't exist. The field will be always unset in the constructor.

You are trying to print the injected value (or do some real initialization :)), Using PostConstruct:- in your code you have two beans. 1 SomeBean after constructor called the filed value is set . 2 SomeBean2 you are passing arg as value 2 that have been set in second bean you can use a method annotated with @PostConstruct, which will be executed after the injection process.

@RunWith(SpringRunner.class)
@ContextConfiguration("classpath*:applicationContext-test.xml")
public class SomeBeanTest {
@Autowired
ApplicationContext ctx;
@Autowired
@Value("1")
private SomeBean someBean;

private SomeBean someBean2;

@Before
public void setUp() throws Exception {
    someBean2 = ctx.getBean(SomeBean.class, "2");
}

@Test
public void test() {
    System.out.println("test");
}
}
Linchpin answered 13/3, 2018 at 10:9 Comment(3)
The field will be always unset in the constructor. Which field in which constructor are you referring to?Clerk
SomeBean(String arg) I am referring arg constructor which we are trying to set by value in 1st one and in second one we are passing 2.Linchpin
There's no field injection going on in the SomeBean bean.Clerk

© 2022 - 2024 — McMap. All rights reserved.