Search code examples
jakarta-eepluginscdihotplugging

Java EE Plugin Framework using CDI Instance Iterator


I have an application that consists of wars, an core ejb with lots of service beans in a jar and remote interfaces in another jar. Everything is packaged in an ear and running on Glassfish 4.1.

Now i want to add extenstion points or plugin support to the core ejb.

The objective is to have different hot-pluggable data import services that all share the same interface because they fetch and normalize financial data from vendors like Reuters and Bloomberg.

These plugins should be detected and managed by a "Plugin Manager" bean in the core ejb jar. The plugins should support load, undload and replace at runtime.

Ideally the plugin interfaces are in a separate package, so that someone else can develop against them without the need for my application or a Glassfish and perfectly even without the Java EE stack. I also want to deploy plugins on demand and not always the whole application.

Currently i try to use the CDI Instance iterator which works fine with two import service implementations, as long as they are in the core ejb. If i put one implementation into a separate ejb jar, then it is simply not found by CDI. I guess the problem is that Glassfish loads each ejb jar as an application in a separate classloader.

Now comes my current simplified code!

The plugin interface in a separate jar package:

package com.photon.extensions;

import java.io.Serializable;

public interface ImportServiceExtension extends Serializable {
    String getImportServiceName();
}

The plugin implementation in a separate ejb jar package that is not found:

package com.photon.services.extensions.vitrex.services;

import com.photon.extensions.ImportServiceExtension;
import javax.ejb.Remote;
import javax.ejb.Stateless;

@Remote(ImportServiceExtension.class)
@Stateless
public class ReutersImportService implements ImportServiceExtension {
    @Override
    public String getImportServiceName() {
        return "Reuters";
    }
}

The plugin implementation in the core ejb jar package that is found:

package com.photon.services.extensions;

import com.photon.extensions.ImportServiceExtension;
import javax.ejb.Stateless;

@Stateless
public class BloombergImportService implements ImportServiceExtension {
    @Override
    public String getImportServiceName() {
        return "Bloomberg";
    }
}

The remote interface for the "Plugin Manager" in the remote interfaces jar:

package com.photon.services.extensions;

import java.util.List;
import javax.ejb.Remote;

@Remote
public interface ImportServiceExtensionsRemote {
    List<String> getImportServiceNames();
}

The "Plugin Manager" bean implementation in the core ejb jar:

package com.photon.services.extensions;

import com.photon.extensions.ImportServiceExtension;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.ejb.Stateless;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;

@Stateless
public class ImportersService implements ImportServiceExtensionsRemote {

    @Inject private Instance<ImportServiceExtension> importServiceExtensions;

    @Override
    public List<String> getImportServiceNames() {
        Iterator<ImportServiceExtension> iter = importServiceExtensions.iterator();
        List<String> names = new ArrayList<>();
        while ( iter.hasNext() ) {
            ImportServiceExtension extension = iter.next();
            names.add(extension.getImportServiceName());
        }
        return names;
    }
}

And finally the controller that renders the names to a website in the wars:

package com.photon.website;

import com.photon.services.extensions.ImportServiceExtensionsRemote;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;

@RequestScoped
@Named
public class ImportController implements Serializable {

    @EJB private ImportServiceExtensionsRemote importServiceExtensions;

    public String getImportServiceNames() {
        String names = "";
        for ( String name : importServiceExtensions.getImportServiceNames() ) {
            names += name;
        }
        return names;
    }
}

In the end only "Bloomberg" is rendered.

Now my questions:

  1. Am i on the right track?

  2. If so, what am i missing in the code?

  3. Are there better solutions to this problem (OSGI, custom clazz.forName, ...)?


Solution

  • I cannot give you complete answer, but here is some food for thoughts...

    I guess the problem is that Glassfish loads each ejb jar as an application in a separate classloader.

    You nailed it. Sadly this is per JEE spec, so it's expected behaviour.

    I don't know about Glassfish but there might be some feature allowing you to share classloader between deployments (there is some in Wildfly - called deployment isolation). That might resolve your problem.

    Other thing I know from Wildfly is that you can have some application deployed as a server module which will then have access to all other deployments (and their class loaders). If there is something similar in Glassfish, you could try that. In case you are willing to give a shot to Wildfly, here is a link to issue where this was discussed.

    Now, from CDI point of view this behaviour is also correct and I am afraid there is no way you could change it, because you do not have access to class loader from the other deployments (if you had, you might load the BeanManager for given deployment and search it for relevant bean).

    I hope this gives you at least some insight.