diff --git a/core/src/main/java/com/linecorp/armeria/client/ClientFactoryConfigurator.java b/core/src/main/java/com/linecorp/armeria/client/ClientFactoryConfigurator.java new file mode 100644 index 00000000000..2123326a85b --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/client/ClientFactoryConfigurator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.armeria.client; + +import com.linecorp.armeria.common.annotation.UnstableApi; + +/** + * Configures a built-in default {@link ClientFactory} using the specified {@link ClientFactoryBuilder}. + * + *

This configurator is invoked while creating the built-in default {@link ClientFactory}s returned by + * {@link ClientFactory#ofDefault()} and {@link ClientFactory#insecure()}. + * + *

This configurator is applied to both the default and insecure built-in + * {@link ClientFactory}s, so it must not call + * {@link ClientFactory#ofDefault()} or {@link ClientFactory#insecure()}. + * + *

Because {@link ClientFactory#insecure()} applies {@link ClientFactoryBuilder#tlsNoVerify()} after this + * configurator runs, TLS verification-related customization is unsupported. + */ +@UnstableApi +@FunctionalInterface +public interface ClientFactoryConfigurator { + + /** + * Configures the built-in default {@link ClientFactory} using the specified + * {@link ClientFactoryBuilder}. + * + *

Note that {@link ClientFactoryBuilder#tlsNoVerify()} is applied after this method returns when + * creating {@link ClientFactory#insecure()}. + */ + void configureDefault(ClientFactoryBuilder builder); + + /** + * Returns a {@link ClientFactoryConfigurator} that does not customize the specified + * {@link ClientFactoryBuilder}. + */ + static ClientFactoryConfigurator noop() { + return builder -> { + }; + } +} diff --git a/core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java b/core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java index 9c4cc44aadb..04c35e1e423 100644 --- a/core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java +++ b/core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java @@ -36,6 +36,7 @@ import com.google.common.collect.Streams; import com.linecorp.armeria.client.endpoint.EndpointGroup; +import com.linecorp.armeria.common.Flags; import com.linecorp.armeria.common.Scheme; import com.linecorp.armeria.common.SessionProtocol; import com.linecorp.armeria.common.TlsProvider; @@ -70,11 +71,14 @@ final class DefaultClientFactory implements ClientFactory { private static volatile boolean shutdownHookDisabled; + private static final ClientFactoryConfigurator DEFAULT_CLIENT_FACTORY_CONFIGURATOR = + Flags.defaultClientFactoryConfigurator(); + static final DefaultClientFactory DEFAULT = - (DefaultClientFactory) ClientFactory.builder().build(); + newDefaultClientFactory(false, DEFAULT_CLIENT_FACTORY_CONFIGURATOR); static final DefaultClientFactory INSECURE = - (DefaultClientFactory) ClientFactory.builder().tlsNoVerify().build(); + newDefaultClientFactory(true, DEFAULT_CLIENT_FACTORY_CONFIGURATOR); static { if (DefaultClientFactory.class.getClassLoader() == ClassLoader.getSystemClassLoader()) { @@ -94,6 +98,16 @@ static void disableShutdownHook0() { shutdownHookDisabled = true; } + private static DefaultClientFactory newDefaultClientFactory(boolean insecure, + ClientFactoryConfigurator configurator) { + final ClientFactoryBuilder builder = ClientFactory.builder(); + configurator.configureDefault(builder); + if (insecure) { + builder.tlsNoVerify(); + } + return (DefaultClientFactory) builder.build(); + } + private final HttpClientFactory httpClientFactory; private final Multimap clientFactories; private final List clientFactoriesToClose; diff --git a/core/src/main/java/com/linecorp/armeria/common/DefaultFlagsProvider.java b/core/src/main/java/com/linecorp/armeria/common/DefaultFlagsProvider.java index ac2b6e39677..857541b4199 100644 --- a/core/src/main/java/com/linecorp/armeria/common/DefaultFlagsProvider.java +++ b/core/src/main/java/com/linecorp/armeria/common/DefaultFlagsProvider.java @@ -26,6 +26,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableSet; +import com.linecorp.armeria.client.ClientFactoryConfigurator; import com.linecorp.armeria.client.ResponseTimeoutMode; import com.linecorp.armeria.common.multipart.MultipartFilenameDecodingMode; import com.linecorp.armeria.common.util.Sampler; @@ -374,6 +375,11 @@ public Long defaultRequestAutoAbortDelayMillis() { return DEFAULT_REQUEST_AUTO_ABORT_DELAY_MILLIS; } + @Override + public ClientFactoryConfigurator defaultClientFactoryConfigurator() { + return ClientFactoryConfigurator.noop(); + } + @Override public String routeCacheSpec() { return ROUTE_CACHE_SPEC; diff --git a/core/src/main/java/com/linecorp/armeria/common/Flags.java b/core/src/main/java/com/linecorp/armeria/common/Flags.java index 4426e59eafc..2d598e908a6 100644 --- a/core/src/main/java/com/linecorp/armeria/common/Flags.java +++ b/core/src/main/java/com/linecorp/armeria/common/Flags.java @@ -47,6 +47,7 @@ import com.linecorp.armeria.client.ClientBuilder; import com.linecorp.armeria.client.ClientFactoryBuilder; +import com.linecorp.armeria.client.ClientFactoryConfigurator; import com.linecorp.armeria.client.ClientTlsSpec; import com.linecorp.armeria.client.DnsResolverGroupBuilder; import com.linecorp.armeria.client.Endpoint; @@ -1634,6 +1635,22 @@ public static MeterRegistry meterRegistry() { return METER_REGISTRY; } + /** + * Returns the {@link ClientFactoryConfigurator} that customizes the built-in default + * {@link com.linecorp.armeria.client.ClientFactory}s. + * + *

This value is consulted while initializing the built-in default client factories. + * + * @see ClientFactoryConfigurator + */ + @UnstableApi + public static ClientFactoryConfigurator defaultClientFactoryConfigurator() { + final ClientFactoryConfigurator configurator = + getValue(FlagsProvider::defaultClientFactoryConfigurator, + "defaultClientFactoryConfigurator"); + return configurator != null ? configurator : ClientFactoryConfigurator.noop(); + } + /** * Returns the default interval in milliseconds between the reports on unlogged exceptions. * diff --git a/core/src/main/java/com/linecorp/armeria/common/FlagsProvider.java b/core/src/main/java/com/linecorp/armeria/common/FlagsProvider.java index fda7fcb9dc8..5de195c5c87 100644 --- a/core/src/main/java/com/linecorp/armeria/common/FlagsProvider.java +++ b/core/src/main/java/com/linecorp/armeria/common/FlagsProvider.java @@ -33,6 +33,7 @@ import com.linecorp.armeria.client.ClientBuilder; import com.linecorp.armeria.client.ClientFactoryBuilder; +import com.linecorp.armeria.client.ClientFactoryConfigurator; import com.linecorp.armeria.client.DnsResolverGroupBuilder; import com.linecorp.armeria.client.ResponseTimeoutMode; import com.linecorp.armeria.client.retry.Backoff; @@ -822,6 +823,29 @@ default Long defaultRequestAutoAbortDelayMillis() { return null; } + /** + * Returns a {@link ClientFactoryConfigurator} that customizes the built-in default + * {@link com.linecorp.armeria.client.ClientFactory}s. + * + *

If {@code null} is returned, the next available {@link FlagsProvider} is consulted.

+ * + *

The returned {@link ClientFactoryConfigurator} is applied while creating the built-in default + * {@link com.linecorp.armeria.client.ClientFactory}s, so it must not call + * {@link com.linecorp.armeria.client.ClientFactory#ofDefault()} or + * {@link com.linecorp.armeria.client.ClientFactory#insecure()}.

+ * + *

This configurator is applied to both the default and insecure built-in + * {@link com.linecorp.armeria.client.ClientFactory}s. Because + * {@link com.linecorp.armeria.client.ClientFactory#insecure()} applies + * {@link com.linecorp.armeria.client.ClientFactoryBuilder#tlsNoVerify()} after the configurator runs, + * TLS verification-related customization is unsupported.

+ */ + @UnstableApi + @Nullable + default ClientFactoryConfigurator defaultClientFactoryConfigurator() { + return null; + } + /** * Returns the {@linkplain CaffeineSpec Caffeine specification string} of the cache that stores the recent * request routing history for all {@link Service}s. diff --git a/it/flags-provider/src/test/java/com/linecorp/armeria/common/BaseFlagsProvider.java b/it/flags-provider/src/test/java/com/linecorp/armeria/common/BaseFlagsProvider.java index 85d356cb64d..2a1d5a8e694 100644 --- a/it/flags-provider/src/test/java/com/linecorp/armeria/common/BaseFlagsProvider.java +++ b/it/flags-provider/src/test/java/com/linecorp/armeria/common/BaseFlagsProvider.java @@ -19,6 +19,7 @@ import java.net.InetAddress; import java.util.function.Predicate; +import com.linecorp.armeria.client.ClientFactoryConfigurator; import com.linecorp.armeria.common.util.InetAddressPredicates; import io.micrometer.core.instrument.MeterRegistry; @@ -71,6 +72,11 @@ public Long defaultServerConnectionDrainDurationMicros() { return 500L; } + @Override + public ClientFactoryConfigurator defaultClientFactoryConfigurator() { + return builder -> builder.connectTimeoutMillis(4242); + } + @Override public String routeCacheSpec() { return "off"; diff --git a/it/flags-provider/src/test/java/com/linecorp/armeria/common/FlagsProviderTest.java b/it/flags-provider/src/test/java/com/linecorp/armeria/common/FlagsProviderTest.java index 9377ab9c6cb..6a850e80395 100644 --- a/it/flags-provider/src/test/java/com/linecorp/armeria/common/FlagsProviderTest.java +++ b/it/flags-provider/src/test/java/com/linecorp/armeria/common/FlagsProviderTest.java @@ -25,6 +25,7 @@ import java.lang.reflect.Method; import java.net.URL; import java.net.URLConnection; +import java.util.Map; import org.assertj.core.api.ObjectAssert; import org.junit.jupiter.api.BeforeEach; @@ -32,11 +33,13 @@ import org.junitpioneer.jupiter.ClearSystemProperty; import org.junitpioneer.jupiter.SetSystemProperty; +import com.linecorp.armeria.client.ClientFactory; import com.linecorp.armeria.common.util.Exceptions; import com.linecorp.armeria.common.util.InetAddressPredicates; import com.linecorp.armeria.common.util.TlsEngineType; import io.micrometer.core.instrument.Metrics; +import io.netty.channel.ChannelOption; @SetSystemProperty( key = "com.linecorp.armeria.requestContextStorageProvider", @@ -45,11 +48,13 @@ class FlagsProviderTest { private Class flags; + private Class clientFactoryClass; @BeforeEach void reloadFlags() throws ClassNotFoundException { final FlagsClassLoader classLoader = new FlagsClassLoader(); flags = classLoader.loadClass(Flags.class.getCanonicalName()); + clientFactoryClass = classLoader.loadClass(ClientFactory.class.getName()); } @Test @@ -159,11 +164,55 @@ void testDistributionStatisticConfig() { .isEqualTo(DistributionStatisticConfigUtil.DEFAULT_DIST_STAT_CFG); } + @Test + void defaultClientFactoryConfiguratorIsAppliedToDefaultClientFactory() throws Throwable { + try { + assertClientFactoryConnectTimeoutMillis("ofDefault").isEqualTo(4242); + } finally { + closeDefaultClientFactories(); + } + } + + @Test + void defaultClientFactoryConfiguratorIsAppliedToInsecureClientFactory() throws Throwable { + try { + assertClientFactoryConnectTimeoutMillis("insecure").isEqualTo(4242); + } finally { + closeDefaultClientFactories(); + } + } + private ObjectAssert assertFlags(String flagsMethod) throws Throwable { final Method method = flags.getDeclaredMethod(flagsMethod); return assertThat(method.invoke(null)); } + private ObjectAssert assertClientFactoryConnectTimeoutMillis(String factoryMethod) + throws Throwable { + final Object options = clientFactoryOptions(factoryMethod); + final Method channelOptionsMethod = options.getClass().getDeclaredMethod("channelOptions"); + @SuppressWarnings("unchecked") + final Map, Object> channelOptions = + (Map, Object>) channelOptionsMethod.invoke(options); + return assertThat(channelOptions.get(ChannelOption.CONNECT_TIMEOUT_MILLIS)); + } + + private Object clientFactoryOptions(String factoryMethod) throws Throwable { + final Object clientFactory = clientFactory(factoryMethod); + final Method optionsMethod = clientFactoryClass.getDeclaredMethod("options"); + return optionsMethod.invoke(clientFactory); + } + + private Object clientFactory(String factoryMethod) throws Throwable { + final Method factoryGetter = clientFactoryClass.getDeclaredMethod(factoryMethod); + return factoryGetter.invoke(null); + } + + private void closeDefaultClientFactories() throws Throwable { + final Method closeDefaultMethod = clientFactoryClass.getDeclaredMethod("closeDefault"); + closeDefaultMethod.invoke(null); + } + private static class FlagsClassLoader extends ClassLoader { FlagsClassLoader() { super(getSystemClassLoader());