Nawigacja w Vaadin z użyciem Guice
I kolejny temat około pracowy mi wyszedł. Zaczęło się od tweeta:
Assisted injection providera w providera tworzenego z factory za pomocą Assisted injection #java kurwa! #guice kurwa! #spring ssie
— koziolek (@koziolek) February 7, 2015
I generalnie tu pojawił się pomysł na wpis.
Problem
Chcemy by nasze View w Vaadin mogły być tworzone w taki sposób by Guice mógł wstrzyknąć im np. serwisy. Najprościej jest to zrobić w następujący sposób:
Listing 1. Wykorzystanie zdarzeń
public class NavigationProvider implements Provider<Navigator> {
@Inject
private MyUI ui;
@Inject
private ViewRegister viewRegister;
@Inject
private Provider<Injector> injector;
@Override
public Navigator get() {
Navigator nav = new Navigator(ui, ui);
viewRegister.initNavigation(nav);
nav.addViewChangeListener(new ViewChangeListener() {
@Override
public boolean beforeViewChange(ViewChangeEvent event) {
injector.get().injectMembers(event.getNewView());
return true;
}
@Override
public void afterViewChange(ViewChangeEvent event) {
}
});
return nav;
}
}
Mamy tu trzy zasadnicze elementy. Po zerowe obiekty Navigation są dostarczane providerem do głównego UI. Po pierwsze wstrzykujemy UI po utworzeniu providera (i wstrzyknięciu go do UI). Zrobienie tego w konstruktorze jest OK, ale jakieś takie niekoszerne i wymaga kombinowania.
Po drugie potrzebujemy instancji injectora i wstrzykujemy go przez providera.
Po trzecie po utworzeniu Navigation dodajemy do niego listener, w którym mając już utworzony nowy widok wstrzykujemy w niego potrzebne składowe. ViewRegister to już ciekawostka, do budowania „zagłębień” w menu.
Wadą tego rozwiązania jest brak możliwości tworzenia widoków wykorzystujących wstrzykiwanie przez konstruktor. Wynika to z faktu, że w bebechach Vaadin wykorzystywany jest ClassBasedViewProvider, który poprzez refleksję tworzy nowe widoki. By to działało narzuca on ograniczenie z bezparametrowym konstruktorem.
To czy wykorzystywać wstrzykiwanie przez konstruktor czy też przez pola to problem na osobą dyskusję. W każdym razie ja trafiłem na sytuację gdy wstrzykiwanie przez konstruktor okazało się zdroworozsądkowe.
Rozwiązanie
Kto śledzi bloga ten zapewne kojarzy mechanizm Assisted Injection obecny w Guice. Kliknijcie poczytajcie. Do rozwiązania naszego problemu wykorzystam właśnie ten mechanizm.
Własny Navigation
W sumie nie własny, bo rozszerzający ten z Vaadin, ale zawsze:
Listing 2. Ta klasa ma straszną nazwę…
public class GuicefiedNavigator extends Navigator {
private GuiceViewProviderFactory guiceViewProviderFactory;
@Inject
public GuicefiedNavigator(GuiceViewProviderFactory guiceViewProviderFactory,
@Assisted UI ui, @Assisted SingleComponentContainer container) {
super(ui, container);
this.guiceViewProviderFactory = guiceViewProviderFactory;
}
@Override
public void addView(String viewName, Class extends View> viewClass) {
Preconditions.checkNotNull(viewName, "view and viewClass must be non-null");
Preconditions.checkNotNull(viewClass, "view and viewClass must be non-null");
removeView(viewName);
addProvider(guiceViewProviderFactory.create(viewName, viewClass));
}
}
Jeden konstruktor, który akurat potrzebuję (bonus AI – można mieć wiele konstruktorów oznaczonych @Inject) i który jest zmapowany w odpowiedniej fabryce:
Listing 3. Ta nazwa jest bardzo javowa
public interface GuicefiedNavigatorFactory {
GuicefiedNavigator create(UI ui, SingleComponentContainer container);
}
nadpisujemy tu jedną metodę. Służy ona to dodawania widoków i ich providerów do mechanizmu nawigacji. W oryginale jest to wykorzystana jak wspomniałem klasa ClassBasedViewProvider. Ja zastąpiłem ją za pomocą GuiceViewProviderFactory
I jeszcze GuiceViewProvider
Skoro jest factory to mamy tu kanapkę z podwójnym mięskiem, czyli kolejny AI na pokładzie.
Listing 4. Pracuję nad nazwami
public class GuiceViewProvider implements ViewProvider {
private Provider<Injector> injectorProvider;
private String viewName;
private Class extends View> viewClass;
@Inject
public GuiceViewProvider(Provider<Injector> injectorProvider,
@Assisted String viewName, @Assisted Class extends View> viewClass) {
this.injectorProvider = injectorProvider;
this.viewName = checkNotNull(viewName);
this.viewClass = checkNotNull(viewClass);
}
@Override
public String getViewName(String navigationState) {
if (null == navigationState) {
return null;
}
if (viewName.equals(navigationState)
|| navigationState.startsWith(viewName + "/")) {
return viewName;
}
return null;
}
@Override
public View getView(String viewName) {
if (this.viewName.equals(viewName)) {
return injectorProvider.get().getInstance(viewClass);
}
return null;
}
}
Tu magii nie ma (jest za to factory). Wstrzykujemy injector, z którego później pobieramy nowe instancje widoków.
Konfiguracja
Ostatnim krokiem w tej zabawie jest skonfigurowanie tego wszystkiego by śmigało jak należy dodać odpowiedni wpis w konfiguracji modułów.
Listing 5. Guice 3.0 w akcji w środowisku 4.0
install(new FactoryModuleBuilder().
implement(GuicefiedNavigator.class, GuicefiedNavigator.class)
.build(GuicefiedNavigatorFactory.class));
install(new FactoryModuleBuilder().
implement(GuiceViewProvider.class, GuiceViewProvider.class)
.build(GuiceViewProviderFactory.class));
Podsumowanie
Są dwie pułapki w tym całym rozwiązaniu. Po pierwsze kwestia sprawdzenia nazw w GuiceViewProvider musi spełniać kontrakt z interfejsu. Jednak nie ma tam słowa o tym slashu. Po drugie należy pamiętać, że wszystkie widoki musimy bindować jako bez scopa, bo domyślny singleton zrobi nam sieczkę z aplikacji (nie wiem jak to działa, bo z obserwacji wynika duża losowość).
Jeżeli spodobał ci się ten wpis to podziel się nim ze znajomymi korzystając z przycisków poniżej. Do tego one służą.