Java 使用 CDI 实例迭代器的 EE 插件框架

Java EE Plugin Framework using CDI Instance Iterator

我有一个由 wars 组成的应用程序,一个核心 ejb,在一个 jar 中有很多服务 bean,在另一个 jar 中有远程接口。在 Glassfish 4.1 上,一切都打包在一个耳朵和 运行 中。

现在我想为核心 ejb 添加扩展点或插件支持。

objective 将拥有不同的热插拔数据导入服务,这些服务共享相同的接口,因为它们从路透社和彭博社等供应商处获取并规范化财务数据.

这些插件应该由核心 ejb jar 中的 "Plugin Manager" bean 检测和管理。插件应该支持在运行时加载、卸载和替换。

理想情况下,插件接口位于一个单独的包中,这样其他人就可以针对它们进行开发,而无需我的应用程序或 Glassfish,甚至在没有 Java EE 堆栈的情况下也是如此。我还想按需部署插件,而不是总是部署整个应用程序。

目前我尝试使用 CDI 实例迭代器,只要它们在核心 ejb 中,它就可以很好地处理两个导入服务实现。如果我将一个实现放入一个单独的 ejb jar,那么 CDI 根本找不到它。我想问题是 Glassfish 将每个 ejb jar 作为应用程序加载到单独的类加载器中。

我现在的简化代码来了!

独立jar包中的插件接口:

package com.photon.extensions;

import java.io.Serializable;

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

未找到单独的ejb jar包中的插件实现:

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";
    }
}

找到的核心ejb jar包中的插件实现:

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";
    }
}

远程接口 jar 中 "Plugin Manager" 的远程接口:

package com.photon.services.extensions;

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

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

核心ejb jar中的"Plugin Manager" bean实现:

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;
    }
}

最后是在战争中将名称呈现给网站的控制器:

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;
    }
}

最后只渲染了"Bloomberg"

现在我的问题:

  1. 我走对了吗?

  2. 如果是这样,我在代码中缺少什么?

  3. 这个问题有没有更好的解决方案(OSGI,自定义 clazz.forName, ...)?

我无法给你完整的答案,但这里有一些值得深思的地方......

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

你成功了。遗憾的是,这是 每个 JEE 规范,所以这是预期的行为。

我不知道 Glassfish,但可能有一些功能允许您在部署之间共享 classloader(Wildfly 中有一些功能 - 称为部署隔离)。这可能会解决您的问题。

我从 Wildfly 了解到的另一件事是,您可以将某些应用程序部署为 服务器模块 ,然后可以访问所有其他部署(及其 class装载机)。如果 Glassfish 中有类似的东西,你可以试试。如果您愿意试一试 Wildfly,here 是 link 问题的讨论地点。

现在,从 CDI 的角度来看 这种行为也是正确的,恐怕您无法更改它,因为您无权访问 class 来自其他部署的加载程序(如果有,您可以为给定部署加载 BeanManager 并搜索相关 bean)。

我希望这至少能让您有所了解。