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żą.