Skip to content

Commit a1fb1bc

Browse files
committed
Merge pull request #18339 from dreis2211
* pr/18339: Polish 'Apply TTL invocation caching on reactor types' Apply TTL invocation caching on reactor types Closes gh-18339
2 parents 89e7d5f + 38968d2 commit a1fb1bc

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,32 @@
1616

1717
package org.springframework.boot.actuate.endpoint.invoker.cache;
1818

19+
import java.time.Duration;
1920
import java.util.Map;
2021
import java.util.Objects;
2122

23+
import reactor.core.publisher.Flux;
24+
import reactor.core.publisher.Mono;
25+
2226
import org.springframework.boot.actuate.endpoint.InvocationContext;
2327
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
2428
import org.springframework.util.Assert;
29+
import org.springframework.util.ClassUtils;
2530
import org.springframework.util.ObjectUtils;
2631

2732
/**
2833
* An {@link OperationInvoker} that caches the response of an operation with a
2934
* configurable time to live.
3035
*
3136
* @author Stephane Nicoll
37+
* @author Christoph Dreis
38+
* @author Phillip Webb
3239
* @since 2.0.0
3340
*/
3441
public class CachingOperationInvoker implements OperationInvoker {
3542

43+
private static final boolean IS_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Mono", null);
44+
3645
private final OperationInvoker invoker;
3746

3847
private final long timeToLive;
@@ -68,8 +77,8 @@ public Object invoke(InvocationContext context) {
6877
CachedResponse cached = this.cachedResponse;
6978
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
7079
Object response = this.invoker.invoke(context);
71-
this.cachedResponse = new CachedResponse(response, accessTime);
72-
return response;
80+
cached = createCachedResponse(response, accessTime);
81+
this.cachedResponse = cached;
7382
}
7483
return cached.getResponse();
7584
}
@@ -85,6 +94,13 @@ private boolean hasInput(InvocationContext context) {
8594
return false;
8695
}
8796

97+
private CachedResponse createCachedResponse(Object response, long accessTime) {
98+
if (IS_REACTOR_PRESENT) {
99+
return new ReactiveCachedResponse(response, accessTime, this.timeToLive);
100+
}
101+
return new CachedResponse(response, accessTime);
102+
}
103+
88104
/**
89105
* Apply caching configuration when appropriate to the given invoker.
90106
* @param invoker the invoker to wrap
@@ -124,4 +140,25 @@ public Object getResponse() {
124140

125141
}
126142

143+
/**
144+
* {@link CachedResponse} variant used when Reactor is present.
145+
*/
146+
static class ReactiveCachedResponse extends CachedResponse {
147+
148+
ReactiveCachedResponse(Object response, long creationTime, long timeToLive) {
149+
super(applyCaching(response, timeToLive), creationTime);
150+
}
151+
152+
private static Object applyCaching(Object response, long timeToLive) {
153+
if (response instanceof Mono) {
154+
return ((Mono<?>) response).cache(Duration.ofMillis(timeToLive));
155+
}
156+
if (response instanceof Flux) {
157+
return ((Flux<?>) response).cache(Duration.ofMillis(timeToLive));
158+
}
159+
return response;
160+
}
161+
162+
}
163+
127164
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@
1717
package org.springframework.boot.actuate.endpoint.invoker.cache;
1818

1919
import java.security.Principal;
20+
import java.util.Arrays;
2021
import java.util.Collections;
2122
import java.util.HashMap;
2223
import java.util.Map;
2324

2425
import org.junit.Test;
26+
import reactor.core.publisher.Flux;
27+
import reactor.core.publisher.Mono;
2528

2629
import org.springframework.boot.actuate.endpoint.InvocationContext;
2730
import org.springframework.boot.actuate.endpoint.SecurityContext;
31+
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
2832
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
2933

3034
import static org.assertj.core.api.Assertions.assertThat;
@@ -39,6 +43,8 @@
3943
* Tests for {@link CachingOperationInvoker}.
4044
*
4145
* @author Stephane Nicoll
46+
* @author Christoph Dreis
47+
* @author Phillip Webb
4248
*/
4349
public class CachingOperationInvokerTests {
4450

@@ -62,6 +68,30 @@ public void cacheInTtlWithNullParameters() {
6268
assertCacheIsUsed(parameters);
6369
}
6470

71+
@Test
72+
public void cacheInTtlWithMonoResponse() {
73+
MonoOperationInvoker.invocations = 0;
74+
MonoOperationInvoker target = new MonoOperationInvoker();
75+
InvocationContext context = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap());
76+
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
77+
Object response = ((Mono<?>) invoker.invoke(context)).block();
78+
Object cachedResponse = ((Mono<?>) invoker.invoke(context)).block();
79+
assertThat(MonoOperationInvoker.invocations).isEqualTo(1);
80+
assertThat(response).isSameAs(cachedResponse);
81+
}
82+
83+
@Test
84+
public void cacheInTtlWithFluxResponse() {
85+
FluxOperationInvoker.invocations = 0;
86+
FluxOperationInvoker target = new FluxOperationInvoker();
87+
InvocationContext context = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap());
88+
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
89+
Object response = ((Flux<?>) invoker.invoke(context)).blockLast();
90+
Object cachedResponse = ((Flux<?>) invoker.invoke(context)).blockLast();
91+
assertThat(FluxOperationInvoker.invocations).isEqualTo(1);
92+
assertThat(response).isSameAs(cachedResponse);
93+
}
94+
6595
private void assertCacheIsUsed(Map<String, Object> parameters) {
6696
OperationInvoker target = mock(OperationInvoker.class);
6797
Object expected = new Object();
@@ -119,4 +149,32 @@ public void targetInvokedWhenCacheExpires() throws InterruptedException {
119149
verify(target, times(2)).invoke(context);
120150
}
121151

152+
private static class MonoOperationInvoker implements OperationInvoker {
153+
154+
static int invocations;
155+
156+
@Override
157+
public Object invoke(InvocationContext context) throws MissingParametersException {
158+
return Mono.fromCallable(() -> {
159+
invocations++;
160+
return Mono.just("test");
161+
});
162+
}
163+
164+
}
165+
166+
private static class FluxOperationInvoker implements OperationInvoker {
167+
168+
static int invocations;
169+
170+
@Override
171+
public Object invoke(InvocationContext context) throws MissingParametersException {
172+
return Flux.fromIterable(() -> {
173+
invocations++;
174+
return Arrays.asList("spring", "boot").iterator();
175+
});
176+
}
177+
178+
}
179+
122180
}

0 commit comments

Comments
 (0)