Flow: A sketch for enhanced OSGi support in Flow

Created on 2 Feb 2019  路  26Comments  路  Source: vaadin/flow

Hi,

I've followed the ongoing effort of making Flow OSGi capable. Thank you for this great work by the way. However, this effort seems to aim at merely making the Flow framework run within an OSGi container, but not to enable OSGi developers to incorporate Flow in a pure OSGi application, e.g., making heavy use of Declarative Services (the DI of OSGi).

Therefore, I have tried to sketch a solution on how we might enhance the current version of Flow to enable OSGi developers to make @Route/@RouteAlias annotated classes also be OSGi components whose lifecycle is controlled by OSGi instead of Flow.
Additionally, this solution would not need any invasive code changes in Flow core, as the current solution does with classes like OSGiAccess and others, but merely would be an additional bundle that can be deployed if running in an OSGi container but doesn't have to if Flow is used in another context.
This would also be more similar to the OSGi integration offered by Vaadin 8.

As said above, I already have tried to sketch a possible solution that requires as few changes as possible to existing code. This with additional explanations on how this might work can be found here:
https://github.com/Sandared/flow-osgi

This sketch is not yet working. The places where additional work and input is needed are commented with TODOs
I'm sure I didn't cover all aspects of what is needed to make such an enhanced integration work, as I'm not very familiar with the inner workings of Flow. So any feedback is highly welcome. :)

Kind regards,
Thomas

OSGi investigation

All 26 comments

I've updated the repository to also include a proposal on how the FlowOSGiInstantiator might create OSGi components when asked for a route target. Additionally, I had a look into the ServiceLoader mechanisms in OSGi and the Instantiator should now be able to be picked up by Flow instead of the DefaultInstantiator

@pleku / @denis-anisimov / @mehdi-vaadin I mention you because I saw you all are deeply involved in the osgi integration. What do you think about this proposal? Is it something worth further investigating? (I will gladly help where I can) Or do you not (yet) plan to include Declarative Service integration for Vaadin 10+?

Kind regards,
Thomas

@Sandared Hi and thanks for the contribution! Sorry for late response - we've been a bit busy lately with finalizing things for V13 beta release.

I agree that we could make a lot more for OSGi rather than just _making it work_, so we welcome all improvement suggestions and contributions. We will take a look at this during the next sprint if not earlier. It will take us some time, since I have to admit that our OSGi experience is limited.

Thank you @Sandared 馃憤

@pleku / @mehdi-vaadin that's good to hear. I just wanted to make sure that I'm not wasting your and my time with this issue ;)

I really would love to see this integration work out, so if you need anything regarding OSGi (tips/hints/design/explanations/implementations) I will gladly help you out whenever I can. Just write me.

BTW:
From what I saw during digging through the framework code I think that the OSGi integration can even be done without (or just minimal) changes to other parts of the framework, so maybe we are able to free the core part of the framework from OSGi dependencies completely.

I refactored my initial sketch a little bit.
What it now contains is the following:

  • An Instantiator that registers in Flow via ServiceLoader mechanism and that takes care of creating OSGi declarative services for route targets
  • An Initializer that registers Flow's ServletDeployer and JSR356WebsocketInitializer in the right order
  • An abstract BundleTracker that can be used to mimic the behavior of ServletContainerInitializer which is shown exemplarily for @Routes and @RouteAlias in FlowOsgiRouteTracker
  • A FlowOsgiRouteRegistry that takes care of registering Routes/RouteAliases found and registered by the FlowOsgiRouteTracker (This would probably make make the exisiting OSGiDataCollector and OSGiRouteRegistry superfluous )
  • A FlowOsgiRouteRegistryInitializer that takes care of setting the FlowOsgiRouteRegsitry in all ServletContexts so they can be found by other non-OSGi components via the ServletContext.

This implementation does not touch any internal code.

If you have any questions regarding implementation/design then don't hesitate to ask.

Kind regards,
Thomas

A FlowOsgiRouteRegistry that takes care of registering Routes/RouteAliases found and registered by the FlowOsgiRouteTracker

Did you take into account that Vaadin 13 (i.e. Flow 1.3) adds support for dynamically adding and removing routes without requiring any special route registry implementation?

No I didn't. I wasn't aware that this feature is finished already. This could make the implementation even simpler 馃憤
As you are investigating this topic now: There is an ongoing discussion on the OSGi dev mailing list about how to best implement the Instantiator which basically connects Flow with OSGi comopnents. I think my sketch still has some flaws, e.g., typing a fqcn for each component that is a route is rather errorprone.

