简体   繁体   English

当使用构造函数参数自动装配原型bean时,为什么不调用@PostConstruct方法

[英]Why @PostConstruct method is not called when autowiring prototype bean with constructor argument

I have a prototype-scope bean, which I want to be injected by @Autowired annotation. 我有一个原型范围bean,我希望通过@Autowired注释注入它。 In this bean, there is also @PostConstruct method which is not called by Spring and I don't understand why. 在这个bean中,还有@PostConstruct方法,Spring没有调用它,我不明白为什么。

My bean definition: 我的bean定义:

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: 我想要注入bean的JUnit类:

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. 当我通过从ApplicationContext调用getBean来初始化bean时,一切都按预期工作。 My question is why injecting bean by @Autowire and @Value combination is not calling @PostConstruct method 我的问题是为什么@Autowire@Value组合注入bean不会调用@PostConstruct方法

Why is @Value used instead of @Autowired? 为什么使用@Value代替@Autowired?

The @Value annotation is used to inject values and normally has as destination strings, primitives, boxed types and java collections. @Value注释用于注入值,通常具有目标字符串,基元,盒装类型和Java集合。

Acording to Spring's documentation : 根据Spring的文档

The @Value annotation can be placed on fields, methods and method/constructor parameters to specify a default value. @Value注释可以放在字段,方法和方法/构造函数参数上,以指定默认值。

Value receives a string expression which is used by spring to handle the conversion to the destination object. Value接收一个字符串表达式,spring用它来处理到目标对象的转换。 This conversion can be through the Spring's type conversion , the java bean property editor , and the Spring's SpEL expresions . 这种转换可以通过Spring的类型转换java bean属性编辑器Spring的SpEL表达式进行 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). 原则上,此转换的结果对象不受spring管理(即使您可以从任何此方法返回已管理的bean)。

By the other hand, the AutowiredAnnotationBeanPostProcessor is a 另一方面, AutowiredAnnotationBeanPostProcessor是一个

BeanPostProcessor implementation that autowires annotated fields, setter methods and arbitrary config methods. BeanPostProcessor实现,自动装配带注释的字段,setter方法和任意配置方法。 Such members to be injected are detected through a Java 5 annotation: by default, Spring's @Autowired and @Value annotations. 要注入的这些成员是通过Java 5注释检测的:默认情况下,Spring的@Autowired和@Value注释。

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. 这个类处理字段注入,解析依赖关系,并最终调用方法doResolveDependency ,在这个方法中解决了注入的'priority',spring检查是否存在一个通常是表达式字符串的sugested值,这个sugested value是注释Value的内容,因此如果存在,则调用类SimpleTypeConverter ,否则spring会查找candicate bean并解析autowire。

Simply the reason @Autowired is ignored and @Value is used, is because the injection strategy of value is checked first. 简单地忽略@Autowired并使用@Value的原因是因为首先检查值的注入策略。 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. 显然总是必须优先考虑,当使用多个冲突注释时,spring也会抛出异常,但在这种情况下,由之前检查的sugested值决定。

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. 我找不到与此'priority'相关的任何内容是spring,但简单的是因为并不打算将这个注释一起使用,例如,它不打算同时使用@Autowired@Resource


Why does @Value creates a new intance of the object 为什么@Value会创建对象的新内容

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. 以前我说过,当建议的值存在时调用类SimpleTypeConverter ,具体调用是convertIfNecessary方法,这是执行将字符串转换为目标对象的方法,同样可以使用属性编辑器或自定义转换器,但这里没有使用它们。 A SpEL expression isn't used either, just a string literal. 也不使用SpEL表达式,只是字符串文字。

