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.