pre-requisites

Following concepts should be studied and clearly understandable before proceeding with this:

Knowledge of the Guice SPI principles is not mandatory but could also help in understanding the concept.

Previous IoC principles in UI

Up till now various Ui contexts use dedicated GuiceComponentProviders to make sure that they are supplied with bindings and instances specific only to them.

The scope in such case is determined by the lifecycle of the corresponding injector instance: all the "scoped" objects are singletons bound to this or that injector and they will stick around until that injector will be garbage collected (i.e. an app is closed and its context is deregistered, or the whole tab is closed and Vaadin destroys the corresponding admincentral session).

Regarding component mapping relevance - it is trivial since bindings are each and every time collected specifically for the current context (e.g. AppController collects the bindings relevant for the current app only). The drawbacks of such an approach are mentioned in the previous research concept linked above. Here we provide a rough outline of the current state of things:



























TL;DR of the new Ui IoC architecture

  • There is one and only one Ui-related GuiceComponentProvider instance for the whole lifecycle of the Magnolia web-app. 
  • CP lives in the servlet context outside of any HTTP session.
  • CP provides bindings for all UI related components from all the module at the same time.
  • Type binding conflicts are resolved with Guice binding annotations.
  • Scoped objects stored in http session and can be queried via the id of related Ui entity (app, sub-app, admincentral contexts etc).
  • Storage look-ups and caching of instances is done via Guice custom scopes.
  • UiInjectionContext drives scopes to the correct bean storages.
  • UiInjectionContext helps to select the most relevant implementation of type when several available.
  • All the UI framework Guice CP are replaced with ones that merely set the state of UiInjectionContext and delegate to the one CP from servlet context.

New scoping principles

TL;DR

  • New scoping is done via Guice custom scopes.
  • Whatever that used to have a dedicated GuiceComponentProvider (app, sub-app etc) now merely has a key-value storage in VaadinSession.
  • Ui Scopes connect Guice Injector with a such key-value storage depending on "who is currently injecting" (app, sub-app or whoever else...).
  • UiInjectionContext allows to set the current Ui context so scopes know where to look.

In details

The new concept involves several Magnolia UI specific scopes. Those are legit Guice scopes implemented according to the documentation (link is in the disclaimer above). 