@Legioth after discussing this issue on the mailing list it turns out there is a much simpler and safer way to implement this (also thanks to the dynamic RouteRegistry). I've updated my sketch in my repo. The new classes needed to make Routes also OSGi Components are in the v2 package.

A short explanation:
Routes that want to be components should look like this:

@Route("")
@Component(scope=ServiceScope.PROTOTYPE, service=Component.class) // or HasElement.class ?
public class MyUI extends VerticalLayout{...}

In the Flow-OSGi integration we then can use a whiteboard to collect all these Routes/Components

@Component 
public class FlowOSGiRouteWhiteboard {
  @Reference(cardinality=ReferenceCardinality.MULTIPLE, policy=ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY)
  void addRoute(Component route) { // or HasElement?
    // RouteRegistry.addRoute()
  }
  void removeRoute(Component route){
    // RouteRegistry.removeRoute()
  }
}

The Instantiator then can make use of OSGi's ServiceObjects (Kind of Factory for all @Component annotated class that have scope PROTOTYPE) to create instances of the respective Route:

public class FlowOsgiInstantiator extends DefaultInstantiator {
...
  private Component createComponent(Class<?> routeTargetType){
    String filter = "(" + ComponentConstants.COMPONENT_NAME + "=" + routeTargetType.getName() + ")";
    Collection<ServiceReference<Component>> refs = context.getServiceReferences(Component.class, filter);
    ServiceObjects<Component> so = context.getServiceObjects(refs.iterator().next());
    return so.getService();
  }
}

I hope this helps you for your implementation :) Can't wait to write my first Flow-OSGi application ;)

Kind regards,
Thomas

Two small remarks:
1) I guess @Component and Component.class actually refer to two completely different Component types, which means that either would need to be expressed using the fully qualified name instead? We have the same problem in the Spring integration where we have created an @SpringComponent annotation that works as an alias for Spring's own @Component annotation. Is the same kind of aliasing possible with OSGi? Otherwise, the idea of using HasElement might be a good alternative.
2) I guess the whiteboard should explicitly filter out instances that are not annotated with @Route, since it would otherwise cause problems if you would for some reason add @Component(scope=ServiceScope.PROTOTYPE, service=Component.class) to other component classes as well.

1) AFAIK this is not possible. The OSGi @Component annotation is used at compiletime by tools like bnd to generate the corresponding declarative service XML. But I've asked this question on the OSGi mailing list. If there is a possiblity I will notify you :) If it is indeed not possible at all to alias the OSGi @Component annotation, then I would either recommend to use the FQCN for the OSGi annotation, as there is at most one per class, or (as you noted) use the HasElement if this is sensible.
1) You're totally right. Sorry for that missing part. It was late when I wrote the comment ;)

Rather than FQCN, I'd then recommend using HasElement as the service type. Alternatively, a separate marker interface could maybe be introduced for this purpose, e.g. RouteComponent.

Here are my comments after the code review.

Common observation: it would be very useful to have a demo of the features which are the purpose of this ticket. The demo https://github.com/Sandared/flow-osgi/blob/master/flow.osgi.simpleui/src/main/java/io/jatoms/flow/osgi/simpleui/MainView.java doesn't show any enhancement usecase.

Only readme contains the code snippet which is the main enhancement as I understand:

@Route("")
@Component(factory="io.jatoms.example.MainView")
public class MainView extends VerticalLayout {

    @Reference
    GreeterService greeter;

    public MainView() {
        Button button = new Button("Click me",
                event -> Notification.show(greeter.greet()));
        add(button);
    }
}

