Spring Java Config: jak stworzyć @Bean o zasięgu prototypu z argumentami runtime?

139

Korzystając z Java Config Springa, muszę uzyskać / utworzyć komponent bean o zakresie prototypu z argumentami konstruktora, które są dostępne tylko w czasie wykonywania. Rozważmy następujący przykład kodu (uproszczony dla zwięzłości):

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

gdzie klasa Thing jest zdefiniowana w następujący sposób:

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

Wskazówka namejest final: może być dostarczone tylko przez konstruktora i gwarantuje niezmienność. Inne zależności są zależnościami Thingklasy specyficznymi dla implementacji i nie powinny być znane (ściśle powiązane) z implementacją programu obsługi żądań.

Ten kod działa doskonale z konfiguracją Spring XML, na przykład:

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

Jak osiągnąć to samo dzięki konfiguracji Java? Następujące elementy nie działają w Spring 3.x:

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

Teraz mógłbym stworzyć Factory np:

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

Ale to podważa cały sens używania Springa do zastąpienia wzorca projektowego ServiceLocator i Factory , który byłby idealny w tym przypadku użycia.

Gdyby Spring Java Config mógł to zrobić, byłbym w stanie uniknąć:

  • definiowanie interfejsu Factory
  • definiowanie implementacji Factory
  • pisanie testów na wdrożenie Factory

To mnóstwo pracy (względnie mówiąc) na coś tak trywialnego, że Spring obsługuje już konfigurację XML.

Les Hazlewood
źródło
18
Świetne pytanie.
Sotirios Delimanolis
Czy jest jednak powód, dla którego nie możesz po prostu samodzielnie utworzyć instancji klasy i musisz pobrać ją ze Springa? Czy ma zależności od innych ziaren?
Sotirios Delimanolis
@SotiriosDelimanolis tak, Thingimplementacja jest w rzeczywistości bardziej złożona i ma zależności od innych fasoli (po prostu je pominąłem dla zwięzłości). W związku z tym nie chcę, aby implementacja obsługi żądań wiedziała o nich, ponieważ ściśle powiązałoby to procedurę obsługi z interfejsami API / fasolami, których nie potrzebuje. Zaktualizuję pytanie, aby odzwierciedlić Twoje (doskonałe) pytanie.
Les Hazlewood
2
Wiosną 4 Twój przykład z @Beanpracami. @BeanMetoda jest wywoływana z odpowiednimi argumentami zdałeś do getBean(..).
Sotirios Delimanolis
1
To nie jest trudne do wstrzykiwać pola oznaczone @Autowiredlub ustawiaczy pomocą Wiosenne zajęcia siebie (kilka linii kodu) i używam go na chwilę (ze sprężyną 2,5 jeśli dobrze pamiętam) za pomocą żadnego z @Bean, @Scope("prototype"), @Configuration. Nie uważam za BeanFactory.getBean(String, Object[])rozsądne, ponieważ brakuje sprawdzania czasu kompilacji. Prawdopodobnie napiszę odpowiedź, kiedy wymyślę projekt, który mogę polecić (mój obecny projekt ma pewne problemy).
Vytenis Bivainis

Odpowiedzi:

97

W @Configurationklasie taka @Beanmetoda

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

służy do rejestrowania definicji fasoli i dostarczania fabryki do jej tworzenia . Komponent bean, który definiuje, jest tworzony tylko na żądanie przy użyciu argumentów, które są określane bezpośrednio lub poprzez skanowanie ApplicationContext.

W przypadku prototypefasoli za każdym razem tworzony jest nowy obiekt i dlatego @Beanwykonywana jest również odpowiednia metoda.

Można pobrać ze strony fasoli ApplicationContextpoprzez swoją BeanFactory#getBean(String name, Object... args)który stanowi, metody

Umożliwia określenie jawnych argumentów konstruktora / argumentów metody fabrycznej, przesłaniając określone argumenty domyślne (jeśli istnieją) w definicji komponentu bean.

Parametry:

args argumenty do użycia podczas tworzenia prototypu przy użyciu jawnych argumentów do statycznej metody fabrycznej. W każdym innym przypadku użycie wartości argumentów innej niż null jest nieprawidłowe.

Innymi słowy, dla tego prototypekomponentu bean o określonym zakresie podajesz argumenty, które będą używane nie w konstruktorze klasy bean, ale w @Beanwywołaniu metody. (Ta metoda ma bardzo słabe gwarancje typu, ponieważ używa wyszukiwania nazwy dla fasoli).

Alternatywnie możesz użyć BeanFactory#getBean(Class requiredType, Object... args)metody wpisanej , która wyszukuje ziarno według typu.

Dotyczy to przynajmniej wersji Spring 4+.

Zauważ, że jeśli nie chcesz zaczynać od ApplicationContextlub BeanFactorydo pobierania fasoli, możesz wstrzyknąć ObjectProvider(od wiosny 4.3).

