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端点转发(推荐)
此方案从根源避免重复端点问题,完全支持运行时动态切换:
- 修改原接口:移除REST相关注解,仅保留业务方法定义
public interface Service { Amount convert(String param, ...); } - 实现客户端类:保留业务逻辑,无需REST注解
@ApplicationScoped public class ManagerClientImpl implements Service { // 主服务调用逻辑 } @ApplicationScoped public class ManagerClientBackup implements Service { // 本地降级逻辑 } - 创建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(); } } - 统一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在运行时动态注册/注销端点:
- 调整实现类:移除
@ApplicationScoped,改为手动管理实例public class ManagerClientImpl implements Service { // 主实现逻辑 } public class ManagerClientBackup implements Service { // 降级实现逻辑 } - 端点管理类:监听配置变更并切换端点
@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容器动态管理实例:
- 添加条件注解:给两个实现类配置激活条件
@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 { // 降级实现 } - 配置变更监听器:动态创建/销毁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

