Spring Java Config: как создать прототип с областью @Bean с аргументами времени выполнения?

используя Java-конфигурацию Spring, мне нужно получить / создать экземпляр компонента с областью прототипа с аргументами конструктора, которые доступны только во время выполнения. Рассмотрим следующий пример кода (упрощенный для краткости):

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

где класс вещи определяется следующим образом:

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

обратите внимание name is final: он может поставляться только через конструктор и гарантирует неизменность. Другие зависимости являются зависимостями, специфичными для реализации Thing class, и не должно быть известно (тесно связано с) реализацией обработчика запросов.

этот код отлично работает с конфигурацией Spring XML, например:

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

как я могу достичь того же самого с конфигурацией Java? Не работает следующее:

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

Я мог бы создать фабрику, например:

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

а то побеждает весь пункт использования весны для замены ServiceLocator и Шаблон дизайна фабрики, что было бы идеально для этого случая использования.

если Spring Java Config может это сделать, я мог бы избежать:

  • определение Заводского интерфейса
  • определение Заводской реализации
  • написание тестов для заводской реализации

это тонна работы (относительно говоря) для чего-то настолько тривиального, что Spring уже поддерживает через XML config.

4 ответов


на @Configuration класса,@Bean способ Вот так

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

используется для регистрации определение bean и предоставить завод для создания bean. Боб, который он определяет, создается только по запросу с использованием аргументов, которые определяются либо непосредственно, либо путем сканирования that ApplicationContext.

в случае prototype bean, каждый раз создается новый объект и, следовательно, соответствующий @Bean метод также выполняется.

вы можете получить Боб из ApplicationContext через BeanFactory#getBean(String name, Object... args) метод, который государства -

позволяет указать явные аргументы конструктора / заводской метод аргументы, отменяя указанное аргументы по умолчанию (если таковые имеются) в определение фасоли.

параметры:

args аргументы для использования при создании прототипа с использованием явных аргументов статический метод фабрики. Это недопустимо использовать ненулевое значение args в любом другом случае.

другими словами, для этого prototype scoped bean, вы предоставляете аргументы, которые будут использоваться не в конструкторе класса bean, а в @Bean вызов метода.

это по крайней мере верно для весны версий 4+.


С Spring > 4.0 и Java 8 вы можете сделать это более безопасно:

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

}

использование:

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

    // ...
}

теперь вы можете получить свой bean во время выполнения. Конечно, это фабричный шаблон, но вы можете сэкономить время на написании определенного класса, такого как ThingFactory (однако вам придется написать custom @FunctionalInterface для передачи более двух параметров).


обновлено за комментарий

во-первых, я не уверен, почему вы говорите "это не работает" для чего-то, что отлично работает весной 3.X. Я подозреваю, что что-то не так в вашей конфигурации.

это работает:

-- Config File:

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

-- тестовый файл для выполнения:

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

используя Spring 3.2.8 и Java 7, дает этот вывод:

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

таким образом, Боб "Синглтон" запрашивается дважды. Однако, как и следовало ожидать, Весна создает его только один раз. Во второй раз он видит, что у него есть этот боб и просто возвращает существующий объект. Конструктор (метод@Bean) не вызывается второй раз. В соответствии с этим, когда "прототип" Bean запрашивается из одного и того же объекта контекста дважды, мы видим, что ссылка изменяется в выходных данных и что конструктор (метод@Bean) вызывается дважды.

Итак, вопрос в том, как ввести синглтон в прототип. Этот класс конфигурации выше показывает,как это сделать! Вы должны передать все такие ссылки в конструктор. Это позволит созданному классу быть чистым POJO, а также сделать содержащиеся ссылочные объекты неизменяемыми, как они должны быть. Таким образом, услуга трансфера может выглядеть примерно так:

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

если вы пишете модульные тесты, вы будете очень рады, что создали классы без всех @Autowired. Если вам нужны компоненты autowired, держите их локальными для java конфигурационный файл.

это вызовет метод ниже в BeanFactory. Обратите внимание в описании, как это предназначено для вашего точного варианта использования.

/**
 * 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;

С весны 4.3 есть новый способ сделать это, который был сшит для этого вопроса.

ObjectProvider - это позволяет просто добавить его в качестве зависимости от вашего" аргументированного " прототипа с областью действия и создать его экземпляр с помощью аргумента

вот простой пример того, как его использовать:

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

это, конечно, напечатает строку hello при вызове usePrototype.