Skip to content

Commit a1405d0

Browse files
support http-exchange configuration dynamic refresh (#17)
1 parent 6d8c8d7 commit a1405d0

File tree

7 files changed

+142
-29
lines changed

7 files changed

+142
-29
lines changed

build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ repositories {
1717
dependencies {
1818
api(platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}"))
1919
annotationProcessor(platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}"))
20+
compileOnly(platform("org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"))
21+
testImplementation(platform("org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"))
2022

2123
api("org.springframework.boot:spring-boot-starter")
2224
api("org.springframework:spring-webflux")
2325

26+
// dynamic refresh configuration for exchange clients
27+
compileOnly("org.springframework.cloud:spring-cloud-context")
28+
2429
compileOnly("org.projectlombok:lombok")
2530
annotationProcessor("org.projectlombok:lombok")
2631
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
@@ -31,6 +36,7 @@ dependencies {
3136
testCompileOnly("org.springframework.boot:spring-boot-starter-web")
3237
testCompileOnly("org.springframework.boot:spring-boot-starter-webflux")
3338
testCompileOnly("org.springframework.boot:spring-boot-starter-validation")
39+
testImplementation("org.springframework.cloud:spring-cloud-context")
3440
}
3541

3642
compileJava {

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ artifact=httpexchange-spring-boot-starter
33
version=3.1.2-SNAPSHOT
44

55
springBootVersion=3.1.1
6+
springCloudVersion=2022.0.3
67
spotlessVersion=6.19.0
78
spotbugsVersion=5.0.14
89

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
11
package com.freemanan.starter.httpexchange;
22

3-
import java.util.Set;
3+
import java.util.Map;
44
import java.util.concurrent.ConcurrentHashMap;
55
import lombok.experimental.UtilityClass;
6+
import org.springframework.aop.framework.AopProxyUtils;
67

78
/**
89
* @author Freeman
910
*/
1011
@UtilityClass
1112
class Cache {
12-
private static final Set<Class<?>> clientClasses = ConcurrentHashMap.newKeySet();
13+
/**
14+
* Cache all clients.
15+
*/
16+
private static final Map<Class<?>, Object> classToInstance = new ConcurrentHashMap<>();
1317

14-
public static void addClientClass(Class<?> type) {
15-
clientClasses.add(type);
18+
/**
19+
* Add client to cache.
20+
*
21+
* @param client client
22+
*/
23+
public static void addClient(Object client) {
24+
classToInstance.put(AopProxyUtils.ultimateTargetClass(client), client);
1625
}
1726

18-
public static Set<Class<?>> getClientClasses() {
19-
return Set.copyOf(clientClasses);
27+
/**
28+
* Get clients.
29+
*
30+
* @return unmodifiable map
31+
*/
32+
public static Map<Class<?>, Object> getClients() {
33+
return Map.copyOf(classToInstance);
2034
}
2135

36+
/**
37+
* Clear cache.
38+
*/
2239
public static void clear() {
23-
clientClasses.clear();
40+
classToInstance.clear();
2441
}
2542
}

src/main/java/com/freemanan/starter/httpexchange/ExchangeClientCreator.java

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.freemanan.starter.httpexchange.shaded.ShadedHttpServiceProxyFactory;
66
import java.time.Duration;
77
import java.util.Optional;
8+
import java.util.concurrent.atomic.AtomicReference;
89
import org.slf4j.Logger;
910
import org.slf4j.LoggerFactory;
1011
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@@ -29,21 +30,15 @@ class ExchangeClientCreator {
2930
private final ConfigurableBeanFactory beanFactory;
3031
private final Environment environment;
3132
private final Class<?> clientType;
32-
private final HttpClientsProperties.Channel channelConfig;
3333
private final boolean usingClientSideAnnotation;
34+
private final AtomicReference<HttpClientsProperties> properties = new AtomicReference<>();
3435

35-
ExchangeClientCreator(
36-
ConfigurableBeanFactory beanFactory,
37-
HttpClientsProperties properties,
38-
Class<?> clientType,
39-
boolean usingClientSideAnnotation) {
36+
ExchangeClientCreator(ConfigurableBeanFactory beanFactory, Class<?> clientType, boolean usingClientSideAnnotation) {
4037
Assert.notNull(beanFactory, "beanFactory must not be null");
41-
Assert.notNull(properties, "properties must not be null");
4238
Assert.notNull(clientType, "clientType must not be null");
4339
this.beanFactory = beanFactory;
4440
this.environment = beanFactory.getBean(Environment.class);
4541
this.clientType = clientType;
46-
this.channelConfig = findMatchedConfig(clientType, properties).orElseGet(properties::defaultClient);
4742
this.usingClientSideAnnotation = usingClientSideAnnotation;
4843
}
4944

@@ -55,33 +50,45 @@ class ExchangeClientCreator {
5550
*/
5651
@SuppressWarnings("unchecked")
5752
public <T> T create() {
53+
HttpClientsProperties httpClientsProperties = beanFactory
54+
.getBeanProvider(HttpClientsProperties.class)
55+
.getIfUnique(() -> {
56+
HttpClientsProperties prop = properties.get();
57+
if (prop == null) {
58+
properties.set(Util.getProperties(environment));
59+
}
60+
return properties.get();
61+
});
62+
HttpClientsProperties.Channel chan =
63+
findMatchedConfig(clientType, httpClientsProperties).orElseGet(httpClientsProperties::defaultClient);
5864
if (usingClientSideAnnotation) {
59-
HttpServiceProxyFactory cachedFactory = buildServiceProxyFactory();
65+
HttpServiceProxyFactory cachedFactory = buildServiceProxyFactory(chan);
6066
T result = (T) cachedFactory.createClient(clientType);
61-
Cache.addClientClass(clientType);
67+
Cache.addClient(result);
6268
return result;
6369
}
64-
ShadedHttpServiceProxyFactory cachedFactory = buildShadedServiceProxyFactory();
70+
ShadedHttpServiceProxyFactory cachedFactory = buildShadedServiceProxyFactory(chan);
6571
T result = (T) cachedFactory.createClient(clientType);
66-
Cache.addClientClass(clientType);
72+
Cache.addClient(result);
6773
return result;
6874
}
6975

70-
private HttpServiceProxyFactory buildServiceProxyFactory() {
71-
HttpServiceProxyFactory.Builder builder = proxyFactoryBuilder();
76+
private HttpServiceProxyFactory buildServiceProxyFactory(HttpClientsProperties.Channel channelConfig) {
77+
HttpServiceProxyFactory.Builder builder = proxyFactoryBuilder(channelConfig);
7278
return builder.build();
7379
}
7480

75-
private ShadedHttpServiceProxyFactory buildShadedServiceProxyFactory() {
76-
ShadedHttpServiceProxyFactory.Builder builder = ShadedHttpServiceProxyFactory.builder(proxyFactoryBuilder());
81+
private ShadedHttpServiceProxyFactory buildShadedServiceProxyFactory(HttpClientsProperties.Channel channelConfig) {
82+
ShadedHttpServiceProxyFactory.Builder builder =
83+
ShadedHttpServiceProxyFactory.builder(proxyFactoryBuilder(channelConfig));
7784
return builder.build();
7885
}
7986

80-
private HttpServiceProxyFactory.Builder proxyFactoryBuilder() {
87+
private HttpServiceProxyFactory.Builder proxyFactoryBuilder(HttpClientsProperties.Channel channelConfig) {
8188
HttpServiceProxyFactory.Builder builder = beanFactory
8289
.getBeanProvider(HttpServiceProxyFactory.Builder.class)
8390
.getIfUnique(HttpServiceProxyFactory::builder)
84-
.clientAdapter(WebClientAdapter.forClient(buildWebClient()));
91+
.clientAdapter(WebClientAdapter.forClient(buildWebClient(channelConfig)));
8592

8693
// Customized argument resolvers
8794
beanFactory
@@ -102,7 +109,7 @@ private HttpServiceProxyFactory.Builder proxyFactoryBuilder() {
102109
return builder;
103110
}
104111

105-
private WebClient buildWebClient() {
112+
private WebClient buildWebClient(HttpClientsProperties.Channel channelConfig) {
106113
WebClient.Builder builder =
107114
beanFactory.getBeanProvider(WebClient.Builder.class).getIfUnique(WebClient::builder);
108115
if (StringUtils.hasText(channelConfig.getBaseUrl())) {

src/main/java/com/freemanan/starter/httpexchange/HttpClientBeanRegistrar.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@
77
import java.util.concurrent.ConcurrentHashMap;
88
import org.slf4j.Logger;
99
import org.slf4j.LoggerFactory;
10+
import org.springframework.aop.scope.ScopedProxyUtils;
1011
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
1112
import org.springframework.beans.factory.config.BeanDefinition;
13+
import org.springframework.beans.factory.config.BeanDefinitionHolder;
1214
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
1315
import org.springframework.beans.factory.support.AbstractBeanDefinition;
1416
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
1517
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
18+
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
1619
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
1720
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
1821
import org.springframework.core.annotation.AnnotationUtils;
1922
import org.springframework.core.type.AnnotationMetadata;
2023
import org.springframework.core.type.ClassMetadata;
2124
import org.springframework.core.type.classreading.MetadataReader;
2225
import org.springframework.util.Assert;
26+
import org.springframework.util.ClassUtils;
2327
import org.springframework.util.ObjectUtils;
2428
import org.springframework.util.ReflectionUtils;
2529
import org.springframework.web.bind.annotation.RequestMapping;
@@ -32,6 +36,8 @@ class HttpClientBeanRegistrar {
3236
private static final Logger log = LoggerFactory.getLogger(HttpClientBeanRegistrar.class);
3337

3438
private static final Set<BeanDefinitionRegistry> registries = ConcurrentHashMap.newKeySet();
39+
private static final boolean SPRING_CLOUD_CONTEXT_PRESENT =
40+
ClassUtils.isPresent("org.springframework.cloud.context.scope.refresh.RefreshScope", null);
3541

3642
private final ClassPathScanningCandidateComponentProvider scanner;
3743
private final HttpClientsProperties properties;
@@ -89,7 +95,7 @@ public void registerHttpClientBean(BeanDefinitionRegistry registry, String class
8995
Assert.isInstanceOf(ConfigurableBeanFactory.class, registry);
9096

9197
ExchangeClientCreator creator =
92-
new ExchangeClientCreator((ConfigurableBeanFactory) registry, properties, clz, hasClientSideAnnotation);
98+
new ExchangeClientCreator((ConfigurableBeanFactory) registry, clz, hasClientSideAnnotation);
9399

94100
AbstractBeanDefinition abd = BeanDefinitionBuilder.genericBeanDefinition(clz, creator::create)
95101
.getBeanDefinition();
@@ -99,7 +105,14 @@ public void registerHttpClientBean(BeanDefinitionRegistry registry, String class
99105
abd.setLazyInit(true);
100106

101107
try {
102-
registry.registerBeanDefinition(className, abd);
108+
if (SPRING_CLOUD_CONTEXT_PRESENT) {
109+
abd.setScope("refresh");
110+
BeanDefinitionHolder scopedProxy =
111+
ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(abd, className), registry, false);
112+
BeanDefinitionReaderUtils.registerBeanDefinition(scopedProxy, registry);
113+
} else {
114+
registry.registerBeanDefinition(className, abd);
115+
}
103116
} catch (BeanDefinitionOverrideException ignore) {
104117
// clients are included in base packages
105118
log.warn(

src/main/java/com/freemanan/starter/httpexchange/HttpClientsAutoConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void destroy() {
5757

5858
private void warningUnusedConfiguration() {
5959
// Identify the configuration items that are not taking effect and print warning messages.
60-
Set<Class<?>> classes = Cache.getClientClasses();
60+
Set<Class<?>> classes = Cache.getClients().keySet();
6161

6262
List<HttpClientsProperties.Channel> channels = properties.getChannels();
6363

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.freemanan.starter.httpexchange;
2+
3+
import static com.freemanan.starter.Dependencies.springBootVersion;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
6+
import com.freemanan.cr.core.anno.Action;
7+
import com.freemanan.cr.core.anno.ClasspathReplacer;
8+
import com.freemanan.starter.PortFinder;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
11+
import org.springframework.boot.builder.SpringApplicationBuilder;
12+
import org.springframework.cloud.endpoint.event.RefreshEvent;
13+
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.core.env.ConfigurableEnvironment;
15+
import org.springframework.web.bind.annotation.GetMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
import org.springframework.web.service.annotation.GetExchange;
18+
19+
/**
20+
* @author Freeman
21+
*/
22+
class DynamicRefreshTests {
23+
24+
@Test
25+
@ClasspathReplacer({@Action("org.springframework.boot:spring-boot-starter-webflux:" + springBootVersion)})
26+
void testDynamicRefresh() {
27+
int port = PortFinder.availablePort();
28+
var ctx = new SpringApplicationBuilder(Cfg.class)
29+
.properties("server.port=" + port)
30+
.properties("http-exchange.base-url=http://localhost:" + port)
31+
.run();
32+
ConfigurableEnvironment env = ctx.getEnvironment();
33+
34+
FooApi api = ctx.getBean(FooApi.class);
35+
assertThat(api.get()).isEqualTo("OK");
36+
37+
env.getSystemProperties().put("http-exchange.base-url", "http://localhost:" + port + "/v2");
38+
ctx.publishEvent(new RefreshEvent(ctx, null, null));
39+
40+
// base-url changed
41+
assertThat(api.get()).isEqualTo("OK v2");
42+
43+
ctx.close();
44+
}
45+
46+
@Configuration(proxyBeanMethods = false)
47+
@EnableAutoConfiguration
48+
@EnableExchangeClients(clients = FooApi.class)
49+
@RestController
50+
static class Cfg implements FooApi {
51+
52+
@Override
53+
@GetMapping("/get")
54+
public String get() {
55+
return "OK";
56+
}
57+
58+
@GetMapping("/v2/get")
59+
public String getV2() {
60+
return "OK v2";
61+
}
62+
}
63+
64+
interface FooApi {
65+
66+
@GetExchange("/get")
67+
String get();
68+
}
69+
}

0 commit comments

Comments
 (0)