Skip to content

Commit c70d7a6

Browse files
GH-8625: Add Duration support for <poller> (#8627)
* GH-8625: Add Duration support for `<poller>` Fixes #8625 The duration can be represented in a ISO 8601 format, e.g. `PT10S`, `P1D` etc. The `<poller>` and `@Poller` don't support such a format. * Introduce a `PeriodicTriggerFactoryBean` to accept string values for trigger options and parse them manually before creating the target `PeriodicTrigger` * Use this `PeriodicTriggerFactoryBean` in the `PollerParser` and `AbstractMethodAnnotationPostProcessor` where we parse options for the `PeriodicTrigger` * Modify tests to ensure that feature works * Document the duration option * Add more cross-links into polling docs * Fix typos in the affected doc files * Add `-parameters` for compiler options since SF 6.1 does not support `-debug` anymore for method parameter names discovery * Fix typos Co-authored-by: Gary Russell <[email protected]> --------- Co-authored-by: Gary Russell <[email protected]>
1 parent ba417de commit c70d7a6

File tree

13 files changed

+210
-84
lines changed

13 files changed

+210
-84
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ configure(javaProjects) { subproject ->
212212

213213
compileJava {
214214
options.release = 17
215+
options.compilerArgs << '-parameters'
215216
}
216217

217218
compileTestJava {

spring-integration-core/src/main/java/org/springframework/integration/annotation/Poller.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -65,16 +65,16 @@
6565
String maxMessagesPerPoll() default "";
6666

6767
/**
68-
* @return The fixed delay in milliseconds to create the
69-
* {@link org.springframework.scheduling.support.PeriodicTrigger}. Can be specified as
70-
* 'property placeholder', e.g. {@code ${poller.fixedDelay}}.
68+
* @return The fixed delay in milliseconds or a {@link java.time.Duration} compliant string
69+
* to create the {@link org.springframework.scheduling.support.PeriodicTrigger}.
70+
* Can be specified as 'property placeholder', e.g. {@code ${poller.fixedDelay}}.
7171
*/
7272
String fixedDelay() default "";
7373

7474
/**
75-
* @return The fixed rate in milliseconds to create the
76-
* {@link org.springframework.scheduling.support.PeriodicTrigger} with
77-
* {@code fixedRate}. Can be specified as 'property placeholder', e.g.
75+
* @return The fixed rate in milliseconds or a {@link java.time.Duration} compliant string
76+
* to create the {@link org.springframework.scheduling.support.PeriodicTrigger} with
77+
* the {@code fixedRate} option. Can be specified as 'property placeholder', e.g.
7878
* {@code ${poller.fixedRate}}.
7979
*/
8080
String fixedRate() default "";

spring-integration-core/src/main/java/org/springframework/integration/config/AbstractMethodAnnotationPostProcessor.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.Method;
21-
import java.time.Duration;
2221
import java.util.ArrayList;
2322
import java.util.Arrays;
2423
import java.util.Collection;
@@ -744,14 +743,11 @@ else if (StringUtils.hasText(cron)) {
744743
"The '@Poller' 'cron' attribute is mutually exclusive with other attributes.");
745744
trigger = new CronTrigger(cron);
746745
}
747-
else if (StringUtils.hasText(fixedDelayValue)) {
748-
Assert.state(!StringUtils.hasText(fixedRateValue),
749-
"The '@Poller' 'fixedDelay' attribute is mutually exclusive with other attributes.");
750-
trigger = new PeriodicTrigger(Duration.ofMillis(Long.parseLong(fixedDelayValue)));
751-
}
752-
else if (StringUtils.hasText(fixedRateValue)) {
753-
trigger = new PeriodicTrigger(Duration.ofMillis(Long.parseLong(fixedRateValue)));
754-
((PeriodicTrigger) trigger).setFixedRate(true);
746+
else if (StringUtils.hasText(fixedDelayValue) || StringUtils.hasText(fixedRateValue)) {
747+
PeriodicTriggerFactoryBean periodicTriggerFactoryBean = new PeriodicTriggerFactoryBean();
748+
periodicTriggerFactoryBean.setFixedDelayValue(fixedDelayValue);
749+
periodicTriggerFactoryBean.setFixedRateValue(fixedRateValue);
750+
trigger = periodicTriggerFactoryBean.getObject();
755751
}
756752
//'Trigger' can be null. 'PollingConsumer' does fallback to the 'new PeriodicTrigger(10)'.
757753
pollerMetadata.setTrigger(trigger);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* 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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.config;
18+
19+
import java.time.Duration;
20+
import java.util.concurrent.TimeUnit;
21+
22+
import org.springframework.beans.factory.FactoryBean;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.scheduling.support.PeriodicTrigger;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* The {@link FactoryBean} to produce a {@link PeriodicTrigger}
30+
* based on parsing string values for its options.
31+
* This class is mostly driven by the XML configuration requirements for
32+
* {@link Duration} value representations for the respective attributes.
33+
*
34+
* @author Artem Bilan
35+
*
36+
* @since 6.2
37+
*/
38+
public class PeriodicTriggerFactoryBean implements FactoryBean<PeriodicTrigger> {
39+
40+
@Nullable
41+
private String fixedDelayValue;
42+
43+
@Nullable
44+
private String fixedRateValue;
45+
46+
@Nullable
47+
private String initialDelayValue;
48+
49+
@Nullable
50+
private TimeUnit timeUnit;
51+
52+
public void setFixedDelayValue(String fixedDelayValue) {
53+
this.fixedDelayValue = fixedDelayValue;
54+
}
55+
56+
public void setFixedRateValue(String fixedRateValue) {
57+
this.fixedRateValue = fixedRateValue;
58+
}
59+
60+
public void setInitialDelayValue(String initialDelayValue) {
61+
this.initialDelayValue = initialDelayValue;
62+
}
63+
64+
public void setTimeUnit(TimeUnit timeUnit) {
65+
this.timeUnit = timeUnit;
66+
}
67+
68+
@Override
69+
public PeriodicTrigger getObject() {
70+
boolean hasFixedDelay = StringUtils.hasText(this.fixedDelayValue);
71+
boolean hasFixedRate = StringUtils.hasText(this.fixedRateValue);
72+
73+
Assert.isTrue(hasFixedDelay ^ hasFixedRate,
74+
"One of the 'fixedDelayValue' or 'fixedRateValue' property must be provided but not both.");
75+
76+
TimeUnit timeUnitToUse = this.timeUnit;
77+
if (timeUnitToUse == null) {
78+
timeUnitToUse = TimeUnit.MILLISECONDS;
79+
}
80+
81+
Duration duration = toDuration(hasFixedDelay ? this.fixedDelayValue : this.fixedRateValue, timeUnitToUse);
82+
83+
PeriodicTrigger periodicTrigger = new PeriodicTrigger(duration);
84+
periodicTrigger.setFixedRate(hasFixedRate);
85+
if (StringUtils.hasText(this.initialDelayValue)) {
86+
periodicTrigger.setInitialDelay(toDuration(this.initialDelayValue, timeUnitToUse));
87+
}
88+
return periodicTrigger;
89+
}
90+
91+
@Override
92+
public Class<?> getObjectType() {
93+
return PeriodicTrigger.class;
94+
}
95+
96+
private static Duration toDuration(String value, TimeUnit timeUnit) {
97+
if (isDurationString(value)) {
98+
return Duration.parse(value);
99+
}
100+
return toDuration(Long.parseLong(value), timeUnit);
101+
}
102+
103+
private static boolean isDurationString(String value) {
104+
return (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1))));
105+
}
106+
107+
private static boolean isP(char ch) {
108+
return (ch == 'P' || ch == 'p');
109+
}
110+
111+
private static Duration toDuration(long value, TimeUnit timeUnit) {
112+
return Duration.of(value, timeUnit.toChronoUnit());
113+
}
114+
115+
}

