Spring Java Config: ¿cómo se crea un @Bean de prototipo con argumentos de tiempo de ejecución?


134

Usando Spring Config de Java, necesito adquirir / instanciar un bean de ámbito prototipo con argumentos de constructor que solo se pueden obtener en tiempo de ejecución. Considere el siguiente ejemplo de código (simplificado por brevedad):

@Autowired
private ApplicationContext appCtx;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = appCtx.getBean(Thing.class, name);

    //System.out.println(thing.getName()); //prints name
}

donde la clase Thing se define de la siguiente manera:

public class Thing {

    private final String name;

    @Autowired
    private SomeComponent someComponent;

    @Autowired
    private AnotherComponent anotherComponent;

    public Thing(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

El aviso namees final: solo se puede suministrar a través de un constructor y garantiza la inmutabilidad. Las otras dependencias son dependencias específicas de la implementación de la Thingclase, y no deben conocerse (estrechamente relacionadas) con la implementación del controlador de solicitudes.

Este código funciona perfectamente con la configuración Spring XML, por ejemplo:

<bean id="thing", class="com.whatever.Thing" scope="prototype">
    <!-- other post-instantiation properties omitted -->
</bean>

¿Cómo logro lo mismo con la configuración de Java? Lo siguiente no funciona con Spring 3.x:

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

Ahora, podría crear una Fábrica, por ejemplo:

public interface ThingFactory {
    public Thing createThing(String name);
}

Pero eso invalida el punto de usar Spring para reemplazar el patrón de diseño ServiceLocator y Factory , lo que sería ideal para este caso de uso.

Si Spring Java Config pudiera hacer esto, podría evitar:

  • definir una interfaz de fábrica
  • definir una implementación de Factory
  • escribir pruebas para la implementación de Factory

Eso es un montón de trabajo (relativamente hablando) para algo tan trivial que Spring ya admite a través de la configuración XML.


15
Excelente pregunta
Sotirios Delimanolis

Sin embargo, ¿hay alguna razón por la que no pueda crear una instancia de la clase usted mismo y tener que obtenerla de Spring? ¿Tiene dependencias de otros beans?
Sotirios Delimanolis

@SotiriosDelimanolis sí, la Thingimplementación es en realidad más compleja y tiene dependencias de otros beans (simplemente los omití por brevedad). Como tal, no quiero que la implementación del controlador de Solicitud sepa sobre ellos, ya que esto acoplaría estrechamente el controlador a las API / beans que no necesita. Actualizaré la pregunta para reflejar su pregunta (excelente).
Les Hazlewood

No estoy seguro de si Spring permite esto en un constructor, pero sé que puedes poner @Qualifierparámetros a un setter @Autowireden el setter.
CodeChimp

2
En la primavera 4, su ejemplo con @Beanobras. Se @Beanllama al método con los argumentos apropiados a los que pasó getBean(..).
Sotirios Delimanolis

Respuestas:


94

En una @Configurationclase, un @Beanmétodo así

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

se utiliza para registrar una definición de bean y proporcionar la fábrica para crear el bean . El bean que define solo se instancia mediante solicitud utilizando argumentos que se determinan directamente o escaneando eso ApplicationContext.

En el caso de un prototypebean, se crea un nuevo objeto cada vez y, por lo tanto, @Beantambién se ejecuta el método correspondiente .

Puede recuperar un bean a ApplicationContexttravés de su BeanFactory#getBean(String name, Object... args)método que establece

Permite especificar argumentos explícitos del constructor / argumentos del método de fábrica, anulando los argumentos predeterminados especificados (si los hay) en la definición del bean.

Parámetros:

args argumentos a utilizar si la creación de un prototipo utilizando argumentos explícitos a un método de fábrica estática. No es válido utilizar un valor de args no nulo en ningún otro caso.

En otras palabras, para este prototypebean de ámbito, está proporcionando los argumentos que se utilizarán, no en el constructor de la clase de bean, sino en la @Beaninvocación del método.

Esto es al menos cierto para las versiones Spring 4+.


12
Mi problema con este enfoque es que no puede limitar el @Beanmétodo a la invocación manual. Si alguna vez se llamará @Autowire Thingal @Beanmétodo, probablemente muera al no poder inyectar el parámetro. Lo mismo si tu @Autowire List<Thing>. Encontré esto un poco frágil.
Jan Zyka

@ JanZyka, ¿hay alguna forma en que pueda conectar automáticamente algo más que lo que se describe en estas respuestas (que son esencialmente lo mismo si entorna los ojos). Más específicamente, si conozco los argumentos por adelantado (en el momento de la compilación / configuración), ¿hay alguna forma de expresar estos argumentos en alguna anotación con la que pueda calificar @Autowire?
M. Prokhorov

52

Con Spring> 4.0 y Java 8 puedes hacer esto de forma más segura:

@Configuration    
public class ServiceConfig {

    @Bean
    public Function<String, Thing> thingFactory() {
        return name -> thing(name); // or this::thing
    } 

    @Bean
    @Scope(value = "prototype")
    public Thing thing(String name) {
       return new Thing(name);
    }

}

Uso:

@Autowired
private Function<String, Thing> thingFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = thingFactory.apply(name);

    // ...
}

Así que ahora puedes obtener tu bean en tiempo de ejecución. Este es un patrón de fábrica, por supuesto, pero puede ahorrar algo de tiempo escribiendo clases específicas como ThingFactory(sin embargo, tendrá que escribir personalizado @FunctionalInterfacepara pasar más de dos parámetros).


1
Este enfoque me parece muy útil y limpio. ¡Gracias!
Alex Objelean

1
¿Qué es una tela? Entiendo su uso ... pero no la terminología ... no creo haber oído hablar del "patrón de la tela"
AnthonyJClink

1
@AnthonyJClink Supongo que acabo de usar en fabriclugar de factorymi mal :)
Roman Golyshev

