Can Bnd generate a "factory" service like LoggerFactory?

Hi,

I have been trying to use Bnd to create a service which can be used like LoggerFactory can:

@Reference(service = LoggerFactory.class)
Logger logger;

I understand this capability to be a new feature of DS 1.4. However, I am struggling to find the correct combination of instructions to do this. The Felix Log bundle apparently uses a Bundle Activator to register a ServiceFactory - both practices which AFAIK are obsolete. The @Component(serviceFactory=xxx) element is also deprecated in favour of scope(), which suggests it isn’t what I want anyway.

Does Bnd support creating a service that can do this please? Or can it only be achieved through hand-coding inside a Bundle Activator? However, I don’t understand how the Bundle Activator can be the solution either, because Bnd fails when it tries to parse my own factory service @Reference annotation with this message:

error : No interface specified on instance

(where instance is the name of my constructor parameter) whereas it parses an equivalent factory reference using LoggerFactory without any problem at all :confused:.

Thanks for any help here,
Cheers,
Chris

DS 1.4 has special support for LoggerFactory/Logger only. This support is enabled through special runtime support in Service Component Runtime (along with Bnd not declaring a type mismatch between the field type and the service type).

There is no support in DS for doing this for your own custom service.

Oh, OK. Thanks for replying, although it’s a bit disappointing :cry:. However, it occurs to me that I could still register an implementation of ServiceFactory via a Bundle-Activator though, like the Felix Log bundle does, so that I could write

@Reference
Bongo bongo

in the usual way, instead of

@Reference(service = BongoFactory.class)
Bongo bongo

My underlying problem being that only BongoFactory can be annotated as a @Component, although we only really want to use Bongo instances.

Does this seem like a worthwhile thing to try please? Or do you think I would only be storing up future pain?

Thanks for any advice,
Cheers,
Chris

I am not sure what your actual goal is here. If your goal is to create a new instance of your service component for each bundle consuming the service, which is basically the purpose of ServiceFactory, then just register your service component with scope=ServiceScope.BUNDLE.

@Component(scope=ServiceScope.BUNDLE)
public class BongoImpl implements Bongo {}

This tells SCR to create a distinct component instance for each consuming bundle. To know which bundle the component instance is associated with, have the activate method take a ComponentContext and its getUsingBundle() method will tell you.

Then you don’t need to mess with BundleActivator and ServiceFactory.

Well, the issue is that Bongo objects have constructor parameters that are not themselves injectable. I can only inject a BongoFactory and then create an new Bongo instance.

Cheers,
Chris

OK. This is why interface types make better service types than concrete types.

:+1:

Completely coincidentally, I happened to be looking at this exact same issue today. Like @chrisr3, I was excited as I remembered that the Logger/LoggerFactory has exactly this capability. And like @chrisr3, my excitement was short-lived when I discovered that this capability had been hacked into bnd & DS as a special case for Logger/LoggerFactory.

@bjhargrave
OK. This is why interface types make better service types than concrete types.

I don’t think this is relevant. “Logger” is an interface type - you still found it necessary to implement a factory pattern. At the fundamental level, this was necessary because the concrete logger implementation requires construction parameters. In this case, the shorthand can still work because all the necessary parameters (Bundle, type, etc) can be inferred or set to sensible defaults based on the caller context. Then each caller gets a concrete Logger implementation that is tailored for its context. It is not hard to think of other situations where this pattern might also be useful.

In my particular case, I have a webservice client which needs username/password set up during construction. I can determine the username/password required automatically based on caller context. At the moment, I have to inject a factory service and invoke the “newService()” method to generate the actual instance that I want to work on. This is eminently feasible and it works. But being able to use the @Reference(service=XXX.class) shorthand would have been neat and would have eliminated this boilerplate (which are presumably the same reasons that this shorthand was introduced for the Logger service in the first place).

Just a thought. There might be valid reasons why this wouldn’t work.

I have just tried the following:

@Component(service = [BongoFactory::class])
class BongoFactory @Activate constructor(context: BundleContext) {
    private val registration = context.registerService(arrayOf(Bongo::class.java.name), BongoServiceFactory(), null)

    @Deactivate
    fun done() {
        registration.unregister()
    }

    private inner class BongoServiceFactory : ServiceFactory<Bongo> {
       // etc
    }
}

which does at least compile. However, Bnd’s Resolve task rejects any attempt to inject

@Reference
Bongo bongo

presumably because my dynamic service registration has no XML metadata :cry:. But apart from that, this does seem to work at runtime! Is there any way to get the Resolve task still to pass please?

Cheers,
Chris

P.S. And I’ve just realised that I can annotate BongoServiceFactory as a @Component too, and suddenly the Resolve is happy! Is this legal?