Spring checks first if the destination object is a string, or a collection/array (can convert eg 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). Spring首先检查目标对象是字符串,还是集合/数组(可以转换为逗号分隔列表),然后检查目标是否为枚举,如果是,则尝试转换字符串,如果不是,则它不是一个接口而是一个类,它检查Constructor(String)的存在以最终创建对象(不由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: 这个实例化只能使用字符串作为参数,如果你使用例如SpEL表达式返回一个长@Value("#{2L}") ,并使用带有Constructor(Long)的对象,它将抛出IllegalStateException有类似的消息:

Cannot convert value of type 'java.lang.Long' to required type 'com.fiberg.test.springboot.object.Hut': no matching editors or conversion strategy found 无法将'java.lang.Long'类型的值转换为必需类型'com.fiberg.test.springboot.object.Hut':找不到匹配的编辑器或转换策略


Possible Solution 可能解决方案

Using a simple @Configuration class as a supplier. 使用简单的@Configuration类作为供应商。

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. 您可以将MyBean定义为MyBeanSupplier类中的静态类,如果它是唯一的方法。 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. 此外,您不能使用代理模式ScopedProxyMode.TARGET_CLASS,因为您需要提供参数作为bean,并且将忽略传递给getMyBean的参数。

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. 使用这种方法,您将无法自动装配bean本身,而是通过自动装配供应商然后调用get方法。

// ...
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. 您还可以使用应用程序上下文创建bean。

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 . 无论你使用哪个@PostConstruct方法都应该被调用,但是在原型bean中不调用 @PreDestroy

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. 当它在@Autowire情况下创建bean时,它基本上最终通过使用一些转换器向构造函数注入值。 See screenshot below:- 见下面的截图: -

使用转换器

  1. The @Autowire is ineffective. @Autowire无效。 So, in your code, if you even remove @Autowired it will still work. 因此,在您的代码中,如果您甚至删除@Autowired它仍然可以工作。 Hence, supporting #1 when @Value is used on property it basically created the Object. 因此,当在属性上使用@Value时支持#1它基本上创建了Object。

Solution :- 解决方案 : -

You should be having a bean with name arg injected with whatever value you want. 你应该有一个名为arg的bean注入你想要的任何值。 Eg I preferred using configuration class(you could create the bean in context file) and did below:- 例如,我更喜欢使用配置类(您可以在上下文文件中创建bean)并执行以下操作: -

@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 ):- 然后测试类如下(注意你可以使用更改ContextConfiguration来使用类路径来读取上下文文件): -

@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. 因此,对我来说也要小心使用@Value因为它可能会误导它通过从后台创建的一些spring bean中注入价值来帮助自动装配,并且可能会使应用程序行为异常。

When you run the test, a new bean for the test is created (ie not the SomeBean class, the SomeBeanTest class). 运行测试时,会创建一个用于测试的新bean(即不是SomeBean类,SomeBeanTest类)。 @Value will be instantiated as a member value (not a bean) and thus default BeanPostProcessor (AutowiredAnnotationBeanPostProcessor) will not attempt initializing it. @Value将被实例化为成员值(而不是bean),因此默认的BeanPostProcessor(AutowiredAnnotationBeanPostProcessor)不会尝试初始化它。

To show that I have moved your System.out.println() to log.info() (keep lines in sync). 为了表明我已将System.out.println()移动到log.info()(保持行同步)。 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.beans.factory.annotation.InjectionMetadata - 处理bean的注入元素'somepackage.SomeBeanTest':org.springframework.context.ApplicationContext的autowiredFieldElement 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.core.annotation.AnnotationUtils - 无法进行meta-introspect注释[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.AutowiredAnnotationBeanPostProcessor - 从bean名称'somepackage.SomeBeanTest'按类型自动装配到名为'org.springframework.context.support.GenericApplicationContext@39c0f4a'的bean

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.beans.factory.annotation.InjectionMetadata - 处理bean的注入元素'somepackage.SomeBeanTest':私有somepackage的AutowiredFieldElement.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.core.annotation.AnnotationUtils - 无法进行meta-introspect注释[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 DEBUG org.springframework.beans.BeanUtils - 根据'Editor'后缀约定找不到类型somepackage.SomeBean的属性编辑器[somepackage.SomeBeanEditor]

INFO somepackage.SomeBean - Constructor called, arg: 0 INFO somepackage.SomeBean - 名为arg:0的构造函数

DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test method: .... DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - 在测试方法之前:....

INFO somepackage.SomeBeanTest - test INFO somepackage.SomeBeanTest - 测试

DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - After test method: DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - 测试方法后:

A way to workaround that works is to initialize the bean manually: 一种解决方法是手动初始化bean:

@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.SomeBean - 名为arg:0的构造函数

INFO somepackage.SomeBeanTest - Custom Init method called INFO somepackage.SomeBeanTest - 调用自定义Init方法

INFO somepackage.SomeBean - Post construct called INFO somepackage.SomeBean - Post构造调用

INFO somepackage.SomeBeanTest - test INFO somepackage.SomeBeanTest - 测试

@Value does not do what you are expecting it to do. @Value没有做你期望它做的事情。 It cannot be used to supply a constructor arg to the bean being created. 它不能用于向正在创建的bean提供构造函数arg。

See this SO Q&A: Spring Java Config: how do you create a prototype-scoped @Bean with runtime arguments? 请参阅此问答: Spring Java Config:如何使用运行时参数创建原型范围的@Bean?

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. 如果我没有错: - Spring RULE字段注入在构造对象之后发生,因为显然容器不能设置不存在的东西的属性。 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. 您正在尝试打印注入的值(或进行一些真正的初始化:)),使用PostConstruct: - 在您的代码中,您有两个bean。 1 SomeBean after constructor called the filed value is set . 1构造函数调用后的SomeBean设置了字段值。 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. 2 SomeBean2您传递的arg为值2,已在第二个bean中设置,您可以使用@PostConstruct注释的方法,该方法将在注入过程后执行。

@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");
}
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM