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 name
jest final
: może być dostarczone tylko przez konstruktora i gwarantuje niezmienność. Inne zależności są zależnościami Thing
klasy 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.
Thing
implementacja 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.@Bean
pracami.@Bean
Metoda jest wywoływana z odpowiednimi argumentami zdałeś dogetBean(..)
.@Autowired
lub 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 zaBeanFactory.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).Odpowiedzi:
W
@Configuration
klasie taka@Bean
metoda@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
prototype
fasoli za każdym razem tworzony jest nowy obiekt i dlatego@Bean
wykonywana jest również odpowiednia metoda.Można pobrać ze strony fasoli
ApplicationContext
poprzez swojąBeanFactory#getBean(String name, Object... args)
który stanowi, metodyInnymi słowy, dla tego
prototype
komponentu bean o określonym zakresie podajesz argumenty, które będą używane nie w konstruktorze klasy bean, ale w@Bean
wywoł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
ApplicationContext
lubBeanFactory
do pobierania fasoli, możesz wstrzyknąćObjectProvider
(od wiosny 4.3).i użyj jego
getObject(Object... args)
metodyNa przykład,
@Autowired private ObjectProvider<Thing> things; [...] Thing newThing = things.getObject(name); [...]
źródło
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,@FunctionalInterface
aby przekazać więcej niż dwa parametry).źródło
fabric
zamiast tegofactory
, mój błąd :)Provider
lub do anObjectFactory
, czy się mylę? W moim przykładzie możesz przekazać do niego parametr ciągu (lub dowolny parametr)@Bean
i dodaćScope
adnotacjeThing thing
. Ponadto ta metoda może być prywatna, aby się ukryć i opuścić tylko fabrykę.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.
źródło
MyPrototype
jestbean
zależny od innych ziaren, co oznacza, że nie można go utworzyć przy użyciunew
?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;
źródło
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); } } }
źródło
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
@Configuration
klasie, 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ć
Thing
s:@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.
źródło
@Configuration
tego mechanizmu.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.w pliku XML fasoli użyj atrybutu scope = "prototyp"
źródło