Skip to content

Commit 5db7c37

Browse files
authored
Allow overriding the default ClientFactory via FlagsProvider (#6671)
Motivation: The built-in default `ClientFactory` is currently fixed, so applications cannot customize default client entry points such as `WebClient.of()`. Rather than overriding the default `ClientFactory` instance itself, this change narrows the scope to customizing the built-in default client factories through `FlagsProvider`. Modifications: - Add `FlagsProvider.defaultClientFactoryConfigurator()`. - Add `Flags.defaultClientFactoryConfigurator()`. - Add `ClientFactoryConfigurator` for customizing `ClientFactoryBuilder`. - Apply the configurator when creating `DefaultClientFactory.DEFAULT` and `DefaultClientFactory.INSECURE`. - Keep `ClientFactory.ofDefault()` returning `DefaultClientFactory.DEFAULT`. - Keep lifecycle behavior such as `closeDefault()`, direct `close()`, and shutdown hook ownership unchanged. - Add SPI coverage in `FlagsProviderTest`. Result: - Users can customize the built-in default client factories via `FlagsProvider`. - Default client entry points that use `ClientFactory.ofDefault()` pick up the customized settings. - The canonical default factory identity and lifecycle remain unchanged. Closes #6425.
1 parent 71c56aa commit 5db7c37

7 files changed

Lines changed: 172 additions & 2 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2026 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package com.linecorp.armeria.client;
17+
18+
import com.linecorp.armeria.common.annotation.UnstableApi;
19+
20+
/**
21+
* Configures a built-in default {@link ClientFactory} using the specified {@link ClientFactoryBuilder}.
22+
*
23+
* <p>This configurator is invoked while creating the built-in default {@link ClientFactory}s returned by
24+
* {@link ClientFactory#ofDefault()} and {@link ClientFactory#insecure()}.
25+
*
26+
* <p>This configurator is applied to both the default and insecure built-in
27+
* {@link ClientFactory}s, so it must not call
28+
* {@link ClientFactory#ofDefault()} or {@link ClientFactory#insecure()}.
29+
*
30+
* <p>Because {@link ClientFactory#insecure()} applies {@link ClientFactoryBuilder#tlsNoVerify()} after this
31+
* configurator runs, TLS verification-related customization is unsupported.
32+
*/
33+
@UnstableApi
34+
@FunctionalInterface
35+
public interface ClientFactoryConfigurator {
36+
37+
/**
38+
* Configures the built-in default {@link ClientFactory} using the specified
39+
* {@link ClientFactoryBuilder}.
40+
*
41+
* <p>Note that {@link ClientFactoryBuilder#tlsNoVerify()} is applied after this method returns when
42+
* creating {@link ClientFactory#insecure()}.
43+
*/
44+
void configureDefault(ClientFactoryBuilder builder);
45+
46+
/**
47+
* Returns a {@link ClientFactoryConfigurator} that does not customize the specified
48+
* {@link ClientFactoryBuilder}.
49+
*/
50+
static ClientFactoryConfigurator noop() {
51+
return builder -> {
52+
};
53+
}
54+
}

core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.google.common.collect.Streams;
3737

3838
import com.linecorp.armeria.client.endpoint.EndpointGroup;
39+
import com.linecorp.armeria.common.Flags;
3940
import com.linecorp.armeria.common.Scheme;
4041
import com.linecorp.armeria.common.SessionProtocol;
4142
import com.linecorp.armeria.common.TlsProvider;
@@ -70,11 +71,14 @@ final class DefaultClientFactory implements ClientFactory {
7071

7172
private static volatile boolean shutdownHookDisabled;
7273

74+
private static final ClientFactoryConfigurator DEFAULT_CLIENT_FACTORY_CONFIGURATOR =
75+
Flags.defaultClientFactoryConfigurator();
76+
7377
static final DefaultClientFactory DEFAULT =
74-
(DefaultClientFactory) ClientFactory.builder().build();
78+
newDefaultClientFactory(false, DEFAULT_CLIENT_FACTORY_CONFIGURATOR);
7579

7680
static final DefaultClientFactory INSECURE =
77-
(DefaultClientFactory) ClientFactory.builder().tlsNoVerify().build();
81+
newDefaultClientFactory(true, DEFAULT_CLIENT_FACTORY_CONFIGURATOR);
7882

7983
static {
8084
if (DefaultClientFactory.class.getClassLoader() == ClassLoader.getSystemClassLoader()) {
@@ -94,6 +98,16 @@ static void disableShutdownHook0() {
9498
shutdownHookDisabled = true;
9599
}
96100

101+
private static DefaultClientFactory newDefaultClientFactory(boolean insecure,
102+
ClientFactoryConfigurator configurator) {
103+
final ClientFactoryBuilder builder = ClientFactory.builder();
104+
configurator.configureDefault(builder);
105+
if (insecure) {
106+
builder.tlsNoVerify();
107+
}
108+
return (DefaultClientFactory) builder.build();
109+
}
110+
97111
private final HttpClientFactory httpClientFactory;
98112
private final Multimap<Scheme, ClientFactory> clientFactories;
99113
private final List<ClientFactory> clientFactoriesToClose;

core/src/main/java/com/linecorp/armeria/common/DefaultFlagsProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.common.base.Splitter;
2727
import com.google.common.collect.ImmutableSet;
2828

29+
import com.linecorp.armeria.client.ClientFactoryConfigurator;
2930
import com.linecorp.armeria.client.ResponseTimeoutMode;
3031
import com.linecorp.armeria.common.multipart.MultipartFilenameDecodingMode;
3132
import com.linecorp.armeria.common.util.Sampler;
@@ -374,6 +375,11 @@ public Long defaultRequestAutoAbortDelayMillis() {
374375
return DEFAULT_REQUEST_AUTO_ABORT_DELAY_MILLIS;
375376
}
376377

378+
@Override
379+
public ClientFactoryConfigurator defaultClientFactoryConfigurator() {
380+
return ClientFactoryConfigurator.noop();
381+
}
382+
377383
@Override
378384
public String routeCacheSpec() {
379385
return ROUTE_CACHE_SPEC;

core/src/main/java/com/linecorp/armeria/common/Flags.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
import com.linecorp.armeria.client.ClientBuilder;
4949
import com.linecorp.armeria.client.ClientFactoryBuilder;
50+
import com.linecorp.armeria.client.ClientFactoryConfigurator;
5051
import com.linecorp.armeria.client.ClientTlsSpec;
5152
import com.linecorp.armeria.client.DnsResolverGroupBuilder;
5253
import com.linecorp.armeria.client.Endpoint;
@@ -1634,6 +1635,22 @@ public static MeterRegistry meterRegistry() {
16341635
return METER_REGISTRY;
16351636
}
16361637

1638+
/**
1639+
* Returns the {@link ClientFactoryConfigurator} that customizes the built-in default
1640+
* {@link com.linecorp.armeria.client.ClientFactory}s.
1641+
*
1642+
* <p>This value is consulted while initializing the built-in default client factories.
1643+
*
1644+
* @see ClientFactoryConfigurator
1645+
*/
1646+
@UnstableApi
1647+
public static ClientFactoryConfigurator defaultClientFactoryConfigurator() {
1648+
final ClientFactoryConfigurator configurator =
1649+
getValue(FlagsProvider::defaultClientFactoryConfigurator,
1650+
"defaultClientFactoryConfigurator");
1651+
return configurator != null ? configurator : ClientFactoryConfigurator.noop();
1652+
}
1653+
16371654
/**
16381655
* Returns the default interval in milliseconds between the reports on unlogged exceptions.
16391656
*

core/src/main/java/com/linecorp/armeria/common/FlagsProvider.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import com.linecorp.armeria.client.ClientBuilder;
3535
import com.linecorp.armeria.client.ClientFactoryBuilder;
36+
import com.linecorp.armeria.client.ClientFactoryConfigurator;
3637
import com.linecorp.armeria.client.DnsResolverGroupBuilder;
3738
import com.linecorp.armeria.client.ResponseTimeoutMode;
3839
import com.linecorp.armeria.client.retry.Backoff;
@@ -822,6 +823,29 @@ default Long defaultRequestAutoAbortDelayMillis() {
822823
return null;
823824
}
824825

826+
/**
827+
* Returns a {@link ClientFactoryConfigurator} that customizes the built-in default
828+
* {@link com.linecorp.armeria.client.ClientFactory}s.
829+
*
830+
* <p>If {@code null} is returned, the next available {@link FlagsProvider} is consulted.</p>
831+
*
832+
* <p>The returned {@link ClientFactoryConfigurator} is applied while creating the built-in default
833+
* {@link com.linecorp.armeria.client.ClientFactory}s, so it must not call
834+
* {@link com.linecorp.armeria.client.ClientFactory#ofDefault()} or
835+
* {@link com.linecorp.armeria.client.ClientFactory#insecure()}.</p>
836+
*
837+
* <p>This configurator is applied to both the default and insecure built-in
838+
* {@link com.linecorp.armeria.client.ClientFactory}s. Because
839+
* {@link com.linecorp.armeria.client.ClientFactory#insecure()} applies
840+
* {@link com.linecorp.armeria.client.ClientFactoryBuilder#tlsNoVerify()} after the configurator runs,
841+
* TLS verification-related customization is unsupported.</p>
842+
*/
843+
@UnstableApi
844+
@Nullable
845+
default ClientFactoryConfigurator defaultClientFactoryConfigurator() {
846+
return null;
847+
}
848+
825849
/**
826850
* Returns the {@linkplain CaffeineSpec Caffeine specification string} of the cache that stores the recent
827851
* request routing history for all {@link Service}s.

it/flags-provider/src/test/java/com/linecorp/armeria/common/BaseFlagsProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.net.InetAddress;
2020
import java.util.function.Predicate;
2121

22+
import com.linecorp.armeria.client.ClientFactoryConfigurator;
2223
import com.linecorp.armeria.common.util.InetAddressPredicates;
2324

2425
import io.micrometer.core.instrument.MeterRegistry;
@@ -71,6 +72,11 @@ public Long defaultServerConnectionDrainDurationMicros() {
7172
return 500L;
7273
}
7374

75+
@Override
76+
public ClientFactoryConfigurator defaultClientFactoryConfigurator() {
77+
return builder -> builder.connectTimeoutMillis(4242);
78+
}
79+
7480
@Override
7581
public String routeCacheSpec() {
7682
return "off";

it/flags-provider/src/test/java/com/linecorp/armeria/common/FlagsProviderTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,21 @@
2525
import java.lang.reflect.Method;
2626
import java.net.URL;
2727
import java.net.URLConnection;
28+
import java.util.Map;
2829

2930
import org.assertj.core.api.ObjectAssert;
3031
import org.junit.jupiter.api.BeforeEach;
3132
import org.junit.jupiter.api.Test;
3233
import org.junitpioneer.jupiter.ClearSystemProperty;
3334
import org.junitpioneer.jupiter.SetSystemProperty;
3435

36+
import com.linecorp.armeria.client.ClientFactory;
3537
import com.linecorp.armeria.common.util.Exceptions;
3638
import com.linecorp.armeria.common.util.InetAddressPredicates;
3739
import com.linecorp.armeria.common.util.TlsEngineType;
3840

3941
import io.micrometer.core.instrument.Metrics;
42+
import io.netty.channel.ChannelOption;
4043

4144
@SetSystemProperty(
4245
key = "com.linecorp.armeria.requestContextStorageProvider",
@@ -45,11 +48,13 @@
4548
class FlagsProviderTest {
4649

4750
private Class<?> flags;
51+
private Class<?> clientFactoryClass;
4852

4953
@BeforeEach
5054
void reloadFlags() throws ClassNotFoundException {
5155
final FlagsClassLoader classLoader = new FlagsClassLoader();
5256
flags = classLoader.loadClass(Flags.class.getCanonicalName());
57+
clientFactoryClass = classLoader.loadClass(ClientFactory.class.getName());
5358
}
5459

5560
@Test
@@ -159,11 +164,55 @@ void testDistributionStatisticConfig() {
159164
.isEqualTo(DistributionStatisticConfigUtil.DEFAULT_DIST_STAT_CFG);
160165
}
161166

167+
@Test
168+
void defaultClientFactoryConfiguratorIsAppliedToDefaultClientFactory() throws Throwable {
169+
try {
170+
assertClientFactoryConnectTimeoutMillis("ofDefault").isEqualTo(4242);
171+
} finally {
172+
closeDefaultClientFactories();
173+
}
174+
}
175+
176+
@Test
177+
void defaultClientFactoryConfiguratorIsAppliedToInsecureClientFactory() throws Throwable {
178+
try {
179+
assertClientFactoryConnectTimeoutMillis("insecure").isEqualTo(4242);
180+
} finally {
181+
closeDefaultClientFactories();
182+
}
183+
}
184+
162185
private ObjectAssert<Object> assertFlags(String flagsMethod) throws Throwable {
163186
final Method method = flags.getDeclaredMethod(flagsMethod);
164187
return assertThat(method.invoke(null));
165188
}
166189

190+
private ObjectAssert<Object> assertClientFactoryConnectTimeoutMillis(String factoryMethod)
191+
throws Throwable {
192+
final Object options = clientFactoryOptions(factoryMethod);
193+
final Method channelOptionsMethod = options.getClass().getDeclaredMethod("channelOptions");
194+
@SuppressWarnings("unchecked")
195+
final Map<ChannelOption<?>, Object> channelOptions =
196+
(Map<ChannelOption<?>, Object>) channelOptionsMethod.invoke(options);
197+
return assertThat(channelOptions.get(ChannelOption.CONNECT_TIMEOUT_MILLIS));
198+
}
199+
200+
private Object clientFactoryOptions(String factoryMethod) throws Throwable {
201+
final Object clientFactory = clientFactory(factoryMethod);
202+
final Method optionsMethod = clientFactoryClass.getDeclaredMethod("options");
203+
return optionsMethod.invoke(clientFactory);
204+
}
205+
206+
private Object clientFactory(String factoryMethod) throws Throwable {
207+
final Method factoryGetter = clientFactoryClass.getDeclaredMethod(factoryMethod);
208+
return factoryGetter.invoke(null);
209+
}
210+
211+
private void closeDefaultClientFactories() throws Throwable {
212+
final Method closeDefaultMethod = clientFactoryClass.getDeclaredMethod("closeDefault");
213+
closeDefaultMethod.invoke(null);
214+
}
215+
167216
private static class FlagsClassLoader extends ClassLoader {
168217
FlagsClassLoader() {
169218
super(getSystemClassLoader());

0 commit comments

Comments
 (0)