I have 3 topics of the comments for the ticket/enhancements/source code:

  • Feature description as a requirement for enhancement.
  • API/SPI level. Some top level implementation details with the usage examples.
  • Implementation discussion (comments for the code https://github.com/Sandared/flow-osgi).

Let's start from the first topic.

So as I understood from the readme example you would like to instantiate Vaadin route components via OSGi mechanisms so that they may be considered as services which allows to use DS inside them.
In the example you are using injection of a GreeterService via @Reference annotation.

As a feature request it has sense of course.

But I would like to clarify this enhancement.
In fact this functionality is quite similar to our Spring plugin: Vaadin components are instantiated via framework mechanisms.
Just to clarify the functionality which Spring plugin provides:

  • ability to autoinject other beans/components/services into the component instances.
  • declare components within scopes so it's possible to reuse existing instance within the scope when it's injected

The second usecase is not applicable for OSGi since there are limited number of service scopes and they are quite OSGi specific (not application specific). So the only meaningful scope which may be used here is prototype scope. As a result we just ignore the second usecase.

The first usecase is semantically the same in Spring and OSGi case.

So the feature request is: instantiate components within OSGi env using OSGi mechanisms so that they are considered as services with prototype scope. It will allow to use annotations/declarative service injections.

And yes, that makes sense.

API/SPI level.

First of all:

  • You suggest to use @Component(factory="io.jatoms.example.MainView") annotation for route targets to be able use a component as a service.

I have several notes here:

  • It's quite easy to forget factory parameter here. As a result a singleton service will be used which is an error and it will break the application.
  • Not sure why it's not scope=ServiceScope.PROTOTYPE.
  • Anyway it's too error-prone. I would suggest to introduce some custom annotation which register the component as a service.

So instead of having

@Route("")
@Component(factory="io.jatoms.example.MainView")
public class MainView extends VerticalLayout {

It will be just

@Route("")
@OSGiComponent
public class MainView extends VerticalLayout {

or whatever.

OSGiComponent annotation would be added to the class which you would like to register as a service with prototype scope.

In fact you don't even need OSGiComponent annotation for @Route target class at all.
Every @Route target class ( @RouteAlias may not be used without @Route so no need to mention it here) will be automatically registered as a service.

A little bit code: I see you already do something similar here https://github.com/Sandared/flow-osgi/blob/master/flow.osgi.integration/src/main/java/io/jatoms/flow/osgi/integration/FlowOsgiRouteTracker.java#L52.

So I'm not sure why you declare @Component(factory="io.jatoms.example.MainView") at all.
Every route component automatically becomes as a service.

May be I miss something here: if you register a service programmatically then you can't use declarative way of injection ? You mentioned in your answer to Leif that @Component annotation is handled at the build time and annotation processor generates service manifest file based on it.
But it's about @Component . We don't have to register service via DS . We may do it programatically.
But I'm not sure about other annotation usage.

Well, I've checked the RetentionPolicy for the @Reference annotation and it looks like it also used at the build time.... Though I'm not sure that it won't be handled in case the class is not declared as a @Component.

So, what I'm trying to say: there is no need to use @Component annotation. We may introduce our own annotation like OSGiComponent and register every class with this annotation as a service programmatically during the class scanning in the same way as we do for @Route classes.
This needs verification.

Instantiator implementation.

We need of course a custom Instantiator implementation to achieve our goal.

But several notes here:

  • We should not limit ourselves to only components as service. The implementation https://github.com/Sandared/flow-osgi/blob/master/flow.osgi.integration/src/main/java/io/jatoms/flow/osgi/integration/FlowOsgiInstantiator.java#L46 uses filter to limit the components which should be instantiated . In fact it should be possible to instantiate _ANY_ route target even if it's not a service. Just use different strategies: if it's a service then instantiate it as a service, if not then fallback to the default impl. But if we are going to make every @Route as a service out of the box this comment is not applicable anymore.
  • I'm not sure that we should use ServiceLoader mechanism to register a custom Instantiator for OSGi. It requires osgi.serviceloader extender. In fact we already require this extender because of one of our dependencies (ph-css) but it might be that we will get rid of it at some point. So I would avoid using service loader in OSGi at all. I think a custom Instantiator may be provided in the same way as it's done in Spring: directly from a subclass of VaadinServletService.
  • Instantiator does many things related to discovering/reusing/instantiating classes. You may check the implementation for Spring here: https://github.com/vaadin/spring/blob/master/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringInstantiator.java. Check the getServiceInitListeners and getOrCreate methods.

The getServiceInitListeners method may also use OSGI service mechanisms to return service init listeners (in addition to the default one or instead) like it's done for Spring.

The getOrCreate method is used in many places. Instantiator delegates to it the createRouteTarget method execution. So it's enough to implement getOrCreate properly.

And the getOrCreate method is used e.g. also in case when you use @Id injection:

@Tag("main-view")
@HtmlImport("xxxx")
public class MainView extends PolymerTemplate<TempalteModel> {
     @Id
    private AnotherComponent anotherComponent;

So it allows to instantiate AnotherComponent also via OSGi mechanisms. And it will allow to use OSGi features inside AnotherComponent class as well (needs to be verified).

Implementation discussion

I've already mentioned that :

  • it makes sense to introduce OSGiComponent and register components programmatically instead of declaratively.
  • it makes sense to subclass VaadinServletService for OSGi and create a custom Instantiator in it in the same way as it's done in Spring add-on.

One implementation question: I totally don't understand the code here https://github.com/Sandared/flow-osgi/blob/master/flow.osgi.integration/src/main/java/io/jatoms/flow/osgi/integration/FlowOsgiRouteRegistryInitializer.java
How does it work ? And does it work at all ?

I think there is no sense to discuss other implementation details since they are just impl details and may be changed ....

So I think I don't have anything else.

I've removed myself from this ticket to allow someone else to review it since several people are supposed to do it.

Hi @denis-anisimov ,
regarding your comments on

  • Feature description as a requirement for enhancement: The feature request description given by you is spot on.
  • API/SPI level:

    • In my second (enhanced) sketch I used ServiceScope.PROTOTYPE just as you suggested and it is definitly the better solution, so I would go with that one instead of factory.

    • Registering @Routes as services doesn't make them components which are managed by a Service Component Runtime. So only registering them as services will not inject references and therefore such a solution would be rather useless from an OSGi perspective. Another aspect might be that an OSGi developer would like to use other mechanisms that are coupled to a @Component annotation, like @Activate/@Decativate annotations or @ComponentPropertyTypes. Below I showed a small component and highlighted what would not be working if Flow would register it programmatically as a service:

// If this component would instead be instantiated by Flow 
// and subsequently registered as a service, 
// then the following things would not work:

@Route("")
// ComponentPropertyType annotations would have no effect
@RequireEventAdmin
@EventTopics("/my/topic")
@Component 
public class MainView extends VerticalLayout implements EventHandler{
   // Activation Fields would not work, those would stay null
   @Activate
   private ComponentContext context;
   @Activate
   private BundleContext bc;

   // References would not be injected (This is CRUCIAL)
   @Reference
   private SomeBusinessLogicService sbls;

    // Lifecycle methods would not be invoked 
   @Activate
   void activate (Map<String, Object> props){...}

   @Deactivate
   void deactivate(){...}

   // This method will never be invoked, as ComponentPropertyTypes
   // will have no effect on programmatically registered services
   @Override
   public void handleEvent(...){...}
}

A better solution to programmatically create components that are routes would be to annotate them with @Component(scope=ServiceScope.PROTOTYPE) and then use ServiceObjects<Component>#getService(). This delegates component creation to a service component runtime which instantiates the above component properly and also takes care of adding all properties/injecting all references/calling lifecycle methods. Such an approach I have already shown in my second version of the sketch: https://github.com/Sandared/flow-osgi/blob/master/flow.osgi.integration/src/main/java/io/jatoms/flow/osgi/integration/v2/OSGiInstantiatorV2.java (Sorry if has been somehow misleading to have both versions in one repository :( All you would need now for routes is in the V2 subpackage)

Summarizing: I would STRONGLY DISCOURAGE to drop the declarative @Component annotation in favor of an implicit service registration! OSGi developers are used to @Component and the dependency injection framework around it. Stripping this possibilty from them would cripple the Flow-OSGi integration in every sense.

  • Regarding your question on FlowOsgiRouteRegistryInitializer.java: This one was used to set the OSGiRouteRegistry in version 1 of my sketch in every ServletContext, so that subsequent calls to RouteRegistry.getInstance() would return my OSGiRouteRegistry instead of any other Registry. This approach seems not to be needed anymore, as we now have a RouteRegistry where I can dynamically add/remove Routes. All you now need are the two classes in the v2 subpackage of my sketch.

Finally: If there are any questions regarding OSGi mechanisms please don't hestiate to ask (here/Twitter/Skype/whatever) :) I will gladly help where I can to make this integration work in a way both, Flow and OSGi, will profit from.

Kind regards,
Thomas

Regarding to your comments

If this component would instead be instantiated by Flow 
and subsequently registered as a service, 
then the following things would not work

Good to know that this doesn't work out of the box.
But it doesn't mean we can't do it working.

I have to repeat my point here:
using @Component(scope=ServiceScope.PROTOTYPE) is error-prone.
This is the code which you have to write every time. And if you miss ServiceScope.PROTOTYPE then it's an error since the singleton scope will be used which breaks the application.

So there are two reasons to avoid this: avoid mistakes and avoid writing the same long line everywhere.
I'm pretty sure that if we can avoid this then it's better than require it all the time.

That's my opinion which I just want to express here. It's fine that there are other opinions.
The purpose of my comments is just a feedback and not make a final decision.

Back to the @Activate/@Reference, etc annotations: I believe those annotations don't represent self-contained functionality. I mean here that they are just provide a declarative way of something which is already available via API (via programatic way).
So most likely we may use this API and make those annotations work in a different way.
As I mentioned the @Reference annotation has RetentionPolicy CLASS which means most likely it's used by an annotation processor to generate things at the build time.
Then the generated data is used at runtime instead of direct annotation introspection.
It might be that we may find a way to call API at runtime which will do the same even of we register a service programatically. This needs investigation.
Again: this is my opinion in my feedback. It's not a final decision.

The very least thing which we may do : make our own maven plugin which generates descriptors.
This is undesirable and we most likely won't go this way but it's still worth to mention.

FlowOsgiRouteRegistryInitializer : I believe you meant FlowOsgiRouteRegistry by OSGiRouteRegistry. In this case there is nothing to talk about since it's not implemented anyway.

Hi Denis,

you are right with respect to error-proneness of having to write the same line over and over again and not be allowed to change the service scope.

If you want to overcome this by introducing a new @OSGiComponent annotation, then I see two possible ways to do so. First, you write your own build plugin that takes care of translating this new annotation into valid DS XML like bnd currently does for the standard annotations. Second (the one you mentioned), at runtime you somehow manage to hook into the ServiceComponentRuntime to create new components programmatically.

I know the follwing is some sort of naysaying but I want to make you aware of what has to be done for both approaches in order to work properly in an OSGi environment.

BUILDTIME:

  • Not only the component annotation has to be transformed into valid XML, also its attributes (aside from scope), e.g., immediate, property, name, etc.
  • Additionally you would have to make sure that ALL other annotations are transformed right:

    • @Activate/@Decativate/@Modified for the well defined lifecycle of an immediate/delayed DS

    • @Reference for scalar and collection reference working for field/method/constructor injection

    • @Activate for activation fields

    • The passing of all eligible lifecycle parameters to the corresponding annotated methods, e.g. BundleContext / ComponentContext/ Map for properties etc.

    • ComponentPropertyTypes and annotations like @Requires/@Provides

      Those are all connected to the @Component annotation in OSGi. Another point is that if you introduce a new @OSGiComponent annotation, then you might break OSGi developer tools like bndTools or the amdatu OSGi plugin for IntelliJ, as both don't allow you to use @Activate/@Reference/etc. annotations within a class that is not annotated with @Component.

RUNTIME:

  • For a full description of the behavior of declarative servcies have a look at the standard specification: https://osgi.org/specification/osgi.cmpn/7.0.0/service.component.html#service.component . I assume you don't want to rebuild this entirely from scratch.
  • Another option might be to somehow hook into SCR in order to programmatically create components at runtime without doing the heavy lifting mentioned before. This would be a solution that I myself would like to have, but at the same time I doubt that it is possible, as the API of SCR is rather restricted to introspection, see https://osgi.org/specification/osgi.cmpn/7.0.0/service.component.html#service.component-service.component.runtime . There is no API that allows you to do something like SCR#createComponent(Class clazz). I would love to have something like that, but the closest that is offered by OSGi is to declare your component to have scope PROTOTYPE and then use ServiceObjects<Type>#getService which is also the way how other reference implementations of OSGi compendium spec are implemented, e.g., Apache Aries for JAX-RS support in OSGi.

That all said, I totally agree with you, that the repetitive use of @Component(scope=ServiceScope.PROTOTYPE) is error-prone and also somehow annoying, but currently (to the best of my knowledge) it is the only (sensible) way to create DS components programmatically on demand. As long as you don't want to break existing tooling or write everything from scratch I think this is the only way to get this to work.

The ticket about implementing the requested functionality : https://github.com/vaadin/flow/issues/5125

Servlet auto-registration ticket : #4796

Closing this ticket since it's splitted up to the number of other tickets mentioned here in comments.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

pleku picture pleku  路  4Comments

marcushellberg picture marcushellberg  路  4Comments

anezthes picture anezthes  路  4Comments

mstahv picture mstahv  路  3Comments

mcollovati picture mcollovati  路  4Comments