Spring Java Config: як створити прототип @Bean з аргументами виконання?


134

Використовуючи 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
}

де клас Thing визначається наступним чином:

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це final: він може бути поставлений тільки через конструктор, і гарантує незмінність. Інші залежності - це залежності, що залежать від впровадження Thingкласу, і не повинні бути відомі (тісно пов'язані з) реалізацією обробника запитів.

Цей код прекрасно працює з конфігурацією Spring XML, наприклад:

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

Як досягти того ж, що і з конфігурацією Java? Нижче не працює з використанням Spring 3.x:

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

Тепер, я міг би створити завод, наприклад:

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

Але це перемагає всю точку використання Spring для заміни моделі дизайну ServiceLocator та Factory , що було б ідеально підходить для цього випадку використання.

Якби Spring Java Config міг це зробити, я міг би уникнути:

  • визначення фабричного інтерфейсу
  • визначення реалізації заводу
  • написання тестів для впровадження на заводі

Це багато роботи (відносно кажучи) для чогось настільки тривіального, що Spring вже підтримує через конфігурацію XML.


15
Відмінне запитання.
Сотіріос Деліманоліс

Однак, чи є причина, що ви не можете просто інстанціювати клас самостійно і доводиться отримувати його з Spring? Чи має вона залежність від інших бобів?
Sotirios Delimanolis

@SotiriosDelimanolis так, Thingреалізація насправді є більш складною і має залежність від інших бобів (я їх просто опустив для стислості). Тому я не хочу, щоб реалізація обробника запиту знала про них, оскільки це щільно з'єднає обробник з API / бобами, які йому не потрібні. Я оновлю питання, щоб відобразити ваше (відмінне) запитання.
Les Hazlewood

Я не впевнений, чи Spring дозволяє це на конструкторі, але я знаю, що ви можете встановити @Qualifierпараметри для сеттера з @Autowiredсамим сеттером.
CodeChimp

2
Навесні 4 ваш приклад з @Beanтворами. @BeanМетод викликається з відповідними аргументами , переданими getBean(..).
Sotirios Delimanolis

Відповіді:


94

У @Configurationкласі такий @Beanспосіб

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

використовується для реєстрації визначення квасолі та надання фабрики для створення квасолі . Блок, який він визначає, створюється лише за запитом, використовуючи аргументи, які визначаються безпосередньо або шляхом сканування ApplicationContext.

У випадку з prototypeквасолею кожен раз створюється новий об'єкт, і відповідно @Beanвиконується відповідний метод.

Ви можете отримати квасоля за ApplicationContextдопомогою BeanFactory#getBean(String name, Object... args)методу, який встановлює

Дозволяє вказувати явні аргументи конструктора / аргументи заводського методу, змінюючи вказані аргументи за замовчуванням (якщо такі є) у визначенні bean.

Параметри:

аргументує аргументи, які слід використовувати при створенні прототипу з використанням явних аргументів статичного заводського методу. Використання ненульового значення аргументів недійсне в будь-якому іншому випадку.

Іншими словами, для цього prototypeмасштабного бобу ви надаєте аргументи, які будуть використані не в конструкторі класу bean, а в @Beanвиклику методу.

Принаймні, це стосується весняних версій 4+.


12
Моя проблема такого підходу полягає в тому, що ви не можете обмежити @Beanметод ручним викликом. Якщо ви коли - небудь метод буде викликаний , імовірно , вмирає від неможливості вводити параметр. Те саме, якщо ти . Я вважав це трохи крихким. @Autowire Thing@Bean@Autowire List<Thing>
Ян Зика

@JanZyka, чи є спосіб, коли я можу автопроводити щось, крім того, що зазначено в цих відповідях (які, по суті, однакові, якщо ти косиш). Більш конкретно, якщо я знаю аргументи заздалегідь (під час компіляції / конфігурації), чи я можу висловити ці аргументи в якійсь анотації, з якою я можу кваліфікуватись @Autowire?
М. Прохоров

52

За допомогою 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);

    // ...
}

Тож тепер ви можете отримати свій боб під час виконання. Це, звичайно, заводський зразок, але ви можете заощадити певний час на написання конкретного класу ThingFactory(однак вам доведеться писати на замовлення, @FunctionalInterfaceщоб пройти більше двох параметрів).


1
Я вважаю такий підхід дуже корисним та чистим. Дякую!
Олексій Облеян

1
Що таке тканина? Я розумію ваше використання .. але не термінологію .. не думаю, що я чув про "візерунок тканини"
AnthonyJClink

1
@AnthonyJClink Я думаю, що я просто використовував fabricзамість factoryмене погано :)
Роман Голишев

1
@AbhijitSarkar о, я бачу. Але ви не можете передати параметр a Providerабо an ObjectFactory, чи я помиляюся? І в моєму прикладі ви можете передавати йому параметр string (або будь-який параметр)
Роман Голишев

