PAYATHON 2026

Quarkus 中如何在运行时动态设置 configKey

支付阿杰

结论

@RegisterRestClient(configKey = "...") 中的 configKey 无法在运行时修改。它是注解属性,必须使用编译期常量。Quarkus 会在应用启动时根据这个值注册并配置 REST Client。

需要动态处理时,应根据具体内容选择方案:

  • 服务地址发生变化时,使用 RestClientBuilder 在运行时创建客户端。
  • 认证 Token 发生变化时,通过 ClientHeadersFactory、@ClientHeaderParam 或请求过滤器动态添加。
  • 配置内容发生变化时,保留固定的 configKey,通过自定义 ConfigSource 提供配置,不要修改 configKey 本身。

为什么不能动态设置

下面的写法要求 configKey 是编译期常量:

@RegisterRestClient(configKey = "payment-api.auth-token-api")
public interface AuthTokenClient {
}

Java 不允许在注解属性中使用运行时变量,因此下面的代码无法编译:

String key = loadKeyFromOtherService();

@RegisterRestClient(configKey = key) // 非法:不是编译期常量
public interface AuthTokenClient {
}

configKey 只是 REST Client 查找配置时使用的键,不是服务地址、Token 或其他业务配置值。例如,固定键:

@RegisterRestClient(configKey = "auth-token-api")

可以对应以下配置:

quarkus:
  rest-client:
    auth-token-api:
      url: https://example.com

一般不需要动态改变键名。需要动态处理的通常是这个键对应的配置值。

方案一:运行时动态创建 REST Client

如果另一个服务返回的是目标服务地址,可以不使用 application.yaml 中配置的 REST Client URL,改为通过 RestClientBuilder 在运行时创建客户端。

客户端接口仍可定义为普通的 REST Client 接口:

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/token")
public interface AuthTokenClient {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    TokenResponse getToken();
}

获取运行时地址并创建客户端:

import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.rest.client.RestClientBuilder;

import java.net.URI;

@ApplicationScoped
public class AuthTokenClientFactory {

    private final EndpointDiscoveryService discoveryService;

    public AuthTokenClientFactory(EndpointDiscoveryService discoveryService) {
        this.discoveryService = discoveryService;
    }

    public AuthTokenClient createClient() {
        String baseUrl = discoveryService.getAuthTokenApiUrl();

        if (baseUrl == null || baseUrl.isBlank()) {
            throw new IllegalStateException("Auth Token API URL is missing");
        }

        return RestClientBuilder.newBuilder()
                .baseUri(URI.create(baseUrl))
                .build(AuthTokenClient.class);
    }
}

调用方式如下:

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class PaymentService {

    private final AuthTokenClientFactory clientFactory;

    public PaymentService(AuthTokenClientFactory clientFactory) {
        this.clientFactory = clientFactory;
    }

    public TokenResponse requestToken() {
        AuthTokenClient client = clientFactory.createClient();
        return client.getToken();
    }
}

如果地址不经常变化,可以缓存客户端实例,避免每次请求都重新构建。地址可能变化时,则要处理好缓存刷新、连接资源释放和并发更新。

方案二:固定 configKey,动态提供配置值

如果仍需使用 CDI 注入,可以保留固定的 configKey:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@RegisterRestClient(configKey = "auth-token-api")
public interface AuthTokenClient {
}
import org.eclipse.microprofile.rest.client.inject.RestClient;

@Inject
@RestClient
AuthTokenClient authTokenClient;

动态提供的应是具体配置,例如:

quarkus.rest-client.auth-token-api.url=https://example.com

配置可以来自环境变量、系统属性或自定义的 MicroProfile Config ConfigSource。

需要注意的是,Quarkus REST Client 通常会在应用启动或 CDI Bean 初始化阶段读取配置。如果远程服务直到应用完全启动后才返回配置,已经注入的客户端不一定会自动刷新。这种情况更适合使用 RestClientBuilder,或者在配置刷新后重新构建客户端。

自定义 ConfigSource 初始化时,不要调用本身依赖该配置的 REST Client,否则可能产生循环依赖。

方案三:动态值实际是认证 Token

从 auth-token-api 这个名称来看,动态变量也可能指 Token。Token 不应写入 configKey,而应作为 HTTP 请求头发送。

可以使用 ClientHeadersFactory:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

@ApplicationScoped
public class AuthorizationHeadersFactory implements ClientHeadersFactory {

    private final TokenService tokenService;

    public AuthorizationHeadersFactory(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    public MultivaluedMap<String, String> update(
            MultivaluedMap<String, String> incomingHeaders,
            MultivaluedMap<String, String> outgoingHeaders) {

        MultivaluedMap<String, String> headers = new MultivaluedHashMap<>();
        headers.putSingle("Authorization", "Bearer " + tokenService.getToken());
        return headers;
    }
}

然后在客户端接口上注册:

import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@RegisterRestClient(configKey = "auth-token-api")
@RegisterClientHeaders(AuthorizationHeadersFactory.class)
public interface AuthTokenClient {
    // REST endpoints
}

这样可以保持 configKey 不变,同时在每次请求时动态获取 Token。

注意事项

  • configKey 是配置命名空间,不是动态业务参数。
  • 对远程服务返回的 URL,应校验协议、主机和允许访问的目标,避免 SSRF 风险。
  • 不要在日志中输出 Token、密码或完整的认证请求头。
  • 动态创建大量客户端会增加连接池和资源开销。如果地址稳定,应复用客户端。
  • 如果远程配置只在启动阶段获取,可以考虑自定义 ConfigSource。如果配置需要在运行期间频繁变化,应优先使用程序化客户端工厂。
  • 不同 Quarkus 版本及 REST Client 实现支持的配置项名称可能略有差异,应以当前项目所用版本的配置参考为准。

备注:内容仅供参考。