java - ioc - Por qué no se llama al método @PostConstruct cuando autowiring prototype bean con un argumento constructor
spring init annotation (5)
@Value no hace lo que usted espera que haga. No se puede utilizar para proporcionar un argumento de constructor al bean que se está creando.
Consulte este SO Q&A: Spring Java Config: ¿cómo crear un @Bean de ámbito prototipo con argumentos de tiempo de ejecución?
Tengo un bean de alcance de prototipo, que quiero que se inyecte mediante la anotación @Autowired. En este bean, también hay un método @PostConstruct que no es llamado por Spring y no entiendo por qué.
Mi definición de frijol:
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");
}
}
Clase de JUnit donde quiero inyectar frijol:
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");
}
}
Configuración de la primavera:
<?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>
La salida de la ejecución:
Constructor called, arg: 1
Constructor called, arg: 2
Post construct called
test
Cuando inicializo el bean llamando a getBean
desde ApplicationContext
todo funciona como se esperaba. Mi pregunta es por qué inyectar bean mediante la combinación @Autowire
y @Value
no está llamando @PostConstruct
método @PostConstruct
Cuando ejecuta la prueba, se crea un nuevo bean para la prueba (es decir, no la clase SomeBean, la clase SomeBeanTest). @Value se creará como un valor miembro (no un bean) y, por lo tanto, BeanPostProcessor (AutowiredAnnotationBeanPostProcessor) no intentará inicializarlo.
Para mostrar que he movido su System.out.println () a log.info () (mantenga las líneas sincronizadas). Habilitar el registro de depuración muestra:
DEBUG org.springframework.beans.factory.annotation.InjectionMetadata - Procesando el elemento inyectado del bean ''somepackage.SomeBeanTest'': AutowiredFieldElement para org.springframework.context.ApplicationContext somepackage.SomeBeanTest.ctx
DEBUG org.springframework.core.annotation.AnnotationUtils - Falló la anotación de meta-introspección [interfaz org.springframework.beans.factory.annotation.Autowired]: java.lang.NullPointerException
DEBUG org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowiring por tipo desde el nombre de bean ''somepackage.SomeBeanTest'' para bean denominado ''org.springframework.context.support.GenericApplicationContext@39cffa.
DEBUG org.springframework.beans.factory.annotation.InjectionMetadata - Procesando el elemento inyectado del frijol ''somepackage.SomeBeanTest'': AutowiredFieldElement para el somepackage privado.SomeBean somepackage.SomeBeanTest.someBean
DEBUG org.springframework.core.annotation.AnnotationUtils - Fallo en la anotación de meta-introspección [interfaz org.springframework.beans.factory.annotation.Value]: java.lang.NullPointerException
DEBUG org.springframework.beans.BeanUtils: no se ha encontrado ningún editor de propiedades [somepackage.SomeBeanEditor] para el tipo somepackage.SomeBean según la convención de sufijo ''Editor''
INFO somepackage.SomeBean - Constructor llamado, arg: 0
DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Antes del método de prueba: ....
INFO somepackage.SomeBeanTest - prueba
DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Después del método de prueba:
Una forma de solucionar el problema es inicializar el bean manualmente:
@Value("0")
private SomeBean someBean;
@PostConstruct
private void customInit() {
log.info("Custom Init method called");
someBean.init();
}
Que producirá:
INFO somepackage.SomeBean - Constructor llamado, arg: 0
INFO somepackage.SomeBeanTest - Método de inicio personalizado llamado
INFO somepackage.SomeBean - Post constructo llamado
INFO somepackage.SomeBeanTest - prueba
Leí los registros de depuración y el seguimiento de la pila para ambos escenarios muchas veces y mis observaciones son las siguientes:
- Cuando se va a crear el bean en el caso de
@Autowire
, básicamente termina por inyectar valor al constructor utilizando algunos convertidores. Vea la captura de pantalla a continuación:
- El @Autowire es ineficaz. Por lo tanto, en su código, si incluso elimina
@Autowired
, aún funcionará. Por lo tanto, el soporte # 1 cuando se usa @Value en la propiedad, básicamente creó el Objeto.
Solución : -
Debería tener un bean con el nombre arg
inyectado con el valor que desee. Por ejemplo, prefiero usar la clase de configuración (podría crear el bean en el archivo de contexto) y lo hice a continuación:
@Configuration
public class Configurations {
@Bean
public String arg() {
return "20";
}
}
Entonces la clase de prueba sería la siguiente (tenga en cuenta que podría usar el cambio de ContextConfiguration
de ContextConfiguration
para usar classpath para leer el archivo de contexto):
@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());
}
}
Por lo tanto, un aprendizaje para mí también para tener cuidado con el uso de @Value
ya que podría inducir a error que ayude a autowiring al inyectar el valor de un bean spring que se creó en segundo plano y podría hacer que las aplicaciones se comporten mal.
¿Por qué se usa @Value en lugar de @Autowired?
La anotación @Value
se usa para inyectar valores y normalmente tiene como cadenas de destino, primitivas, tipos de caja y colecciones Java.
Según la documentación de Spring :
La anotación @Value se puede colocar en los campos, métodos y parámetros del método / constructor para especificar un valor predeterminado.
Value
recibe una expresión de cadena que Spring utiliza para manejar la conversión al objeto de destino. Esta conversión puede realizarse a través de la conversión de tipos de Spring , el editor de propiedades de java y las expresiones SpEL de Spring . El objeto resultante de esta conversión, en principio, no se administra mediante Spring (aunque puede devolver un bean ya administrado desde cualquiera de estos métodos).
Por otro lado, el AutowiredAnnotationBeanPostProcessor es un
Implementación de BeanPostProcessor que autoriza campos anotados, métodos de establecimiento y métodos de configuración arbitrarios. Los miembros a inyectar se detectan mediante una anotación de Java 5: de forma predeterminada, las anotaciones @Autowired y @Value de Spring.
Esta clase maneja la inyección de campo, resuelve las dependencias y, finalmente, llama al método doResolveDependency , es en este método donde se resuelve la ''prioridad'' de la inyección, comprueba si hay un valor sugerido que normalmente es una cadena de expresión, esto sugerido value es el contenido de la anotación Value
, por lo que en caso de que esté presente, se realiza una llamada a la clase SimpleTypeConverter , de lo contrario Spring busca frijoles candicate y resuelve el autowire.
Simplemente la razón @Autowired
se ignora @Value
y se usa @Value
, es porque la estrategia de inyección de valor se verifica primero. Obviamente, siempre tiene que ser una prioridad, Spring también puede lanzar una excepción cuando se usan múltiples anotaciones en conflicto, pero en este caso está determinada por esa verificación previa al valor sugerido.
No pude encontrar nada relacionado con esta ''prioridad'' es primavera, pero simple es porque no pretende usar estas anotaciones juntas, como por ejemplo, tampoco pretende usar @Autowired
y @Resource
juntos.
¿Por qué @Value crea una nueva intención del objeto?
Anteriormente dije que se llamaba a la clase SimpleTypeConverter
cuando estaba presente el valor sugerido, la llamada específica es al método convertIfNecessary , este es el que realiza la conversión de la cadena en el objeto de destino, de nuevo, esto se puede hacer con el editor de propiedades o un convertidor personalizado, pero ninguno de ellos se utilizan aquí. Una expresión SpEL tampoco se usa, solo una cadena literal.
Spring comprueba primero si el objeto de destino es una cadena o una colección / matriz (puede convertir, por ejemplo, una lista delimitada por comas), luego verifica si el destino es una enumeración, si lo es, intenta convertir la cadena, si no lo es, y no es una interfaz sino una clase, verifica la existencia de un Constructor(String)
para crear finalmente el objeto (no administrado por Spring). Básicamente, este convertidor intenta muchas formas diferentes de convertir la cadena al objeto final.
Esta creación de instancias solo funcionará usando una cadena como argumento, si usa, por ejemplo, una expresión SpEL para devolver un @Value("#{2L}")
largo @Value("#{2L}")
, y usa un objeto con un Constructor(Long)
, lanzará una IllegalStateException
con un mensaje similar:
No se puede convertir el valor del tipo ''java.lang.Long'' en el tipo requerido ''com.fiberg.test.springboot.object.Hut'': no se encontraron editores que coincidan ni una estrategia de conversión
Solución posible
Usando una clase simple @Configuration como proveedor.
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);
}
}
Podría definir MyBean como una clase estática en la clase MyBeanSupplier si es el único método que tendría. Además, no puede usar el modo proxy ScopedProxyMode.TARGET_CLASS, porque deberá proporcionar los argumentos como beans y los argumentos pasados a getMyBean
se ignorarán.
Con este enfoque, no podría auto cablear el frijol en sí mismo, sino que, por el contrario, podría autoalimentar al proveedor y luego llamar al método obtener.
// ...
public class SomeBeanTest {
@Autowired private MyBeanSupplier supplier;
// ...
public void setUp() throws Exception {
someBean = supplier.getMyBean("2");
}
}
También puede crear el bean utilizando el contexto de la aplicación.
someBean = ctx.getBean(SomeBean.class, "2");
Y el método @PostConstruct
debe llamarse sin importar cuál use, pero no se llama a @PreDestroy
en los prototipos de beans .
Si no estoy equivocado: - La inyección de campo de REGLA de primavera ocurre después de que se construyen los objetos, ya que obviamente el contenedor no puede establecer una propiedad de algo que no existe. El campo siempre estará desarmado en el constructor.
Está intentando imprimir el valor inyectado (o hacer una inicialización real :)), utilizando PostConstruct: - en su código tiene dos beans. 1 SomeBean después de que el constructor llamado el valor archivado se establezca. 2 SomeBean2 le está pasando arg como valor 2 que se ha establecido en el segundo bean , puede usar un método anotado con @PostConstruct, que se ejecutará después del proceso de inyección.
@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");
}
}