You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Karaf与Hazelcast Jet环境下统一Activator的上下文?

问题与解决方案:Karaf中多模块DataService无法被REST控制器全部获取

我正在开发一个基于Java 17、Spring Boot、Karaf(OSGi)和Hazelcast Jet的应用,用于关联不同模块的数据源。目前遇到以下问题:

两个模块均实现了公共的DataService接口并注册了服务,但REST控制器仅能获取第一个模块的服务,无法识别第二个模块的服务。推测是Hazelcast Jet导致服务注册到不同上下文,请问如何将它们注册到同一BundleContext或Spring Context,以实现数据关联?


公共模块接口(所有模块均引入该Jar包)

public interface DataService {
    String name();
    public BatchStage<Object> getData(Pipeline pipeline, Map<String, Object> source);
}

第一个模块实现类

public class JDBCDataSource implements DataService, Serializable {
    public BatchStage<Object> getData(Pipeline pipeline, Map<String, Object> source) {
        //Hazelcast Jdbc implementation
    }

    @Override
    public String name() {
        return "JDBC Service";
    }
}

第一个模块的Activator

public class Activator implements BundleActivator {
    public static BundleContext bundleContext;
    ConfigurableApplicationContext appContext;

    @Override
    public void start(BundleContext context) throws Exception {
        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
        appContext = SpringApplication.run(Activator.class);
        bundleContext = context;
        registerServices();
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        bundleContext = null;
        SpringApplication.exit(appContext, () -> 0);
    }

    public static BundleContext getBundleContext() {
        return bundleContext;
    }

    private void registerServices() {
        DataService service = new JDBCDataSource();
        bundleContext.registerService(DataService.class.getName(), service, new HashMap<String, Object>());
        System.out.println("Service registered: " + service.name());
    }

    public static void main(String[] args) {
        SpringApplication.run(Activator.class, args);
    }
}

第一个模块中的REST控制器

@RestController
@RequestMapping("/data")
@Component
public class RController {
    public Map<String, Object> fetchRecordsPipline() {
        BundleContext bundleContext = Activator.getBundleContext();

        Collection<ServiceReference<DataService>> references = bundleContext
                .getServiceReferences(DataService.class, null);
        for (ServiceReference<DataService> reference : references) {
            DataService calcService = bundleContext.getService(reference);
            System.out.println(calcService.name());
        }
        return Collections.emptyMap();
    }
}

第二个模块实现类

public class JsonDataSource implements DataService, Serializable {
    public BatchStage<Object> getData(Pipeline pipeline, Map<String, Object> source) {
        //Hazelcast json implementation
    }

    @Override
    public String name() {
        return "Json Service";
    }
}

第二个模块的Activator

public class Activator implements BundleActivator {
    public static BundleContext bundleContext;
    ConfigurableApplicationContext appContext;

    @Override
    public void start(BundleContext context) throws Exception {
        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
        appContext = SpringApplication.run(Activator.class);
        bundleContext = context;
        registerServices();
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        bundleContext = null;
        SpringApplication.exit(appContext, () -> 0);
    }

    public static BundleContext getBundleContext() {
        return bundleContext;
    }

    private void registerServices() {
        DataService service = new JsonDataSource();
        bundleContext.registerService(DataService.class.getName(), service, new HashMap<String, Object>());
        System.out.println("Service registered: " + service.name());
    }

    public static void main(String[] args) {
        SpringApplication.run(Activator.class, args);
    }
}

补充信息:Hazelcast Jet实例在第一个模块中生成。运行在Karaf环境时,两个模块的服务均已注册,但调用REST接口仅能获取第一个模块的"JDBC Service",无法获取第二个模块的"Json Service"。


核心原因分析

  1. BundleContext拿错了:你现在的REST控制器直接调用第一个模块Activator.getBundleContext(),拿到的只是第一个模块自己的BundleContext,不是Karaf的全局服务上下文。OSGi里每个Bundle都有独立的上下文,虽然理论上能看到其他Bundle注册的服务,但你没用到OSGi标准的服务跟踪机制,再加上类加载器或者权限配置的问题,就看不到第二个模块的服务了。
  2. Spring Boot和OSGi打架了:每个模块都用SpringApplication.run()启动独立的Spring上下文,这会把Spring和OSGi的上下文隔离开。服务注册到各自的BundleContext后,跨Bundle找服务得靠正确的OSGi导入导出配置,不然根本找不到。
  3. Hazelcast Jet搞乱了类加载器:Hazelcast Jet可能修改了线程上下文类加载器,导致服务注册时用的类加载器和找服务时的不一样。OSGi对服务接口的类加载器要求很严,公共模块的DataService必须让所有Bundle用同一个类加载器加载才行。