1
@AbhijitSarkar oh, ya veo. Pero no puedes pasar un parámetro a un Providero a ObjectFactory, ¿o me equivoco? Y en mi ejemplo, puede pasarle un parámetro de cadena (o cualquier parámetro)
Roman Golyshev

2
Si no desea (o no necesita) utilizar los métodos del ciclo de vida de Spring bean (que son diferentes para los beans prototipo ...), puede omitir @Beany hacer Scopeanotaciones sobre el Thing thingmétodo. Además, este método puede hacerse privado para ocultarse y dejar solo la fábrica.
m52509791

17

Desde la primavera 4.3, hay una nueva forma de hacerlo, que fue cosida para ese tema.

ObjectProvider : le permite simplemente agregarlo como una dependencia a su bean de alcance Prototype "discutido" e instanciarlo utilizando el argumento.

Aquí hay un ejemplo simple de cómo usarlo:

@Configuration
public class MyConf {
    @Bean
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public MyPrototype createPrototype(String arg) {
        return new MyPrototype(arg);
    }
}

public class MyPrototype {
    private String arg;

    public MyPrototype(String arg) {
        this.arg = arg;
    }

    public void action() {
        System.out.println(arg);
    }
}


@Component
public class UsingMyPrototype {
    private ObjectProvider<MyPrototype> myPrototypeProvider;

    @Autowired
    public UsingMyPrototype(ObjectProvider<MyPrototype> myPrototypeProvider) {
        this.myPrototypeProvider = myPrototypeProvider;
    }

    public void usePrototype() {
        final MyPrototype myPrototype = myPrototypeProvider.getObject("hello");
        myPrototype.action();
    }
}

Por supuesto, esto imprimirá una cadena de saludo cuando llame a usePrototype.


15

ACTUALIZADO por comentario