spring-integration-core/src/main/java/org/springframework/integration/config/xml/PollerParser.java

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,9 +28,9 @@
2828
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
2929
import org.springframework.beans.factory.xml.ParserContext;
3030
import org.springframework.integration.channel.MessagePublishingErrorHandler;
31+
import org.springframework.integration.config.PeriodicTriggerFactoryBean;
3132
import org.springframework.integration.scheduling.PollerMetadata;
3233
import org.springframework.scheduling.support.CronTrigger;
33-
import org.springframework.scheduling.support.PeriodicTrigger;
3434
import org.springframework.util.StringUtils;
3535
import org.springframework.util.xml.DomUtils;
3636

@@ -45,12 +45,15 @@
4545
*/
4646
public class PollerParser extends AbstractBeanDefinitionParser {
4747

48-
private static final String MULTIPLE_TRIGGER_DEFINITIONS = "A <poller> cannot specify more than one trigger configuration.";
48+
private static final String MULTIPLE_TRIGGER_DEFINITIONS =
49+
"A <poller> cannot specify more than one trigger configuration.";
4950

5051
private static final String NO_TRIGGER_DEFINITIONS = "A <poller> must have one and only one trigger configuration.";
5152

5253
@Override
53-
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) throws BeanDefinitionStoreException {
54+
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
55+
throws BeanDefinitionStoreException {
56+
5457
String id = super.resolveId(element, definition, parserContext);
5558
if (element.getAttribute("default").equals("true")) {
5659
if (parserContext.getRegistry().isBeanNameInUse(PollerMetadata.DEFAULT_POLLER_METADATA_BEAN_NAME)) {
@@ -102,29 +105,30 @@ else if (adviceChainElement != null) {
102105

103106
String errorChannel = element.getAttribute("error-channel");
104107
if (StringUtils.hasText(errorChannel)) {
105-
BeanDefinitionBuilder errorHandler = BeanDefinitionBuilder.genericBeanDefinition(MessagePublishingErrorHandler.class);
108+
BeanDefinitionBuilder errorHandler =
109+
BeanDefinitionBuilder.genericBeanDefinition(MessagePublishingErrorHandler.class);
106110
errorHandler.addPropertyReference("defaultErrorChannel", errorChannel);
107111
metadataBuilder.addPropertyValue("errorHandler", errorHandler.getBeanDefinition());
108112
}
109113
return metadataBuilder.getBeanDefinition();
110114
}
111115

112-
private void configureTrigger(Element pollerElement, BeanDefinitionBuilder targetBuilder, ParserContext parserContext) {
116+
private void configureTrigger(Element pollerElement, BeanDefinitionBuilder targetBuilder,
117+
ParserContext parserContext) {
118+
113119
String triggerAttribute = pollerElement.getAttribute("trigger");
114120
String fixedRateAttribute = pollerElement.getAttribute("fixed-rate");
115121
String fixedDelayAttribute = pollerElement.getAttribute("fixed-delay");
116122
String cronAttribute = pollerElement.getAttribute("cron");
117123
String timeUnit = pollerElement.getAttribute("time-unit");
118124

119-
List<String> triggerBeanNames = new ArrayList<String>();
125+
List<String> triggerBeanNames = new ArrayList<>();
120126
if (StringUtils.hasText(triggerAttribute)) {
121127
trigger(pollerElement, parserContext, triggerAttribute, timeUnit, triggerBeanNames);
122128
}
123-
if (StringUtils.hasText(fixedRateAttribute)) {
124-
fixedRate(parserContext, fixedRateAttribute, timeUnit, triggerBeanNames);
125-
}
126-
if (StringUtils.hasText(fixedDelayAttribute)) {
127-
fixedDelay(parserContext, fixedDelayAttribute, timeUnit, triggerBeanNames);
129+
if (StringUtils.hasText(fixedRateAttribute) || StringUtils.hasText(fixedDelayAttribute)) {
130+
period(parserContext, fixedDelayAttribute, fixedRateAttribute, pollerElement.getAttribute("initial-delay"),
131+
timeUnit, triggerBeanNames);
128132
}
129133
if (StringUtils.hasText(cronAttribute)) {
130134
cron(pollerElement, parserContext, cronAttribute, timeUnit, triggerBeanNames);
@@ -142,33 +146,20 @@ private void trigger(Element pollerElement, ParserContext parserContext, String
142146
List<String> triggerBeanNames) {
143147

144148
if (StringUtils.hasText(timeUnit)) {
145-
parserContext.getReaderContext().error("The 'time-unit' attribute cannot be used with a 'trigger' reference.", pollerElement);
149+
parserContext.getReaderContext()
150+
.error("The 'time-unit' attribute cannot be used with a 'trigger' reference.", pollerElement);
146151
}
147152
triggerBeanNames.add(triggerAttribute);
148153
}
149154

150-
private void fixedRate(ParserContext parserContext, String fixedRateAttribute, String timeUnit,
151-
List<String> triggerBeanNames) {
152-
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTrigger.class);
153-
builder.addConstructorArgValue(fixedRateAttribute);
154-
if (StringUtils.hasText(timeUnit)) {
155-
builder.addConstructorArgValue(timeUnit);
156-
}
157-
builder.addPropertyValue("fixedRate", Boolean.TRUE);
158-
String triggerBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
159-
builder.getBeanDefinition(), parserContext.getRegistry());
160-
triggerBeanNames.add(triggerBeanName);
161-
}
162-
163-
private void fixedDelay(ParserContext parserContext, String fixedDelayAttribute, String timeUnit,
164-
List<String> triggerBeanNames) {
155+
private void period(ParserContext parserContext, String fixedDelayAttribute, String fixedRateAttribute,
156+
String initialDelayAttribute, String timeUnit, List<String> triggerBeanNames) {
165157

166-
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTrigger.class);
167-
builder.addConstructorArgValue(fixedDelayAttribute);
168-
if (StringUtils.hasText(timeUnit)) {
169-
builder.addConstructorArgValue(timeUnit);
170-
}
171-
builder.addPropertyValue("fixedRate", Boolean.FALSE);
158+
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTriggerFactoryBean.class);
159+
builder.addPropertyValue("fixedDelayValue", fixedDelayAttribute);
160+
builder.addPropertyValue("fixedRateValue", fixedRateAttribute);
161+
builder.addPropertyValue("timeUnit", timeUnit);
162+
builder.addPropertyValue("initialDelayValue", initialDelayAttribute);
172163
String triggerBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
173164
builder.getBeanDefinition(), parserContext.getRegistry());
174165
triggerBeanNames.add(triggerBeanName);
@@ -187,4 +178,5 @@ private void cron(Element pollerElement, ParserContext parserContext, String cro
187178
builder.getBeanDefinition(), parserContext.getRegistry());
188179
triggerBeanNames.add(triggerBeanName);
189180
}
181+
190182
}

spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,7 +1983,7 @@
19831983
</xsd:sequence>
19841984
<xsd:attribute name="fixed-delay" type="xsd:string">
19851985
<xsd:annotation>
1986-
<xsd:documentation>Fixed delay trigger (in milliseconds).</xsd:documentation>
1986+
<xsd:documentation>Fixed delay trigger (decimal for time unit or Duration string).</xsd:documentation>
19871987
</xsd:annotation>
19881988
</xsd:attribute>
19891989
<xsd:attribute name="ref" type="xsd:string" use="optional">
@@ -1997,7 +1997,14 @@
19971997
</xsd:attribute>
19981998
<xsd:attribute name="fixed-rate" type="xsd:string">
19991999
<xsd:annotation>
2000-
<xsd:documentation>Fixed rate trigger (in milliseconds).</xsd:documentation>
2000+
<xsd:documentation>Fixed rate trigger (decimal for time unit or Duration string).</xsd:documentation>
2001+
</xsd:annotation>
2002+
</xsd:attribute>
2003+
<xsd:attribute name="initial-delay" type="xsd:string">
2004+
<xsd:annotation>
2005+
<xsd:documentation>
2006+
Periodic trigger initial delay (decimal for time unit or Duration string).
2007+
</xsd:documentation>
20012008
</xsd:annotation>
20022009
</xsd:attribute>
20032010
<xsd:attribute name="time-unit">

spring-integration-core/src/test/java/org/springframework/integration/config/xml/PollerParserTests.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
1616

1717
package org.springframework.integration.config.xml;
1818

19-
import java.time.temporal.ChronoUnit;
19+
import java.time.Duration;
2020
import java.util.HashMap;
2121

2222
import org.aopalliance.aop.Advice;
@@ -112,7 +112,9 @@ public void pollerWithReceiveTimeoutAndTimeunit() {
112112
PollerMetadata metadata = (PollerMetadata) poller;
113113
assertThat(metadata.getReceiveTimeout()).isEqualTo(1234);
114114
PeriodicTrigger trigger = (PeriodicTrigger) metadata.getTrigger();
115-
assertThat(TestUtils.getPropertyValue(trigger, "chronoUnit")).isEqualTo(ChronoUnit.SECONDS);
115+
assertThat(trigger.getPeriodDuration()).isEqualTo(Duration.ofSeconds(5));
116+
assertThat(trigger.isFixedRate()).isTrue();
117+
assertThat(trigger.getInitialDelayDuration()).isEqualTo(Duration.ofSeconds(45));
116118
context.close();
117119
}
118120

@@ -123,7 +125,7 @@ public void pollerWithTriggerReference() {
123125
Object poller = context.getBean("poller");
124126
assertThat(poller).isNotNull();
125127
PollerMetadata metadata = (PollerMetadata) poller;
126-
assertThat(metadata.getTrigger() instanceof TestTrigger).isTrue();
128+
assertThat(metadata.getTrigger()).isInstanceOf(TestTrigger.class);
127129
context.close();
128130
}
129131

spring-integration-core/src/test/java/org/springframework/integration/config/xml/pollerWithReceiveTimeout.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
<beans:prop key="seconds">SECONDS</beans:prop>
1616
</util:properties>
1717

18-
<poller id="poller" receive-timeout="1234" fixed-rate="5" time-unit="${seconds}"/>
18+
<poller id="poller" receive-timeout="1234" fixed-rate="5" time-unit="${seconds}" initial-delay="PT45S"/>
1919

2020
</beans:beans>
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
message.history.tracked.components=input, publishedChannel, annotationTestService*
22
poller.maxMessagesPerPoll=10
3-
poller.interval=100
3+
poller.interval=PT0.1S
44
poller.receiveTimeout=10000
55
global.wireTap.pattern=input

0 commit comments

Comments
 (0)