P.P.S. Hmph, I guess it’s not legal as it doesn’t seem to work in the general case.

OK, so this works:

@Component(service = [BongoFactory::class])
class BongoFactory @Activate constructor(context: BundleContext) {
    private val registration = context.registerService(arrayOf(Bongo::class.java.name), BongoServiceFactory(), null)

    @Deactivate
    fun done() {
        registration.unregister()
    }

    // Provides the XML metadata for the dynamic service registration, to keep Resolve happy.
    @Component(service = [ Bongo::class ], enabled = false)
    private abstract class DummyBongo : Bongo

    private inner class BongoServiceFactory : ServiceFactory<Bongo> {
       // etc
    }
}

I am hoping there is nothing “untoward” about that…

Cheers,
Chris

in Java this pattern is simply:

@Component(service = Bongo.class)
public class BongoFactory implements ServiceFactory<Bongo> {
  public Bongo getService(Bundle bundle, ServiceRegistration<Bongo> reg) {
     return new BongoImpl(par1);
  }
  @Reference
  AnyConstructorParamsToBuildBongoImpl par1;
}

I did try something like this, but Bnd complained that BongoFactory was not assignable to Bongo. Presumably because it only implements ServiceFactory<Bongo> and not Bongo itself.

Cheers,
Chris

This wont work as you hope because DS has no provision for a component class to be the actual ServiceFactory. SCR will always be the service factory and a component class will always be the actual service object.

right right of course! you need to use the property @Component.scope (As you stated) to alter the behaviour of the factory. I should just quit all mondays.

This may cause the resolver to accept that there is a Bongo service being registered but, in reality, there is no Bongo service being registered. Just a BongoFactory service.

There is no way to “trick” DS and Bnd into doing what you want. DS does not support that. DS was designed to address most common service registration and usage patterns but not all in an effort to avoid making the design too complex. Sometimes you do need to drop back to using the real service API. In your case, where Bongo object creation requires a specific constructor to be called, you will need to author and register your own ServiceFactory.

If Bongo has a default constructor, you could also try a facade approach where the Bongo component class creates a real Bongo object and delegates to it. But if Bongo does not have a default constructor, that is probably not possible.

In your case, where Bongo object creation requires a specific constructor to be called, you will need to author and register your own ServiceFactory.

Which is what I have done: BongoFactory registers BongoServiceFactory dynamically when it activates, and unregisters it when it deactivates. However, the whole point of doing this is to allow me to inject @Reference Bongo into other services, and the Bnd Resolve task apparently cannot satisfy such references unless it can find a matching XML descriptor too.

The DummyBongo class only exists to satisfy Resolve. BongoFactory and BongoServiceFactory work fine at runtime without it.

Hmm, thinking about this a bit more… Would it more accurate to have:

@Component(
    service = [ Bongo::class ],
    scope = ServiceScope.PROTOTYPE,
    enabled = false
)
private abstract class DummyBongo : Bongo

since this service is actually implemented by a dynamic ServiceFactory? Or in other words - doesn’t using a ServiceFactory automatically imply “prototype” scope?

Just wondering…
Thanks,
Chris

That service does not exist. You are just getting Bnd to put an xml descriptor in the jar file to convince the resolver that there is a provider of the Bongo service. So it is basically irrelevant what the xml descriptor says beyond that it provides a Bongo service.

In reality, no Bongo service is provided at runtime in the OSGi Service Registry. There is only a BongoFactory service. Any Reference to the Bongo service will not be satisfied (your dummy bongo is not enabled and if it was enabled, it would be the supplied Bongo service).

The resolver issue is pointing out the problem (there is no Bongo provider) which you seem to want to wallpaper over with an xml descriptor for a disabled service.

At the very least, I was thinking that adding scope = PROTOTYPE would serve as documentation for how the implementation behaves at runtime: it satisfies Bongo references with the output of my dynamically registered ServiceFactory<Bongo>.

In reality, no Bongo service is provided at runtime in the OSGi Service Registry.

Well, not until the BongoFactory service activates and then registers a Bongo service. Doesn’t BundleContest.registerService() add something to the OSGi Service Registry?

Edit: Aha! I’ve just read the Javadocs for Constants.SCOPE_PROTOTYPE… So my Bongo service actually has BUNDLE scope, unless I implement PrototypeServiceFactory instead.

Doesn’t DS 1.4’s support for constructor injection address the concerns of this topic?

@Component(scope = PROTOTYPE, service = Bongo.class)
public class BongoImpl {
   @Activate
   public BongoImpl(BundleContext bundleContext, @Reference Foo foo) {
       ...
   }
}

Only if Foo is also a @Component, which is not true for my case.