如何在Karaf与Hazelcast Jet环境下统一Activator的上下文?
我正在开发一个基于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"。
核心原因分析
- BundleContext拿错了:你现在的REST控制器直接调用第一个模块
Activator.getBundleContext(),拿到的只是第一个模块自己的BundleContext,不是Karaf的全局服务上下文。OSGi里每个Bundle都有独立的上下文,虽然理论上能看到其他Bundle注册的服务,但你没用到OSGi标准的服务跟踪机制,再加上类加载器或者权限配置的问题,就看不到第二个模块的服务了。 - Spring Boot和OSGi打架了:每个模块都用
SpringApplication.run()启动独立的Spring上下文,这会把Spring和OSGi的上下文隔离开。服务注册到各自的BundleContext后,跨Bundle找服务得靠正确的OSGi导入导出配置,不然根本找不到。 - 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