2
Якщо ви не хочете (або не потрібно) використовувати методи життєвого циклу весняних бобів (які відрізняються від прототипу бобів ...), ви можете пропустити @Beanта Scopeпримітки щодо Thing thingметоду. Більше того, цей спосіб можна зробити приватним, щоб сховатися і залишити лише завод.
m52509791

17

Починаючи з весни 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();
    }
}

Звичайно, це буде надрукувати привітну рядок під час виклику UsePrototype.


15

ОНОВЛЕНО за коментар

По-перше, я не впевнений, чому ви говорите "це не працює" на щось, що працює чудово навесні 3.x. Я підозрюю, що десь має бути не так у вашій конфігурації.

Це працює:

- Налаштування файлу:

@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

Тож "Singleton" Біна просять двічі. Однак, як ми очікували, Весна створює його лише один раз. Вдруге він бачить, що у нього є боб і просто повертає існуючий об’єкт. Конструктор (метод @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. Якщо вам потрібні автоматичні компоненти, зберігайте ці локальні файли у конфігурацій 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;

3
Дякую за відповідь! Однак, я думаю, ви неправильно зрозуміли питання. Найважливіша частина питання полягає в тому, що значення часу виконання необхідно надати як аргумент конструктора при придбанні (інстанції) прототипу.
Les Hazlewood

Я оновив свою відповідь. Насправді здавалося, що обробку значення виконання було виконано правильно, тому я залишив цю частину поза. Це явно підтримується, хоча, як видно з оновлень та результатів програми.
JoeG

0

Ви можете досягти подібного ефекту, просто використовуючи внутрішній клас :

@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

Пізня відповідь з дещо іншим підходом. Це є наслідком цього останнього питання яке стосується самого цього питання.

Так, як це було сказано, ви можете оголосити прототип bean, який приймає параметр у @Configurationкласі, що дозволяє створювати нову квасолю при кожному введенні.
Це зробить цей @Configuration клас фабричним і не дасть цій фабриці занадто багато обов'язків, це не повинно включати інші боби.

@Configuration    
public class ServiceFactory {

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

}

Але ви також можете ввести цю конфігураційну частину для створення 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
    // ...    
}

Він одночасно безпечний і стислий.


1
Дякую за відповідь, але це весняний анти-шаблон. Конфігурація об'єктів не повинна «просочуватися» в код програми - вони існують для налаштування графіку об’єкта програми та інтерфейсу з конструктами Spring. Це схоже на класи XML у ваших додатках (тобто інший механізм конфігурації). Тобто, якщо Spring додається разом з іншим механізмом конфігурації, вам доведеться перефактурувати код програми - чіткий показник це порушує розділення проблем. Краще, щоб ваш Config створював екземпляри інтерфейсу Factory / Function і вводив Factory (Фабрика) - немає жорсткої зв'язку для конфігурування.
Les Hazlewood

1) Я повністю згоден, що в загальному випадку об'єкти конфігурації не повинні просочуватися як поле. Але в цьому конкретному випадку, вводячи об'єкт конфігурації, який визначає один і єдиний боб для отримання прототипу, IHMO має цілком сенс: цей клас конфігурації стає заводським. Де розмежування питань стосується, якщо воно робить лише це? ...
davidxxx

... 2) Про "Тобто, якщо Spring приходить разом з іншим механізмом конфігурації", це неправильний аргумент, тому що коли ви вирішили використовувати рамку у своїй програмі, ви з'єднуєте свою програму з цим. Тож у будь-якому випадку вам також доведеться переробляти будь-які додатки Spring, на які покладається, @Configurationякщо цей механізм змінився.
davidxxx

1
... 3) Відповідь, яку ви прийняли, пропонує використовувати BeanFactory#getBean(). Але це набагато гірше з точки зору з’єднання, оскільки це фабрика, яка дозволяє отримати / інстанціювати будь-які боби програми, а не лише те, що потрібно нинішньому бобу. Завдяки такому використанню ви можете легко поєднувати обов'язки свого класу, оскільки залежність, яку він може спричинити, необмежена, що насправді не радиться, але винятковий випадок.
davidxxx

@ davidxxx - я прийняв відповідь років тому, перш ніж JDK 8 та Spring 4 були фактично. Відповідь Романа вище правильна для сучасних весняних звичаїв. Що стосується вашої заяви "тому, що коли ви вирішили використовувати рамку у своїй програмі, ви поєднуєте свою заявку з цим", це досить антитетично до рекомендацій команди Spring та найкращих практик Java Config - запитайте Джоша Лонга або Євргена Холлера, якщо ви отримаєте шанс особисто поговорити з ними (у мене є, і можу запевнити, вони прямо не радить не приєднувати ваш код програми до Spring, коли це можливо). Ура.
Les Hazlewood
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.