The tricky CDI ServiceProvider pattern

CDI is really cool, and works great. But it’s not the hammer for all your problems!

I’ve seen the following a lot of times and I confess that I also used it in the first place: The CDI ServiceProvider pattern. But first let’s clarify what it is:

The ServiceProvider pattern

Consider you have a bunch of plugable modules and all of them might add an implementation for a certain Interface:

// this is just a marker interface for all my plugins
public interface MyPlugin{} 

The idea is that extensions can later easily implement this marker interface and the system will automatically pick up all the implementations accordingly.

public class PluginA implements MyPlugin {
  ...
}

public class PluginB implements MyPlugin {
  ...
}

The known way

I’ve seen lots of blogs recommending the following solution:

public static  List getContextualReferences(Class type, Annotation... qualifiers)
{
    BeanManager beanManager = getBeanManager();

    Set<Bean> beans = beanManager.getBeans(type, qualifiers);

    List result = new ArrayList(beans.size());

    for (Bean bean : beans)
    {
        Bean<?> bean = beanManager.resolve(beans);
        CreationalContext<?> creationalContext = beanManager.createCreationalContext(bean);
        result.add(beanManager.getReference(bean, type, creationalContext););
    }
    return result;
}

The original implementation can be found in the Apache DeltaSpike BeanProvider (and in Apache MyFaces CODI before that).

This worked really well for some time. But suddenly a few surprises happened.

What’s the problem with getBeans()?

One day we got a bug report in Apache OpenWebBeans that OWB is broken because the pattern shown above doesn’t work: JIRA issue OWB-658

What we found out is that the whole pattern doesn’t work anymore once one single plugin is defined as @Alternative

I digged into the issue and became aware that this is not an OWB issue but BeanManager#getBeans() is just not made for this usage! So let’s have a look about what the CDI spec says:

11.3.4:
The method BeanManager.getBeans() returns the set of beans which have the given required type and qualifiers and are available for injection

The important part here is the difference between “give me all beans of type X” and “give me all beans which can be used for an InjectionPoint of type X”. Those 2 are fundamentally different, because in the later case all other beans are no candidates for injection anymore if a single @Alternative annotated bean gets spotted. OWB did it just right!

Possible Solution 1

Don’t use @Alternative on any of your plugin classes 😉

That sounds a bit ridiculous, but at least it works.

Possible Solution 2

You could create a Message which collects all your plugins in a CDI-Extension-like way.
First we need a data object to store all the collected plugins.

public class PluginDetection {
  private List plugins = new ArrayList();

  public void addPlugin(MyPlugin plugin) {
    plugins.add(plugin);
  }

  public List getPlugins() {
    return plugins;
  }
}

Then we gonna fire this around:

private @Inject Event pdEvent;

public List detectPlugins() {
  PluginDetection pd = new PluginDetection();
  pdEvent.fire(pd);
  return pd.getPlugins();
}

The plugins just need to Observe this event and register themself:

public void register(@Observes PluginDetection pd) {
  pd.add(this);
}

Since the default for @Observes is Reception.ALWAYS, the contextual instance will automatically get created if it doesn’t yet exist.

But there is also a small issue with this approach: There is no way to disable/override a plugin with @Alternative anymore, since the messaging doesn’t do any bean resolving at all.

Advertisements

About struberg
I'm an Apache Software Foundation member blogging about Java, µC, TheASF, OpenWebBeans, Maven, MyFaces, CODI, GIT, OpenJPA, TomEE, DeltaSpike, ...

6 Responses to The tricky CDI ServiceProvider pattern

  1. Philipp says:

    Hello
    I was actually involved in the filing of the bug 658. But I think open web beans got it wrong: what we tried to achieve works with the reference implementation (Weld).

    To make our point more clear, we wrote a set of tests and ran them with weld and open web beans. Here is the new bug report: https://issues.apache.org/jira/browse/OWB-667

    Cheers,
    Philipp

    • struberg says:

      Hi!

      I’ve tested the above mentioned pattern with Weld some time ago and got the same behaviour than OWB. Also please note that the facts written in the spec are more important than Weld.

      I’ll check whether the sample you posted is different than the aforementioned pattern. But it looks that you’ve just spotted a non-portable behaviour, so you better don’t use it.

      LieGrue,
      strub

  2. I must be missing a point… Why not simply use an Instance injection point?

    @Inject
    @Any
    private Instance plugins;

    public void collectPlugins() {
    Iterator it = plugins.iterator();
    while( it.hasNext() )
    {
    MyPlugin plugin = it.next();
    // do something with it
    }
    }

    Cheers,
    Harald

    • struberg says:

      Hi Harald!

      I fear Instance shares the same spec problems than getBeans(). The paragraph points to 5.2. which is not clear if 5.2.1 is also ment or not. From looking at the definition of Instance#get which explicitely mentions 5.2.1 in addition, I’m now tempted to think that the @Alternatives should not get filtered in the Instance#iterator() already.

      I’ve also talked with fellow CDI EG members in the meantime and it looks Weld now returns all the Beans in getBeans() and Instance#iterator() and sorts out all non @Alternatives later in BeanManager#resolve(). I already thought a about that the last few days, and I’m now 85% that we should also change OWB to handle it the same way.

      In any case please note that this still doesn’t solve your problems. You will just get the same behaviour as with the event solution: You might end up with too many beans. Because you will also get all the beans which are ‘overwritten’ by an @Alternative that way…

      LieGrue,
      strub

  3. Interesting… up to now; I had assumed you could just select one of several alternatives, and I didn’t realize that a selected alternative knocks out all non-alternatives of the same type (§ 5.2.1).

    Taking another close look at the spec, I now think the spec is rather clear, Weld is right and OWB is wrong.

    The funny thing is that Instance.get() may return a unique bean even though Instance.isAmbiguous() is true.

    The types of the beans returned by Instance.iterator() correspond to BeanManager.getBeans().
    Similarly, Instance.get() corresponds to BeanManager.resolve().

    What’s missing on both interfaces is something like “give me the set of candidates remaining after ambiguity resolution without throwing an exception if there’s more than one”.

    And Instance does not allow you to check if one of the beans returned by the iterator is an alternative, you have to use the BeanManager for that.

    Maybe CDI 1.1 could add corresponding methods to Instance and BeanManager…?

    • struberg says:

      > I now think the spec is rather clear, Weld is right and OWB is wrong.

      I don’t think so (and Pete did neither when I asked him on irc). There is a big distinction between ‘The spec says it must be…’ and ‘The spec allows for …’

      In the getBeans() case it is currently not clear if the reference to 5.2 also implies the sub-chapter 5.2.1 or not. The description of Instance explicitly separates between 5.2 and 5.2.1 – but getBeans() does not.

      Of course, I do agree that it is just much more sane to split those functionality the way it is now in Weld (it used to be different in the old days). But for relying on it from a users perspective we will need to make this clear in the CDI-1.1 spec. I’m already working on the spec Jira for it.

      Thanks for the tip with the Instance iterator. Can you please post this to the cdi spec mailing list? Maybe we have some more stuff to clarify in this area. In any case it’s good to make people aware of this unexpected behaviour.

      Btw, please again note that this still will not solve your problem with the ServiceProvider as you might now get too many beans! (You will get all the default + the enabled @Alternatives).

      LieGrue,
      strub

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: