Skip to content

Commit 38968d2

Browse files
committed
Polish 'Apply TTL invocation caching on reactor types'
Extract reactor specific code to an inner class to protect against ClassNotFound exceptions if reactor is not in use. Also add support for `Flux`. See gh-18339
1 parent 33d8bfa commit 38968d2

File tree

2 files changed

+75
-27
lines changed

2 files changed

+75
-27
lines changed

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

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,28 @@
2020
import java.util.Map;
2121
import java.util.Objects;
2222

23+
import reactor.core.publisher.Flux;
2324
import reactor.core.publisher.Mono;
2425

2526
import org.springframework.boot.actuate.endpoint.InvocationContext;
2627
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
2728
import org.springframework.util.Assert;
29+
import org.springframework.util.ClassUtils;
2830
import org.springframework.util.ObjectUtils;
2931

3032
/**
3133
* An {@link OperationInvoker} that caches the response of an operation with a
3234
* configurable time to live.
3335
*
3436
* @author Stephane Nicoll
37+
* @author Christoph Dreis
38+
* @author Phillip Webb
3539
* @since 2.0.0
3640
*/
3741
public class CachingOperationInvoker implements OperationInvoker {
3842

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

4147
private final long timeToLive;
@@ -70,20 +76,13 @@ public Object invoke(InvocationContext context) {
7076
long accessTime = System.currentTimeMillis();
7177
CachedResponse cached = this.cachedResponse;
7278
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
73-
Object response = handleMonoResponse(this.invoker.invoke(context));
74-
this.cachedResponse = new CachedResponse(response, accessTime);
75-
return response;
79+
Object response = this.invoker.invoke(context);
80+
cached = createCachedResponse(response, accessTime);
81+
this.cachedResponse = cached;
7682
}
7783
return cached.getResponse();
7884
}
7985

80-
private Object handleMonoResponse(Object response) {
81-
if (response instanceof Mono) {
82-
return ((Mono) response).cache(Duration.ofMillis(this.timeToLive));
83-
}
84-
return response;
85-
}
86-
8786
private boolean hasInput(InvocationContext context) {
8887
if (context.getSecurityContext().getPrincipal() != null) {
8988
return true;
@@ -95,6 +94,13 @@ private boolean hasInput(InvocationContext context) {
9594
return false;
9695
}
9796

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+
98104
/**
99105
* Apply caching configuration when appropriate to the given invoker.
100106
* @param invoker the invoker to wrap
@@ -134,4 +140,25 @@ public Object getResponse() {
134140

135141
}
136142

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+
137164
}

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

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

1919
import java.security.Principal;
20-
import java.time.Duration;
20+
import java.util.Arrays;
2121
import java.util.Collections;
2222
import java.util.HashMap;
2323
import java.util.Map;
2424

25-
import org.junit.Rule;
2625
import org.junit.Test;
26+
import reactor.core.publisher.Flux;
2727
import reactor.core.publisher.Mono;
2828

2929
import org.springframework.boot.actuate.endpoint.InvocationContext;
3030
import org.springframework.boot.actuate.endpoint.SecurityContext;
3131
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
3232
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
33-
import org.springframework.boot.test.rule.OutputCapture;
3433

3534
import static org.assertj.core.api.Assertions.assertThat;
3635
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -44,12 +43,11 @@
4443
* Tests for {@link CachingOperationInvoker}.
4544
*
4645
* @author Stephane Nicoll
46+
* @author Christoph Dreis
47+
* @author Phillip Webb
4748
*/
4849
public class CachingOperationInvokerTests {
4950

50-
@Rule
51-
public OutputCapture outputCapture = new OutputCapture();
52-
5351
@Test
5452
public void createInstanceWithTtlSetToZero() {
5553
assertThatIllegalArgumentException()
@@ -72,17 +70,26 @@ public void cacheInTtlWithNullParameters() {
7270

7371
@Test
7472
public void cacheInTtlWithMonoResponse() {
73+
MonoOperationInvoker.invocations = 0;
7574
MonoOperationInvoker target = new MonoOperationInvoker();
7675
InvocationContext context = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap());
7776
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
78-
Object monoResponse = invoker.invoke(context);
79-
assertThat(monoResponse).isInstanceOf(Mono.class);
80-
Object response = ((Mono) monoResponse).block(Duration.ofSeconds(30));
81-
Object cachedMonoResponse = invoker.invoke(context);
82-
assertThat(cachedMonoResponse).isInstanceOf(Mono.class);
83-
Object cachedResponse = ((Mono) cachedMonoResponse).block(Duration.ofSeconds(30));
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);
8492
assertThat(response).isSameAs(cachedResponse);
85-
assertThat(this.outputCapture.toString()).containsOnlyOnce("invoked");
8693
}
8794

8895
private void assertCacheIsUsed(Map<String, Object> parameters) {
@@ -144,14 +151,28 @@ public void targetInvokedWhenCacheExpires() throws InterruptedException {
144151

145152
private static class MonoOperationInvoker implements OperationInvoker {
146153

154+
static int invocations;
155+
147156
@Override
148157
public Object invoke(InvocationContext context) throws MissingParametersException {
149-
return Mono.fromCallable(this::printInvocation);
158+
return Mono.fromCallable(() -> {
159+
invocations++;
160+
return Mono.just("test");
161+
});
150162
}
151163

152-
private Mono<String> printInvocation() {
153-
System.out.println("MonoOperationInvoker invoked");
154-
return Mono.just("test");
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+
});
155176
}
156177

157178
}

0 commit comments

Comments
 (0)