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

JBoss EAP迁Quarkus:同一REST接口多实现运行时切换方案问询

JBoss EAP迁移Quarkus:REST端点故障转移/降级实现问题

问题背景

正在将JBoss EAP应用迁移到Quarkus,应用为故障转移/降级场景实现了同一REST服务接口的两个版本:

  • ManagerClientImpl - 主HTTP客户端(调用外部服务)
  • ManagerClientBackup - 本地降级实现

两个类均实现同一接口:

@Path("/rate")
public interface Service {
    @GET
    @Path("/convert")
    @Produces("application/json")
    Amount convert(@QueryParam("param") String param, ...);
}

构建错误信息

Quarkus构建时触发重复端点错误:

[ERROR] GET /rate 由以下类声明:
[ERROR] ManagerClientImpl#convert
[ERROR] ManagerClientBackup#convert

[ERROR] at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor.checkForDuplicateEndpoint(ResteasyReactiveProcessor.java:1504)
[ERROR] at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor.setupEndpoints(ResteasyReactiveProcessor.java:731)

该配置在JBoss中可正常运行,且无法大幅改动现有架构,两个实现必须保留在JAR包中以支持故障转移所需的生产者模式。

已尝试的无效方案

  • @EndpointDisabled:仅能在运行时禁用端点,无法阻止构建阶段的重复端点检测
  • @Alternative + @Priority + BuildProperty:构建阶段即确定启用的端点,不支持运行时动态切换

需求

需要在Quarkus中实现:保留两个实现作为构建阶段的潜在REST端点,仅在运行时根据CDI生产者选择或运行时配置注册/启用其中一个,支持随时切换到降级方案。

完整堆栈跟踪

[ERROR] 执行目标 io.quarkus:quarkus-maven-plugin:3.20.1.redhat-00003:build (default) 失败,项目 acs-quarkus-authentication-service: 构建Quarkus应用失败:io.quarkus.builder.BuildException: 构建失败:因错误导致构建失败
[ERROR] [error]: 构建步骤 io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#setupEndpoints 抛出异常:
[ERROR] GET /useragent 由以下类声明 :
[ERROR] com.worldline.acs.utils.babel.client.useragent.UserAgentParserClientBackup#parse consumes *, produces application/json
[ERROR] com.worldline.acs.utils.babel.client.useragent.UserAgentParserClientImpl#parse consumes *, produces application/json
[ERROR] DELETE /proxy/v2 由以下类声明 :
[ERROR] com.worldline.acs.hubs.mock.RestProxyManagerV2MockImpl#deleteAuthentication consumes application/json, produces application/json
[ERROR] com.worldline.acs.hubs.client.proxyImpl.RestProxyBackupManagerV2Impl#deleteAuthentication consumes application/json, produces application/json
[ERROR] GET /iptolocation 由以下类声明 :
[ERROR] com.worldline.acs.utils.babel.client.iptolocation.IpToLocationManagerClientBackup#locate consumes *, produces application/json
[ERROR] com.worldline.acs.utils.babel.client.iptolocation.IpToLocationManagerClientImpl#locate consumes *, produces application/json
[ERROR]
[ERROR] at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor.checkForDuplicateEndpoint(ResteasyReactiveProcessor.java:1504)
[ERROR] at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor.setupEndpoints(ResteasyReactiveProcessor.java:731)
[ERROR] at java.base/java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:732)
[ERROR] at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:856)
[ERROR] at io.quarkus.builder.BuildContext.run(BuildContext.java:255)
[ERROR] at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
[ERROR] at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2675)
[ERROR] at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2654)
[ERROR] at org.jboss.threads.EnhancedQueueExecutor.runThreadBody(EnhancedQueueExecutor.java:1627)
[ERROR] at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1594)
[ERROR] at java.base/java.lang.Thread.run(Thread.java:840)
[ERROR] at org.jboss.threads.JBossThread.run(JBossThread.java:499)
[ERROR] ->

解决方案

方案1:CDI代理+单一REST端点转发(推荐)