Wariant ObjectFactoryzaprojektowany specjalnie dla punktów wtrysku, pozwalający na programową opcjonalność i wygodną nie-unikalną obsługę.

i użyj jego getObject(Object... args)metody

Zwróć instancję (prawdopodobnie współużytkowaną lub niezależną) obiektu zarządzanego przez tę fabrykę.

Pozwala na określenie jawnych argumentów konstrukcyjnych, wzdłuż linii BeanFactory.getBean(String, Object).

Na przykład,

@Autowired
private ObjectProvider<Thing> things;

[...]
Thing newThing = things.getObject(name);
[...]
Sotirios Delimanolis
źródło
58

Dzięki Spring> 4.0 i Java 8 możesz to zrobić bezpieczniej:

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

}

Stosowanie:

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

    // ...
}

Więc teraz możesz dostać swoją fasolę w czasie wykonywania. Jest to oczywiście wzorzec fabryczny, ale możesz zaoszczędzić trochę czasu na pisaniu konkretnych klas ThingFactory(jednak będziesz musiał napisać niestandardowy, @FunctionalInterfaceaby przekazać więcej niż dwa parametry).

Roman Golyshev
źródło
1
Uważam, że to podejście jest bardzo przydatne i czyste. Dzięki!
Alex Objelean
1
Co to jest tkanina? Rozumiem twoje użycie… ale nie terminologię… nie sądzę, że słyszałem o „wzorze tkaniny”
AnthonyJClink
1
@AnthonyJClink Chyba właśnie użyłem fabriczamiast tego factory, mój błąd :)
Roman Golyshev
1
@AbhijitSarkar oh, rozumiem. Ale nie możesz przekazać parametru do a Providerlub do an ObjectFactory, czy się mylę? W moim przykładzie możesz przekazać do niego parametr ciągu (lub dowolny parametr)
Roman Golyshev
2
Jeśli nie chcesz (lub nie musisz) używać metod cyklu życia fasoli wiosennej (które są inne dla prototypowych ziaren ...), możesz pominąć tę metodę @Beani dodać Scopeadnotacje Thing thing. Ponadto ta metoda może być prywatna, aby się ukryć i opuścić tylko fabrykę.
m52509791
21

Od wiosny 4.3 jest nowy sposób na zrobienie tego, który został przyszyty do tego wydania.

ObjectProvider - umożliwia po prostu dodanie go jako zależności do "argumentowanego" komponentu bean Prototype z zakresem i utworzenie jego wystąpienia przy użyciu argumentu.

Oto prosty przykład, jak go używać:

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

Spowoduje to oczywiście wypisanie ciągu hello podczas wywoływania metody usePrototype.

David Barda
źródło
A co, jeśli MyPrototypejest beanzależny od innych ziaren, co oznacza, że ​​nie można go utworzyć przy użyciu new?
Searene
@Searene, zawsze możesz wstrzyknąć inne ziarna, aby stworzyć prototyp, wiosna wstrzyknie je za Ciebie.
David Barda
15

ZAKTUALIZOWANE za komentarz

Po pierwsze, nie jestem pewien, dlaczego mówisz „to nie działa” w przypadku czegoś, co działa dobrze na wiosnę 3.x. Podejrzewam, że gdzieś coś jest nie tak w twojej konfiguracji.

To działa:

- Plik konfiguracyjny:

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

- Plik testowy do wykonania:

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

Używając Spring 3.2.8 i Java 7, daje to:

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

Tak więc fasola „Singleton” jest żądana dwukrotnie. Jednak, jak byśmy się spodziewali, Spring tworzy go tylko raz. Za drugim razem widzi, że ma tę fasolę i po prostu zwraca istniejący obiekt. Konstruktor (metoda @Bean) nie jest wywoływany po raz drugi. W związku z tym, gdy Bean 'Prototype' jest dwukrotnie żądany od tego samego obiektu kontekstu, widzimy, że referencja zmienia się w danych wyjściowych ORAZ że konstruktor (metoda @Bean) JEST dwukrotnie wywoływany.

Zatem pytanie brzmi, jak wstrzyknąć singleton do prototypu. Powyższa klasa konfiguracji również pokazuje, jak to zrobić! Powinieneś przekazać wszystkie takie odwołania do konstruktora. Dzięki temu utworzona klasa będzie czystym POJO, a zawarte w niej obiekty referencyjne będą niezmienne, tak jak powinny. Więc usługa transferu może wyglądać mniej więcej tak:

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

Jeśli piszesz testy jednostkowe, będziesz bardzo szczęśliwy, że stworzyłeś te klasy bez całego @Autowired. Jeśli potrzebujesz automatycznie przypisanych komponentów, zachowaj je lokalnie w plikach konfiguracyjnych Java.

