Skip to content

Commit 374b4b7

Browse files
artembilangaryrussell
authored andcommitted
GH-2880: Handle Pausable in Control Bus (#2940)
* GH-2880: Handle `Pausable` in Control Bus Fixes #2880 * Refactor `ControlBusMethodFilter` to handle `Pausable` managed operations * Optimize and internal `ControlBusMethodFilter.filter()` logic to rely on the `MergedAnnotations` * Modify `EnableIntegrationTests` to test new functionality and document the feature * * `ControlBusMethodFilter` to deal with plain `Lifecycle` impls as well
1 parent e9591c6 commit 374b4b7

File tree

4 files changed

+54
-28
lines changed

4 files changed

+54
-28
lines changed

spring-integration-core/src/main/java/org/springframework/integration/expression/ControlBusMethodFilter.java

+24-19
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616

1717
package org.springframework.integration.expression;
1818

19-
import java.lang.annotation.Annotation;
2019
import java.lang.reflect.Method;
2120
import java.util.ArrayList;
2221
import java.util.List;
2322

2423
import org.springframework.context.Lifecycle;
25-
import org.springframework.core.annotation.AnnotationUtils;
24+
import org.springframework.core.annotation.AnnotationFilter;
25+
import org.springframework.core.annotation.MergedAnnotations;
26+
import org.springframework.core.annotation.RepeatableContainers;
2627
import org.springframework.expression.MethodFilter;
28+
import org.springframework.integration.endpoint.Pausable;
2729
import org.springframework.jmx.export.annotation.ManagedAttribute;
2830
import org.springframework.jmx.export.annotation.ManagedOperation;
2931
import org.springframework.util.CustomizableThreadCreator;
@@ -32,22 +34,23 @@
3234
/**
3335
* SpEL {@link MethodFilter} to restrict method invocations to:
3436
* <ul>
35-
* <li> {@link Lifecycle} components
37+
* <li> {@link Pausable} or {@link Lifecycle} components
3638
* <li> {@code get}, {@code set} and {@code shutdown} methods of {@link CustomizableThreadCreator}
3739
* <li> methods with {@link ManagedAttribute} and {@link ManagedOperation} annotations
3840
* </ul>
3941
* This class isn't designed for target applications and typically is used from {@code ExpressionControlBusFactoryBean}.
4042
*
4143
* @author Mark Fisher
4244
* @author Artem Bilan
43-
* @since 4.0
44-
*/
45+
*
46+
* @since 4.0
47+
*/
4548
public class ControlBusMethodFilter implements MethodFilter {
4649

4750
public List<Method> filter(List<Method> methods) {
48-
List<Method> supportedMethods = new ArrayList<Method>();
51+
List<Method> supportedMethods = new ArrayList<>();
4952
for (Method method : methods) {
50-
if (this.accept(method)) {
53+
if (accept(method)) {
5154
supportedMethods.add(method);
5255
}
5356
}
@@ -56,23 +59,25 @@ public List<Method> filter(List<Method> methods) {
5659

5760
private boolean accept(Method method) {
5861
Class<?> declaringClass = method.getDeclaringClass();
59-
if (Lifecycle.class.isAssignableFrom(declaringClass)
60-
&& ReflectionUtils.findMethod(Lifecycle.class, method.getName(), method.getParameterTypes()) != null) {
62+
String methodName = method.getName();
63+
if ((Pausable.class.isAssignableFrom(declaringClass) || Lifecycle.class.isAssignableFrom(declaringClass))
64+
&& ReflectionUtils.findMethod(Pausable.class, methodName, method.getParameterTypes()) != null) {
6165
return true;
6266
}
67+
6368
if (CustomizableThreadCreator.class.isAssignableFrom(declaringClass)
64-
&& (method.getName().startsWith("get")
65-
|| method.getName().startsWith("set")
66-
|| method.getName().startsWith("shutdown"))) {
67-
return true;
68-
}
69-
if (this.hasAnnotation(method, ManagedAttribute.class) || this.hasAnnotation(method, ManagedOperation.class)) {
69+
&& (methodName.startsWith("get")
70+
|| methodName.startsWith("set")
71+
|| methodName.startsWith("shutdown"))) {
7072
return true;
7173
}
72-
return false;
73-
}
7474

75-
private boolean hasAnnotation(Method method, Class<? extends Annotation> annotationType) {
76-
return AnnotationUtils.findAnnotation(method, annotationType) != null;
75+
MergedAnnotations mergedAnnotations =
76+
MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.EXHAUSTIVE,
77+
RepeatableContainers.none(), AnnotationFilter.PLAIN);
78+
79+
return mergedAnnotations.get(ManagedAttribute.class).isPresent()
80+
|| mergedAnnotations.get(ManagedOperation.class).isPresent();
7781
}
82+
7883
}

spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java

+26-8
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
import org.springframework.integration.core.MessagingTemplate;
9595
import org.springframework.integration.endpoint.AbstractEndpoint;
9696
import org.springframework.integration.endpoint.MethodInvokingMessageSource;
97+
import org.springframework.integration.endpoint.Pausable;
9798
import org.springframework.integration.endpoint.PollingConsumer;
9899
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
99100
import org.springframework.integration.gateway.GatewayProxyFactoryBean;
@@ -409,11 +410,16 @@ public void testAnnotatedServiceActivator() throws Exception {
409410
assertThat(message.getHeaders().get("foo")).isEqualTo("FOO");
410411

411412
MessagingTemplate messagingTemplate = new MessagingTemplate(this.controlBusChannel);
412-
assertThat(messagingTemplate.convertSendAndReceive("@lifecycle.isRunning()", Boolean.class)).isEqualTo(false);
413-
this.controlBusChannel.send(new GenericMessage<>("@lifecycle.start()"));
414-
assertThat(messagingTemplate.convertSendAndReceive("@lifecycle.isRunning()", Boolean.class)).isEqualTo(true);
415-
this.controlBusChannel.send(new GenericMessage<>("@lifecycle.stop()"));
416-
assertThat(messagingTemplate.convertSendAndReceive("@lifecycle.isRunning()", Boolean.class)).isEqualTo(false);
413+
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(false);
414+
this.controlBusChannel.send(new GenericMessage<>("@pausable.start()"));
415+
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(true);
416+
this.controlBusChannel.send(new GenericMessage<>("@pausable.stop()"));
417+
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(false);
418+
this.controlBusChannel.send(new GenericMessage<>("@pausable.pause()"));
419+
Object pausable = this.context.getBean("pausable");
420+
assertThat(TestUtils.getPropertyValue(pausable, "paused", Boolean.class)).isTrue();
421+
this.controlBusChannel.send(new GenericMessage<>("@pausable.resume()"));
422+
assertThat(TestUtils.getPropertyValue(pausable, "paused", Boolean.class)).isFalse();
417423

418424
Map<String, ServiceActivatingHandler> beansOfType =
419425
this.context.getBeansOfType(ServiceActivatingHandler.class);
@@ -638,7 +644,7 @@ public void testBridgeAnnotations() {
638644
@Test
639645
public void testMonoGateway() throws Exception {
640646

641-
final AtomicReference<List<Integer>> ref = new AtomicReference<List<Integer>>();
647+
final AtomicReference<List<Integer>> ref = new AtomicReference<>();
642648
final CountDownLatch consumeLatch = new CountDownLatch(1);
643649

644650
Flux.just("1", "2", "3", "4", "5")
@@ -1035,11 +1041,13 @@ public ExpressionControlBusFactoryBean controlBus() {
10351041
}
10361042

10371043
@Bean
1038-
public Lifecycle lifecycle() {
1039-
return new Lifecycle() {
1044+
public Pausable pausable() {
1045+
return new Pausable() {
10401046

10411047
private volatile boolean running;
10421048

1049+
private volatile boolean paused;
1050+
10431051
@Override
10441052
public void start() {
10451053
this.running = true;
@@ -1055,6 +1063,16 @@ public boolean isRunning() {
10551063
return this.running;
10561064
}
10571065

1066+
@Override
1067+
public void pause() {
1068+
this.paused = true;
1069+
}
1070+
1071+
@Override
1072+
public void resume() {
1073+
this.paused = false;
1074+
}
1075+
10581076
};
10591077
}
10601078

src/reference/asciidoc/control-bus.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ For example, you can specify an output channel if the result of the operation ha
2020
The control bus runs messages on the input channel as Spring Expression Language (SpEL) expressions.
2121
It takes a message, compiles the body to an expression, adds some context, and then runs it.
2222
The default context supports any method that has been annotated with `@ManagedAttribute` or `@ManagedOperation`.
23-
It also supports the methods on Spring's `Lifecycle` interface, and it supports methods that are used to configure several of Spring's `TaskExecutor` and `TaskScheduler` implementations.
23+
It also supports the methods on Spring's `Lifecycle` interface (and its `Pausable` extension since version 5.2), and it supports methods that are used to configure several of Spring's `TaskExecutor` and `TaskScheduler` implementations.
2424
The simplest way to ensure that your own methods are available to the control bus is to use the `@ManagedAttribute` or `@ManagedOperation` annotations.
2525
Since those annotations are also used for exposing methods to a JMX MBean registry, they offer a convenient by-product: Often, the same types of operations you want to expose to the control bus are reasonable for exposing through JMX).
2626
Resolution of any particular instance within the application context is achieved in the typical SpEL syntax.

src/reference/asciidoc/whats-new.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ See <<./transformer.adoc#json-transformers,JSON Transformers>> for more informat
4242
The `splitter` now supports a `discardChannel` configuration option.
4343
See <<./splitter.adoc#splitter,Splitter>> for more information.
4444

45+
The Control Bus can now handle `Pausable` (extension of `Lifecycle`) operations.
46+
See <<control-bus>> for more information.
47+
4548
[[x5.2-amqp]]
4649
==== AMQP Changes
4750

0 commit comments

Comments
 (0)