It is important to understand what does term "scope" mean in current context. In my opinion it can be best described as a caching (or memoizing) decorator of an instance factory. When Guice is done binding all the interfaces to their implementations, it creates factories for them. In normal case, when an instance of Foo needs to be injected, Guice will look-up the corresponding factory (e.g. there's one that invokes a c-tor of FooImpl) and will use it to create an object. However, if the type is bound is a certain scope, Guice will ask that scope to wrap an object factory. The typical scenario is that the scope generates a provider which first looks up an instance of a type in some storage, or, if it is missing - will use the "wrapped" provider and will store the result for later queries.

In our case the storage mentioned is VaadinSession. We treat it as a set of key-value BeanStores each of which corresponds to a Ui entity (admincentral, app, sub-app or view instances). Magnolia UI scopes in such case are responsible for matching the current UI context with the correct BeanStores and cache instances in them. Information about the current context is stored in the UiInjectionContext object in a for of a key.

For instance, the current context is set according to the sub-app BAR of app FOO. In such case several UiContext keys will be resolvable from the UiInjectionContext: the one for sub-app itself, one for the app FOO and one for the current Admincentral instance. When a sub-app-scoped instance will be requested for injection, the SubAppScope will ask the UiInjectionContext for the currently available sub-app key (FOO@BAR)  and will look up the corresponding BeanStore. In the same fashion - the AppScope will ask for the actual app key (FOO) and will look up instances from the related store etc.

For explicitness BeanStores have to be manually created before any related injection happens. Store clean-up should also be done in a manual way.

Currently available scopes:

  • Admincentral scope (annotation @AdmincentralScoped) - for all the components that live as long as the whole Admincentral browser tab (LocationController, Shell, AppController etc)
  • App scope (annotation @AppScoped)  - for all the components that are bound to the AppContext instance.
  • Sub-app scope (annotation @SubAppScoped) - for all the components that are bound to the SubAppContext instance.
  • View scope (annotation @ViewScoped) - currently less used scope which binds instances to the current view and cleans them up as soon as the view is closed. Currently it is used for the chooser dialogs and media editors only.
  • Any UI context scope (annotation @UiContextScoped) - will provide an instance of a type per any UI context which requests it.

Lazy instances vs eager instances

It is worth noticing that by default all the scoped instances are lazy, i.e. they won't be created unless some other object will need them to be injected. This is slightly different from the Guice's treatment of singletons: those are eager by default and rather @LazySingleton annotation can enforce laziness (in production mode even with it the singletons are eager though). With Guice's defaults the opportunity of making the singletons lazy is often overlooked (using just @Singleton is more intuitive) and despite that most of the instances aren't needed right away, they are created instantly. Magnolia UI scopes suggest laziness by default.

However, there are also several annotations that allow to make the scoped instances eagerly created. Instances of types marked with such annotations will still end up in either of the above mentioned scopes, but whenever a bean storage is created (e.g. app/sub-app started), an instance of "eager" types will be created and put in the storage. Here are the eager annotations:

  • @AppScopedEager
  • @AdmincentralScopedEager
  • @SubAppScopedEager
  • @ViewScopedEager

EventBus scopes

There is another "hidden" scope which the developers should not typically care about. It ensures that the EventBus listeners related to the UiContext are cleaned up when the context dies. Whenever a scoped EventBus instance is needed in a narrower scope (e.g. a sub-app tries to inject the app EventBus) - the instance is automatically wrapped into the ResettableEventBus.

The new binding principles

TL;DR

  • All Ui related bindings are bound together.
  • Binding annotations are used heavily to support context-specific component bindings (~ component customisations in apps, different impls etc);
  • when many possible context-specific implementations - type itself is bound to UiInjectionContextResolvingProvider which selects the most relevant impl according to UiInjectionContext.
  • Current way of configuring the component mappings in XML module descriptors has not changed:
    • All the binding annotations and special providers are configured automatically without any action required from the developer. фс

Guice does not allow to simply bind a type to two different implementations for obvious reasons. However, it allows to do so if the additional context is specified (via Binding Annotations). One drawback of such an approach is that normally a developer would then have to add such annotations to the constructor arguments to let Guice know which flavour of the instance we want (like we do now with @Named event buses). We obviously cannot and must not annotate all our constructors: not only that is technically not possible, but in most of the cases, the implementation has to be picked dynamically based on the current UI context.

The solution is to use UiInjectionContext when selecting the right implementation: we bind the un-annotated type to a provider which picks one of the available annotated variants based on the current state of UiInjectionContext.

As it is mentioned above - such bindings are prepared automatically when several different implementations of the same type are detected during preparation of the UI GuiceComponentProvider. Binding annotations are not used when the the type mappings are not ambiguous. 

Compatibility and Workarounds 

There are several cases of component mappings which conflict with the proposed concept and need additional care. 

The major pain point is the singleton types configured "in the old terms" with the @Singleton annotation. Singleton annotation effectively means that the instance of a type will exist as long as the injector itself exists, which was acceptable with the multi-injector approach since injectors themselves had pretty short lifecycle. Now there's a single injector which lives as long as the UI servlet and @Singleton annotation breaks the new scoping. Luckily Guice allows to override the effect of scope annotations programmatically, and so we do. When populating the UI-related bindings we put them through an SPI-transformer which detects and rebinds the @Singleton-marked types.

There is also some painful experience dealing with the non-abstract type bindings and especially types mapped to themselves (in Guice terms those are called UntargettedBindings). Classes like WorkbenchPresenter for instance impose certain problems for the ContextResolvingProvider creating a danger of circular dependencies: un-annotated type is bound to the ContextResolvingProvider which may pick e.g. WorkbenchPresenter binding that is bound to the WorkbenchPresenter itself, which will trigger the ContextResolvingProvider again and so on it goes. Luckily, such cases are detectable and resolved with Guice SPI as well: cases when type Foo is bound to Foo are resolved by binding Foo to constructor of Foo, which will not trigger the circular injections.

What are we getting

  • Faster UI - less time to start apps and sub-apps 
  • Smaller object allocation when starting apps
  • Injector is averagely 300Kb+ (up to 500kb on EE) - with many users using the system at the same time, this is a memory saver
    • 1 injector per admincentral instance + 1 per each app + 1 per each subapp - can grow to pretty big numbers easily.
  • No more Guice in http session => smaller sessions which now can be serialised if we do other things right.
  • Less boilerplate code
    • UiBaseModule provides lot's of UI essentials 
  • Backdoors for the IoC workarounds: when in trouble (need a compatible c-tor or similar case) it is possible to just pull the instance from the VaadinSession (via SessionStrore abstraction)
  • Lot's of type mappings are shaved-off (no need to map the type twice to the same implementation just so that it is injectable in different contexts => most of the choose-dialog mappings are gone).

Next

  • View scopes are underused at the moment, they could help us create more efficient and flexible apps.
  • https://github.com/Netflix/governator - a set of Guice extensions which brings in some interesting features like auto-binding (does not mean we could abandon XML, but would allow to arrange instance exchange between contexts).
  • Further steps towards serialisability: proxy providers for the "main". GuiceComponentProvider allows a parent provider to be specified and all the parent bindings are available through a simpler wrapping provider. We could make a step further and replace simple providers with serialisable proxies in case of UI.

4 Comments

  1. How do the AdminCentralAppSubApp and View scopes relate to the scopes of components defined in module XML files, respectively the <components> sections therein?

  2. Hi Vivian,

    The XML notation hasn't changed much and is as limited as it used to be. Here's how it relates to the new annotations:

    Imagine you want to declare a component that is admincentral-scoped

    • with new annotations you'd want to use @Admincentral (or @AdmincentralEager);
    • to get the same with XML you would use the components id admincentral and would you singleton as a scope (i.e. you get the admincentral-scoped singleton).

    The same logic applies to apps and sub-apps. The view case is different - there's yet no way to declare view-scoped components with XML. Otoh neither there is a clear framework for the view components with Java, we use them for some internal use cases like e.g. choose dialogs. 

    hope this helps, feel free to ask further

    --

    sasha

    1. Hi Sasha,

      thanks a million for this clarification. It now makes sense to me.

      Exactly with the view scoped dependencies of choose dialogs we're facing big trouble when migrating from 5.5.4 to 5.5.7:

      • prior 5.5.7 we injected SubAppContext without problems into our custom ContentConnector (e.g. for a "Countries" app) ; we do this – beyond other things – to access the current authoring locale selected in the detail subapp
      • the same ContentConnector is used for both, the Countries browser subapp as well as for the choose dialog when accessing the App from another app, i.e. a link field (say from the "Projects" app); re-using the same content connector for browser subapp and choose dialog is a common practice in Magnolia, if not the default, right?
      • in the same way we could have injected @Named("subapp") EventBus to access the subapp-scoped event bus from both, the browser subapp and choose dialog
      • now, with 5.5.7 injection of SubAppContextand @Named("subapp") EventBus fails (only) when loading the choose dialog: it turns out that – in case of the choose dialog – only ViewScoped beans are injectable, and not the subapp scoped beans anymore or at least not directly. SubAppContext in this case referred to the calling app (e.g. the Project details subapp containing the link field that opened the choose dialog, not the targeted app, e.g. the Countries app); that said, we found out that it is possible to inject UiContext and cast that guy to SubAppContext again, so it's just a matter of binding respectively accessing the scopes. Sadly, ComponentProvider lists all available beans for injection while debugging (including SubAppContext with App scope ) but does not allow to qualify the getInstance call with that scope annotation. Even trying to access that App-scopedSubAppContext via casting to GuiceComponentProvider and accessing with getInjector and appropriate Key didn't help – it always failed with some No BeanStore for xyz exception if I remember correctly.
      • finally, we found that curious snippet somewhere in your code: 

        final EventBus eventBus = SessionStore.access().getBeanStore(UiContextReference.ofSubApp(subAppContext)).get(Key.get(EventBus.class, Names.named(SubAppEventBus.NAME)));

        ...and that crazy piece of code even gives us the right SubApp scoped EventBus from within the choose dialog content connector. (again, prior 5.5.7 this could have simply been written as @Named("subapp") EventBus eventbus which looks way simpler (wink) )

      Can you still follow me? (smile) Does this make sense to you? Summarized to a simple question:

      How to inject a SubApp scoped EventBus within a content connector that is used in a choose dialog (i.e. view scoped bean)?

      Any further insights are much appreciated.

      Anyway, beyond those troubles, the quite tough refactoring of that stuff is really great – performance-wise (wink)

      Thanks & cheers,

      Vivian

      1. I see the point! Some things aren't clear to me though. Prior to this thing we did, a choose dialog was initiated by the app component provider and => existed in the 'app scope', no sub-app ctx should've been available to the chooser's components (at least from what I know). Any chance you're triggering the chooser yourself somehow in a custom way and push the sub-app's component provider to e.g.ChooseDialogComponentProviderUtil?