Spring Java Config: como você cria um @Bean com protótipo com argumentos de tempo de execução?

134

Usando o Java Config da Spring, preciso adquirir / instanciar um bean com escopo de protótipo com argumentos de construtor que só podem ser obtidos em tempo de execução. Considere o seguinte exemplo de código (simplificado por questões de brevidade):

@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
}

onde a classe Thing é definida da seguinte maneira:

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;
    }
}

O aviso nameé final: ele só pode ser fornecido por meio de um construtor e garante imutabilidade. As outras dependências são dependências específicas da implementação da Thingclasse e não devem ser conhecidas (fortemente acopladas) à implementação do manipulador de solicitações.

Este código funciona perfeitamente bem com a configuração XML da Spring, por exemplo:

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

Como faço para conseguir a mesma coisa com a configuração Java? O seguinte não funciona com o Spring 3.x:

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

Agora, eu poderia criar uma fábrica, por exemplo:

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

Mas isso anula todo o sentido de usar o Spring para substituir o padrão de design ServiceLocator e Factory , o que seria ideal para esse caso de uso.

Se o Spring Java Config pudesse fazer isso, eu seria capaz de evitar:

  • definindo uma interface de fábrica
  • definindo uma implementação de fábrica
  • escrevendo testes para a implementação da fábrica

Isso é muito trabalho (relativamente falando) para algo tão trivial que o Spring já suporta via configuração XML.

Les Hazlewood
fonte
15
Excelente pergunta.
Sotirios Delimanolis
No entanto, existe uma razão para você não poder instanciar a classe sozinho e ter que obtê-la da Spring? Tem dependências em outros beans?
Sotirios Delimanolis
@SotiriosDelimanolis sim, a Thingimplementação é realmente mais complexa e tem dependências de outros beans (eu os omiti por brevidade). Como tal, não quero que a implementação do manipulador de solicitações saiba sobre eles, pois isso acopla fortemente o manipulador às APIs / beans de que não precisa. Vou atualizar a pergunta para refletir sua (excelente) pergunta.
precisa
Não tenho certeza se o Spring permite isso em um construtor, mas sei que você pode colocar @Qualifierparâmetros em um setter @Autowiredno próprio setter.
CodeChimp
2
Na primavera 4, seu exemplo com @Beanobras. O @Beanmétodo é chamado com os argumentos apropriados aos quais você passou getBean(..).
Sotirios Delimanolis

Respostas:

94

Em uma @Configurationclasse, um @Beanmétodo como esse

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

é usado para registrar uma definição de bean e fornecer a fábrica para a criação do bean . O bean que ele define é instanciado somente mediante solicitação, usando argumentos que são determinados diretamente ou através da varredura ApplicationContext.

No caso de um prototypebean, um novo objeto é criado sempre e, portanto, o @Beanmétodo correspondente também é executado.

Você pode recuperar um bean ApplicationContextatravés do BeanFactory#getBean(String name, Object... args)método que afirma

Permite especificar argumentos explícitos do construtor / argumentos do método de fábrica, substituindo os argumentos padrão especificados (se houver) na definição do bean.

Parâmetros:

argumentos args a serem usados ​​ao criar um protótipo usando argumentos explícitos para um método de fábrica estático. É inválido usar um valor args não nulo em qualquer outro caso.

Em outras palavras, para este prototypebean com escopo definido, você está fornecendo os argumentos que serão utilizados, não no construtor da classe de bean, mas na @Beaninvocação do método.

Isso é verdade pelo menos para as versões Spring 4+.

Sotirios Delimanolis
fonte
12
Meu problema com essa abordagem é que você não pode limitar o @Beanmétodo à chamada manual. Se você alguma vez @Autowire Thingo @Beanmétodo for chamado, provavelmente morrendo por não conseguir injetar o parâmetro. Mesmo se você @Autowire List<Thing>. Achei isso um pouco frágil.
Jan Zyka
@ JanZyka, existe alguma maneira de eu autorizar algo que não esteja descrito nestas respostas (que são essencialmente as mesmas se você for apertar os olhos). Mais especificamente, se conheço argumentos antecipadamente (no momento da compilação / configuração), há alguma maneira de expressar esses argumentos em alguma anotação com a qual posso qualificar @Autowire?
M. Prokhorov
52

Com Spring> 4.0 e Java 8, você pode fazer isso com mais segurança:

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

    // ...
}

Então agora você pode obter seu bean em tempo de execução. É claro que esse é um padrão de fábrica, mas você pode economizar algum tempo escrevendo classes específicas como ThingFactory(no entanto, você precisará escrever personalizado @FunctionalInterfacepara passar mais de dois parâmetros).

Roman Golyshev
fonte
1
Acho essa abordagem muito útil e limpa. Obrigado!
Alex Objelean
1
O que é um tecido? Eu entendo o seu uso .. mas não a terminologia .. não acho que eu já ouvi do "padrão de tecido"
AnthonyJClink
1
@AnthonyJClink Acho que eu só utilizado fabricem vez de factory, meu mau :)
Roman Golyshev
1
@AbhijitSarkar oh, entendo. Mas você não pode passar um parâmetro para um Providerou para um ObjectFactory, ou estou errado? E no meu exemplo, você pode passar um parâmetro de cadeia para ele (ou de qualquer parâmetro)
Roman Golyshev
2
Se você não quiser (ou não precisar) usar os métodos de ciclo de vida do bean Spring (que são diferentes para protótipo de beans ...), poderá pular @Beane Scopeanotações sobre o Thing thingmétodo. Além disso, este método pode ser tornado privado para se esconder e sair apenas da fábrica.
M52509791
17

Desde a Primavera 4.3, há uma nova maneira de fazer isso, que foi costurada para esse problema.