Primero, no estoy seguro de por qué dices "esto no funciona" para algo que funciona bien en Spring 3.x. Sospecho que algo debe estar mal en su configuración en alguna parte.

Esto funciona:

-- Archivo de configuración:

@Configuration
public class ServiceConfig {
    // only here to demo execution order
    private int count = 1;

    @Bean
    @Scope(value = "prototype")
    public TransferService myFirstService(String param) {
       System.out.println("value of count:" + count++);
       return new TransferServiceImpl(aSingletonBean(), param);
    }

    @Bean
    public AccountRepository aSingletonBean() {
        System.out.println("value of count:" + count++);
        return new InMemoryAccountRepository();
    }
}

- Archivo de prueba para ejecutar:

@Test
public void prototypeTest() {
    // create the spring container using the ServiceConfig @Configuration class
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class);
    Object singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One");
    System.out.println(transferService.toString());
    transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two");
    System.out.println(transferService.toString());
}

Usando Spring 3.2.8 y Java 7, da este resultado:

value of count:1
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
value of count:2
Using name value of: simulated Dynamic Parameter One
com.spring3demo.account.service.TransferServiceImpl@634d6f2c
value of count:3
Using name value of: simulated Dynamic Parameter Two
com.spring3demo.account.service.TransferServiceImpl@70bde4a2

Por eso se solicita el frijol 'Singleton' dos veces. Sin embargo, como era de esperar, Spring solo lo crea una vez. La segunda vez ve que tiene ese bean y simplemente devuelve el objeto existente. El constructor (método @Bean) no se invoca por segunda vez. En deferencia a esto, cuando se solicita el Bean 'Prototype' del mismo objeto de contexto dos veces, vemos que la referencia cambia en la salida Y que el constructor (método @Bean) se invoca dos veces.

Entonces, la pregunta es cómo inyectar un singleton en un prototipo. ¡La clase de configuración anterior muestra cómo hacerlo también! Debería pasar todas esas referencias al constructor. Esto permitirá que la clase creada sea un POJO puro, así como hacer que los objetos de referencia contenidos sean inmutables como deberían ser. Por lo tanto, el servicio de transferencia podría verse así:

public class TransferServiceImpl implements TransferService {

    private final String name;

    private final AccountRepository accountRepository;

    public TransferServiceImpl(AccountRepository accountRepository, String name) {
        this.name = name;
        // system out here is only because this is a dumb test usage
        System.out.println("Using name value of: " + this.name);

        this.accountRepository = accountRepository;
    }
    ....
}

Si escribe Pruebas unitarias, estará tan feliz de haber creado las clases sin todo @Autowired. Si necesita componentes con conexión automática, mantenga los locales en los archivos de configuración de Java.

Esto llamará al siguiente método en BeanFactory. Observe en la descripción cómo está destinado para su caso de uso exacto.

/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * <p>Allows for specifying explicit constructor arguments / factory method arguments,
 * overriding the specified default arguments (if any) in the bean definition.
 * @param name the name of the bean to retrieve
 * @param args arguments to use if creating a prototype using explicit arguments to a
 * static factory method. It is invalid to use a non-null args value in any other case.
 * @return an instance of the bean
 * @throws NoSuchBeanDefinitionException if there is no such bean definition
 * @throws BeanDefinitionStoreException if arguments have been given but
 * the affected bean isn't a prototype
 * @throws BeansException if the bean could not be created
 * @since 2.5
 */
Object getBean(String name, Object... args) throws BeansException;

3
¡Gracias por la respuesta! Sin embargo, creo que entendiste mal la pregunta. La parte más importante de la pregunta es que se debe proporcionar un valor de tiempo de ejecución como argumento de constructor al adquirir (instanciar) el prototipo.
Les Hazlewood

Actualicé mi respuesta. En realidad, parecía que el manejo del valor de tiempo de ejecución se realizó correctamente, así que dejé esa parte fuera. Sin embargo, es explícitamente compatible, como puede ver en las actualizaciones y resultados del programa.
JoeG