解决步骤

1. 改控制器的服务获取方式,用OSGi的ServiceTracker或者Spring-OSGi集成

别再从第一个模块的Activator拿BundleContext了,换用OSGi的ServiceTracker来监听所有DataService实例,或者用Spring和OSGi的集成来注入服务。

方法一:用ServiceTracker改造控制器

直接让控制器实现BundleActivator,用ServiceTracker跟踪所有DataService:

@RestController
@RequestMapping("/data")
@Component
public class RController implements BundleActivator {
    private ServiceTracker<DataService, DataService> serviceTracker;

    @Override
    public void start(BundleContext context) throws Exception {
        // 创建ServiceTracker跟踪所有DataService服务
        serviceTracker = new ServiceTracker<>(context, DataService.class, null);
        serviceTracker.open();
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        if (serviceTracker != null) {
            serviceTracker.close();
        }
    }

    @GetMapping("/fetch")
    public Map<String, Object> fetchRecordsPipline() {
        Collection<DataService> allServices = serviceTracker.getServices();
        if (allServices != null) {
            allServices.forEach(service -> System.out.println(service.name()));
        }
        return Collections.singletonMap("servicesFound", allServices != null ? allServices.size() : 0);
    }
}

方法二:用Spring Boot OSGi Starter注入服务

给第一个模块加Spring Boot OSGi的依赖,然后用@OsgiService直接注入所有DataService实例:

@RestController
@RequestMapping("/data")
@Component
public class RController {
    // 自动注入所有DataService实现类
    @OsgiService(filter = "")
    private List<DataService> dataServices;

    @GetMapping("/fetch")
    public Map<String, Object> fetchRecordsPipline() {
        dataServices.forEach(service -> System.out.println(service.name()));
        return Collections.singletonMap("servicesFound", dataServices.size());
    }
}

2. 别每个模块都启动独立Spring上下文

每个模块的Activator别再调用SpringApplication.run()了,用Spring DM或者Blueprint来管理Spring Bean,让Bean直接注册到OSGi服务注册表。改一下Activator:

// 第一个模块的Activator(第二个模块照抄改实现类就行)
public class Activator implements BundleActivator {
    private ApplicationContext appContext;

    @Override
    public void start(BundleContext context) throws Exception {
        // 用Spring的OSGi集成加载上下文,自动注册服务
        appContext = new ClassPathXmlApplicationContext("spring-context.xml", context.getClassLoader());
        // 要是用注解驱动的话,就换成下面这段:
        // AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        // ctx.register(JDBCDataSource.class);
        // ctx.setClassLoader(context.getClassLoader());
        // ctx.refresh();
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        if (appContext instanceof ConfigurableApplicationContext) {
            ((ConfigurableApplicationContext) appContext).close();
        }
    }
}

然后在模块里加个spring-context.xml配置服务注册:

<osgi:service id="jdbcDataSource" interface="com.yourpackage.DataService">
    <bean class="com.yourpackage.JDBCDataSource"/>
</osgi:service>

3. 确保公共模块的接口在Karaf里共享

修改Karaf的etc/org.osgi.framework.system.packages.extra配置,把公共模块的包加进去,让所有Bundle用同一个类加载器加载DataService:

org.yourpackage.commons;version=1.0.0

同时每个模块的pom.xml里要正确配置导入导出:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.felix</groupId>
            <artifactId>maven-bundle-plugin</artifactId>
            <version>5.1.9</version>
            <extensions>true</extensions>
            <configuration>
                <instructions>
                    <Export-Package>com.yourpackage.datasource.jdbc</Export-Package>
                    <Import-Package>com.yourpackage.commons, *</Import-Package>
                </instructions>
            </configuration>
        </plugin>
    </plugins>
</build>

4. 修正Hazelcast Jet的类加载器问题

初始化Hazelcast Jet的时候,指定用当前的上下文类加载器,别让它乱改:

// 在第一个模块初始化Jet实例的地方
JetInstance jet = Jet.newJetInstance(new JetConfig()
        .setClassLoader(Thread.currentThread().getContextClassLoader()));

或者直接在Karaf的启动参数里加:

-Dhazelcast.classloader.provider=osgi

内容的提问来源于stack exchange,提问作者user3458271

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 22:50:33