此方案从根源避免重复端点问题,完全支持运行时动态切换:

  1. 修改原接口:移除REST相关注解,仅保留业务方法定义
    public interface Service {
        Amount convert(String param, ...);
    }
    
  2. 实现客户端类:保留业务逻辑,无需REST注解
    @ApplicationScoped
    public class ManagerClientImpl implements Service {
        // 主服务调用逻辑
    }
    
    @ApplicationScoped
    public class ManagerClientBackup implements Service {
        // 本地降级逻辑
    }
    
  3. 创建CDI生产者:根据运行时配置选择实例
    @ApplicationScoped
    public class ServiceProducer {
        @ConfigProperty(name = "service.active.impl")
        String activeImpl;
    
        @Produces
        @Default
        public Service produceService(Instance<ManagerClientImpl> primary, Instance<ManagerClientBackup> backup) {
            return "primary".equals(activeImpl) ? primary.get() : backup.get();
        }
    }
    
  4. 统一REST端点:注入Service并转发请求
    @Path("/rate")
    @ApplicationScoped
    public class ServiceEndpoint {
        @Inject
        Service service;
    
        @GET
        @Path("/convert")
        @Produces("application/json")
        public Amount convert(@QueryParam("param") String param, ...) {
            return service.convert(param, ...);
        }
    }
    

通过修改service.active.impl配置值(如primary/backup),无需重启即可切换实现。

方案2:动态端点注册(Quarkus 3.x+)

利用Quarkus REST的RestControllerRegistry在运行时动态注册/注销端点:

  1. 调整实现类:移除@ApplicationScoped,改为手动管理实例
    public class ManagerClientImpl implements Service {
        // 主实现逻辑
    }
    
    public class ManagerClientBackup implements Service {
        // 降级实现逻辑
    }
    
  2. 端点管理类:监听配置变更并切换端点
    @ApplicationScoped
    public class ServiceEndpointManager {
        @ConfigProperty(name = "service.active.impl")
        String activeImpl;
        @Inject
        RestControllerRegistry registry;
    
        private Object currentEndpoint;
    
        @PostConstruct
        void init() {
            switchEndpoint(activeImpl);
        }
    
        void onConfigChange(@Observes @ConfigPropertyChanged String newImpl) {
            switchEndpoint(newImpl);
        }
    
        private void switchEndpoint(String implType) {
            // 注销当前端点
            if (currentEndpoint != null) {
                registry.unregister(currentEndpoint);
            }
            // 注册新端点
            currentEndpoint = "primary".equals(implType) ? new ManagerClientImpl() : new ManagerClientBackup();
            registry.register(currentEndpoint);
        }
    }
    

注意:需使用支持RestControllerRegistry的Quarkus版本(3.10+已稳定支持)。

方案3:条件Bean+运行时Bean管理

若无法修改原接口注解,可通过条件Bean配合CDI容器动态管理实例:

  1. 添加条件注解:给两个实现类配置激活条件
    @ApplicationScoped
    @ConditionalOnProperty(name = "service.active.impl", havingValue = "primary", matchIfMissing = false)
    public class ManagerClientImpl implements Service {
        // 主实现
    }
    
    @ApplicationScoped
    @ConditionalOnProperty(name = "service.active.impl", havingValue = "backup", matchIfMissing = false)
    public class ManagerClientBackup implements Service {
        // 降级实现
    }
    
  2. 配置变更监听器:动态创建/销毁Bean
    @ApplicationScoped
    public class ServiceSwitcher {
        @Inject
        BeanManager beanManager;
        String activeImpl;
    
        @PostConstruct
        void init(@ConfigProperty(name = "service.active.impl") String initialImpl) {
            activeImpl = initialImpl;
        }
    
        void onConfigChange(@Observes @ConfigPropertyChanged String newImpl) {
            // 销毁旧Bean
            destroyBean("primary".equals(activeImpl) ? ManagerClientImpl.class : ManagerClientBackup.class);
            // 创建新Bean
            createBean("primary".equals(newImpl) ? ManagerClientImpl.class : ManagerClientBackup.class);
            activeImpl = newImpl;
        }
    
        private void destroyBean(Class<?> beanClass) {
            Bean<?> bean = beanManager.getBeans(beanClass).iterator().next();
            CreationalContext<?> ctx = beanManager.createCreationalContext(null);
            Object instance = beanManager.getReference(bean, beanClass, ctx);
            bean.destroy(instance, ctx);
        }
    
        private void createBean(Class<?> beanClass) {
            Bean<?> bean = beanManager.getBeans(beanClass).iterator().next();
            CreationalContext<?> ctx = beanManager.createCreationalContext(null);
            beanManager.getReference(bean, beanClass, ctx);
        }
    }
    

此方案需注意Bean生命周期管理的正确性,避免内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 00:05:54