Spowoduje to wywołanie poniższej metody w BeanFactory. Zwróć uwagę w opisie, w jaki sposób jest to przeznaczone dla twojego konkretnego przypadku użycia.

/**
 * 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
źródło
3
Dziękuję za odpowiedź! Myślę jednak, że źle zrozumiałeś pytanie. Najważniejszą częścią pytania jest to, że podczas pozyskiwania (tworzenia instancji) prototypu należy podać wartość wykonawczą jako argument konstruktora.
Les Hazlewood
Zaktualizowałem swoją odpowiedź. Właściwie wydawało się, że obsługa wartości czasu wykonania została wykonana poprawnie, więc pominąłem tę część. Jest wyraźnie obsługiwany, jak widać po aktualizacjach i wynikach programu.
JoeG
0

Podobny efekt możesz osiągnąć używając tylko klasy wewnętrznej :

@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
źródło
-1

Późna odpowiedź z nieco innym podejściem. To jest kontynuacja tego niedawnego pytania, które samo dotyczy tego pytania.

Tak, tak jak zostało powiedziane, możesz zadeklarować prototypowy bean, który akceptuje parametr w @Configurationklasie, który pozwala na tworzenie nowego ziarna przy każdym wstrzyknięciu.
To uczyni tę @Configuration klasę fabryką i aby nie nakładać na tę fabrykę zbyt wielu obowiązków, nie powinno to obejmować innych fasoli.

@Configuration    
public class ServiceFactory {

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

}

Ale możesz również wstrzyknąć ten komponent bean konfiguracji, aby utworzyć 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
    // ...    
}

Jest zarówno bezpieczny, jak i zwięzły.

davidxxx
źródło
1
Dzięki za odpowiedź, ale to jest wiosenny anty-wzór. Obiekty konfiguracyjne nie powinny „przeciekać” do kodu aplikacji - istnieją po to, aby skonfigurować graf obiektów aplikacji i interfejs za pomocą konstrukcji Spring. Jest to podobne do klas XML w komponentach bean aplikacji (tj. Inny mechanizm konfiguracyjny). Oznacza to, że jeśli Spring pojawi się z innym mechanizmem konfiguracyjnym, będziesz musiał refaktoryzować kod aplikacji - wyraźny wskaźnik, który narusza separację problemów. Lepiej jest, aby Twój Config utworzył instancje interfejsu Factory / Function i wstrzyknął Factory - bez ścisłego połączenia z config.
Les Hazlewood,
1) Całkowicie się zgadzam, że w ogólnym przypadku obiekty konfiguracyjne nie muszą przeciekać jako pola. Ale w tym konkretnym przypadku, wprowadzając obiekt konfiguracyjny, który definiuje jedną i tylko jedną fasolę do produkcji prototypowych ziaren, IHMO ma to całkowicie sensowne: ta klasa konfiguracji staje się fabryką. Gdzie jest kwestia rozdzielenia obaw, jeśli tylko to robi? ...
davidxxx
... 2) Jeśli chodzi o „To znaczy, jeśli Spring jest wyposażony w inny mechanizm konfiguracyjny”, jest to zły argument, ponieważ decydując się na użycie frameworka w swojej aplikacji, łączysz z nim swoją aplikację. W każdym razie będziesz musiał również refaktoryzować wszystkie aplikacje Spring, na których opiera się@Configuration tego mechanizmu.
davidxxx
1
... 3) Odpowiedź, którą zaakceptowałeś, sugeruje użycie BeanFactory#getBean(). Ale to jest o wiele gorsze, jeśli chodzi o sprzężenie, ponieważ jest to fabryka, która pozwala uzyskać / utworzyć wystąpienie dowolnego ziarna aplikacji, a nie tylko tego, którego potrzebuje bieżąca fasola. Dzięki takiemu użyciu możesz bardzo łatwo mieszać obowiązki swojej klasy, ponieważ zależności, które może ściągać, są nieograniczone, co nie jest zalecane, ale jest to wyjątkowy przypadek.
davidxxx
1
@ davidxxx - Przyjąłem odpowiedź lata temu, zanim JDK 8 i Spring 4 były de facto. Powyższa odpowiedź Romana jest bardziej poprawna w przypadku współczesnych zastosowań wiosennych. Jeśli chodzi o twoje stwierdzenie, „ponieważ decydując się na użycie frameworka w swojej aplikacji, łączysz ją z tą aplikacją” jest całkiem sprzeczne z zaleceniami zespołu Spring i najlepszymi praktykami Java Config - zapytaj Josha Longa lub Jeurgena Hoellera, jeśli otrzymasz szansa porozmawiać z nimi osobiście (mam i mogę was zapewnić, że wyraźnie radzą, aby nie para kodzie aplikacji do wiosny, gdy jest to możliwe). Twoje zdrowie.
Les Hazlewood,
-1

w pliku XML fasoli użyj atrybutu scope = "prototyp"

manpreet singh
źródło