0

Puede lograr un efecto similar simplemente usando una clase interna :

@Component
class ThingFactory {
    private final SomeBean someBean;

    ThingFactory(SomeBean someBean) {
        this.someBean = someBean;
    }

    Thing getInstance(String name) {
        return new Thing(name);
    }

    class Thing {
        private final String name;

        Thing(String name) {
            this.name = name;
        }

        void foo() {
            System.out.format("My name is %s and I can " +
                    "access bean from outer class %s", name, someBean);
        }
    }
}


-1

Respuesta tardía con un enfoque ligeramente diferente. Es un seguimiento de esta pregunta reciente. que hace referencia a esta pregunta en sí.

Sí, como se dijo, puede declarar el bean prototipo que acepta un parámetro en una @Configurationclase que permite crear un nuevo bean en cada inyección.
Eso hará de esta @Configuration clase una fábrica y, para no darle demasiadas responsabilidades a esta fábrica, esto no debe incluir otros granos.

@Configuration    
public class ServiceFactory {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Thing thing(String name) {
       return new Thing(name);
   }

}

Pero también puede inyectar ese bean de configuración para crear Things:

@Autowired
private ServiceFactory serviceFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = serviceFactory.thing(name); // create a new bean at each invocation
    // ...    
}

Es a la vez seguro de tipo y conciso.


1
Gracias por la respuesta, pero este es un antipatrón de primavera. Los objetos de configuración no deben 'filtrarse' en el código de la aplicación; existen para configurar el gráfico de objetos de la aplicación y la interfaz con las construcciones Spring. Esto es similar a las clases XML en los beans de su aplicación (es decir, otro mecanismo de configuración). Es decir, si Spring viene con otro mecanismo de configuración, tendría que refactorizar el código de su aplicación, un claro indicador que viola la separación de preocupaciones. Es mejor que su Config cree instancias de una interfaz Factory / Function e inyecte Factory, sin un acoplamiento estrecho a la configuración.
Les Hazlewood

1) Estoy completamente de acuerdo en que, en el caso general, los objetos de configuración no tienen que filtrarse como un campo. Pero en este caso específico, inyectando un objeto de configuración que define uno y solo un bean para producir beans prototipo, IHMO tiene mucho sentido: esta clase de configuración se convierte en una fábrica. ¿Dónde está el problema de la separación de preocupaciones si solo hace eso? ...
davidxxx

... 2) Acerca de "Es decir, si Spring viene con otro mecanismo de configuración", es un argumento incorrecto porque cuando decides usar un marco en tu aplicación, acoplas tu aplicación con eso. En cualquier caso, también tendrá que refactorizar cualquier aplicación Spring en la que se base @Configurationsi ese mecanismo cambió.
davidxxx

1
... 3) La respuesta que aceptó propone usar BeanFactory#getBean(). Pero eso es mucho peor en términos de acoplamiento, ya que es una fábrica que permite obtener / instanciar cualquier bean de la aplicación y no solo cuál necesita el bean actual. Con tal uso, puede mezclar las responsabilidades de su clase muy fácilmente, ya que las dependencias que puede obtener son ilimitadas, lo que realmente no es aconsejable, pero es un caso excepcional.
davidxxx

@ davidxxx: acepté la respuesta hace años, antes de que JDK 8 y Spring 4 fueran de facto. La respuesta de Roman anterior es más correcta para los usos modernos de Spring. Con respecto a su declaración "porque cuando decide usar un marco en su aplicación, combina su aplicación con eso" es bastante antitético a las recomendaciones del equipo de Spring y las mejores prácticas de Java Config: pregunte a Josh Long o Jeurgen Hoeller si obtiene un posibilidad de hablar con ellos en persona (sí, y puedo asegurarles que aconsejan explícitamente no acoplar el código de su aplicación a Spring siempre que sea posible). Salud.
Les Hazlewood
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.