ObjectProvider - Permite apenas adicioná-lo como uma dependência ao seu bean com escopo Prototype "argumentado" e instancia-lo usando o argumento

Aqui está um exemplo simples de como usá-lo:

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

Obviamente, isso imprimirá um hello string ao chamar usePrototype.

David Barda
fonte
15

ATUALIZADO por comentário

Primeiro, não sei por que você diz "isso não funciona" para algo que funciona bem no Spring 3.x. Eu suspeito que algo deve estar errado em sua configuração em algum lugar.

Isso funciona:

- Arquivo de configuração:

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

- Arquivo de teste para executar:

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

O uso do Spring 3.2.8 e Java 7 fornece esta saída:

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

Portanto, o Bean 'Singleton' é solicitado duas vezes. No entanto, como seria de esperar, o Spring apenas o cria uma vez. Na segunda vez, ele vê que possui esse bean e apenas retorna o objeto existente. O construtor (método @Bean) não é chamado pela segunda vez. Em deferência a isso, quando o Bean 'Prototype' é solicitado do mesmo objeto de contexto duas vezes, vemos que a referência muda na saída E que o construtor (método @Bean) É chamado duas vezes.

Então a questão é como injetar um singleton em um protótipo. A classe de configuração acima mostra como fazer isso também! Você deve passar todas essas referências para o construtor. Isso permitirá que a classe criada seja um POJO puro, além de tornar imutáveis ​​os objetos de referência contidos. Portanto, o serviço de transferência pode ser algo como:

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;
    }
    ....
}

Se você escrever testes de unidade, ficará muito feliz por ter criado as classes sem todo o @Autowired. Se você precisar de componentes com conexão automática, mantenha esses locais nos arquivos de configuração java.

Isso chamará o método abaixo no BeanFactory. Observe na descrição como isso se destina ao seu caso de uso exato.

/**
 * 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;
JoeG
fonte
3
Obrigado pela resposta! No entanto, acho que você não entendeu a pergunta. A parte mais importante da pergunta é que um valor de tempo de execução deve ser fornecido como um argumento construtor ao adquirir (instanciar) o protótipo.
precisa
Eu atualizei minha resposta. Na verdade, parecia que manipular o valor do tempo de execução foi feito corretamente, então deixei essa parte de fora. É explicitamente suportado, como você pode ver nas atualizações e resultados do programa.
JoeG
0

Você pode obter um efeito semelhante usando apenas um classe 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);
        }
    }
}
pmartycz
fonte
0

no seu arquivo xml de beans, use o atributo scope = "prototype"

manpreet singh
fonte
-1

Resposta tardia com uma abordagem ligeiramente diferente. Esse é um acompanhamento dessa pergunta recente que se refere a essa questão.

Sim, como foi dito, você pode declarar o protótipo de bean que aceita um parâmetro em uma @Configurationclasse que permite criar um novo bean a cada injeção.
Isso fará desta @Configuration classe uma fábrica e, para não atribuir muitas responsabilidades a essa fábrica, isso não deve incluir outros grãos.

@Configuration    
public class ServiceFactory {

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

}

Mas você também pode injetar esse bean de configuração para criar 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
    // ...    
}

É ao mesmo tempo seguro e conciso.

davidxxx
fonte
1
Obrigado pela resposta, mas este é um anti-padrão do Spring. Os objetos de configuração não devem 'vazar' no código do aplicativo - eles existem para configurar o gráfico de objetos do aplicativo e fazer interface com as construções do Spring. Isso é semelhante às classes XML nos beans de aplicativo (ou seja, outro mecanismo de configuração). Ou seja, se o Spring vier com outro mecanismo de configuração, você precisará refatorar o código do aplicativo - um indicador claro que viola a separação de preocupações. É melhor fazer com que o seu Config crie instâncias de uma interface Factory / Function e injete o Factory - sem acoplamento rígido à configuração.
Les Hazlewood
1) Concordo plenamente que, no caso geral, os objetos de configuração não precisam vazar como um campo. Porém, nesse caso específico, injetando um objeto de configuração que define um e apenas um bean para produzir protótipos de feijão, o IHMO faz todo sentido: essa classe de configuração se torna uma fábrica. Onde está a questão da separação de preocupações, se é que é só isso? ...
davidxxx
... 2) Sobre "Ou seja, se o Spring vier com outro mecanismo de configuração", é um argumento errado, porque quando você decide usar uma estrutura em seu aplicativo, você o associa a ele. Portanto, em qualquer caso, você também precisará refatorar qualquer aplicativo Spring que dependa @Configurationse esse mecanismo for alterado.
Davidxxx 12/08/19
1
... 3) A resposta que você aceitou propõe usar BeanFactory#getBean(). Mas isso é bem pior em termos de acoplamento, pois é uma fábrica que permite obter / instanciar quaisquer beans do aplicativo e não apenas qual deles o bean atual precisa. Com esse uso, você pode misturar responsabilidades de sua classe com muita facilidade, já que as dependências que ela pode gerar são ilimitadas, o que não é realmente recomendado, mas é um caso excepcional.
davidxxx 12/08/19
@ davidxxx - Aceitei a resposta anos atrás, antes que o JDK 8 e o Spring 4 fossem de fato. A resposta de Roman acima é mais correta para os usos modernos da primavera. Com relação à sua declaração "porque quando você decide usar uma estrutura em seu aplicativo, você o associa a isso" é bastante antitético às recomendações da equipe Spring e às práticas recomendadas do Java Config - pergunte a Josh Long ou Jeurgen Hoeller se você tiver uma oportunidade de falar com eles pessoalmente (eu tenho, e posso garantir que eles explicitamente aconselham a não associar o código do seu aplicativo ao Spring sempre que possível). Felicidades.
Les Hazlewood