GH-8625: Add Duration support for <poller> (#8627)
* GH-8625: Add Duration support for `<poller>` Fixes https://github.com/spring-projects/spring-integration/issues/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 <grussell@vmware.com> --------- Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
* Copyright 2014-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -65,16 +65,16 @@ public @interface Poller {
|
||||
String maxMessagesPerPoll() default "";
|
||||
|
||||
/**
|
||||
* @return The fixed delay in milliseconds to create the
|
||||
* {@link org.springframework.scheduling.support.PeriodicTrigger}. Can be specified as
|
||||
* 'property placeholder', e.g. {@code ${poller.fixedDelay}}.
|
||||
* @return The fixed delay in milliseconds or a {@link java.time.Duration} compliant string
|
||||
* to create the {@link org.springframework.scheduling.support.PeriodicTrigger}.
|
||||
* Can be specified as 'property placeholder', e.g. {@code ${poller.fixedDelay}}.
|
||||
*/
|
||||
String fixedDelay() default "";
|
||||
|
||||
/**
|
||||
* @return The fixed rate in milliseconds to create the
|
||||
* {@link org.springframework.scheduling.support.PeriodicTrigger} with
|
||||
* {@code fixedRate}. Can be specified as 'property placeholder', e.g.
|
||||
* @return The fixed rate in milliseconds or a {@link java.time.Duration} compliant string
|
||||
* to create the {@link org.springframework.scheduling.support.PeriodicTrigger} with
|
||||
* the {@code fixedRate} option. Can be specified as 'property placeholder', e.g.
|
||||
* {@code ${poller.fixedRate}}.
|
||||
*/
|
||||
String fixedRate() default "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,7 +18,6 @@ package org.springframework.integration.config;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -744,14 +743,11 @@ public abstract class AbstractMethodAnnotationPostProcessor<T extends Annotation
|
||||
"The '@Poller' 'cron' attribute is mutually exclusive with other attributes.");
|
||||
trigger = new CronTrigger(cron);
|
||||
}
|
||||
else if (StringUtils.hasText(fixedDelayValue)) {
|
||||
Assert.state(!StringUtils.hasText(fixedRateValue),
|
||||
"The '@Poller' 'fixedDelay' attribute is mutually exclusive with other attributes.");
|
||||
trigger = new PeriodicTrigger(Duration.ofMillis(Long.parseLong(fixedDelayValue)));
|
||||
}
|
||||
else if (StringUtils.hasText(fixedRateValue)) {
|
||||
trigger = new PeriodicTrigger(Duration.ofMillis(Long.parseLong(fixedRateValue)));
|
||||
((PeriodicTrigger) trigger).setFixedRate(true);
|
||||
else if (StringUtils.hasText(fixedDelayValue) || StringUtils.hasText(fixedRateValue)) {
|
||||
PeriodicTriggerFactoryBean periodicTriggerFactoryBean = new PeriodicTriggerFactoryBean();
|
||||
periodicTriggerFactoryBean.setFixedDelayValue(fixedDelayValue);
|
||||
periodicTriggerFactoryBean.setFixedRateValue(fixedRateValue);
|
||||
trigger = periodicTriggerFactoryBean.getObject();
|
||||
}
|
||||
//'Trigger' can be null. 'PollingConsumer' does fallback to the 'new PeriodicTrigger(10)'.
|
||||
pollerMetadata.setTrigger(trigger);
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.scheduling.support.PeriodicTrigger;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The {@link FactoryBean} to produce a {@link PeriodicTrigger}
|
||||
* based on parsing string values for its options.
|
||||
* This class is mostly driven by the XML configuration requirements for
|
||||
* {@link Duration} value representations for the respective attributes.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 6.2
|
||||
*/
|
||||
public class PeriodicTriggerFactoryBean implements FactoryBean<PeriodicTrigger> {
|
||||
|
||||
@Nullable
|
||||
private String fixedDelayValue;
|
||||
|
||||
@Nullable
|
||||
private String fixedRateValue;
|
||||
|
||||
@Nullable
|
||||
private String initialDelayValue;
|
||||
|
||||
@Nullable
|
||||
private TimeUnit timeUnit;
|
||||
|
||||
public void setFixedDelayValue(String fixedDelayValue) {
|
||||
this.fixedDelayValue = fixedDelayValue;
|
||||
}
|
||||
|
||||
public void setFixedRateValue(String fixedRateValue) {
|
||||
this.fixedRateValue = fixedRateValue;
|
||||
}
|
||||
|
||||
public void setInitialDelayValue(String initialDelayValue) {
|
||||
this.initialDelayValue = initialDelayValue;
|
||||
}
|
||||
|
||||
public void setTimeUnit(TimeUnit timeUnit) {
|
||||
this.timeUnit = timeUnit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeriodicTrigger getObject() {
|
||||
boolean hasFixedDelay = StringUtils.hasText(this.fixedDelayValue);
|
||||
boolean hasFixedRate = StringUtils.hasText(this.fixedRateValue);
|
||||
|
||||
Assert.isTrue(hasFixedDelay ^ hasFixedRate,
|
||||
"One of the 'fixedDelayValue' or 'fixedRateValue' property must be provided but not both.");
|
||||
|
||||
TimeUnit timeUnitToUse = this.timeUnit;
|
||||
if (timeUnitToUse == null) {
|
||||
timeUnitToUse = TimeUnit.MILLISECONDS;
|
||||
}
|
||||
|
||||
Duration duration = toDuration(hasFixedDelay ? this.fixedDelayValue : this.fixedRateValue, timeUnitToUse);
|
||||
|
||||
PeriodicTrigger periodicTrigger = new PeriodicTrigger(duration);
|
||||
periodicTrigger.setFixedRate(hasFixedRate);
|
||||
if (StringUtils.hasText(this.initialDelayValue)) {
|
||||
periodicTrigger.setInitialDelay(toDuration(this.initialDelayValue, timeUnitToUse));
|
||||
}
|
||||
return periodicTrigger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return PeriodicTrigger.class;
|
||||
}
|
||||
|
||||
private static Duration toDuration(String value, TimeUnit timeUnit) {
|
||||
if (isDurationString(value)) {
|
||||
return Duration.parse(value);
|
||||
}
|
||||
return toDuration(Long.parseLong(value), timeUnit);
|
||||
}
|
||||
|
||||
private static boolean isDurationString(String value) {
|
||||
return (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1))));
|
||||
}
|
||||
|
||||
private static boolean isP(char ch) {
|
||||
return (ch == 'P' || ch == 'p');
|
||||
}
|
||||
|
||||
private static Duration toDuration(long value, TimeUnit timeUnit) {
|
||||
return Duration.of(value, timeUnit.toChronoUnit());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,9 +28,9 @@ import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.integration.channel.MessagePublishingErrorHandler;
|
||||
import org.springframework.integration.config.PeriodicTriggerFactoryBean;
|
||||
import org.springframework.integration.scheduling.PollerMetadata;
|
||||
import org.springframework.scheduling.support.CronTrigger;
|
||||
import org.springframework.scheduling.support.PeriodicTrigger;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.xml.DomUtils;
|
||||
|
||||
@@ -45,12 +45,15 @@ import org.springframework.util.xml.DomUtils;
|
||||
*/
|
||||
public class PollerParser extends AbstractBeanDefinitionParser {
|
||||
|
||||
private static final String MULTIPLE_TRIGGER_DEFINITIONS = "A <poller> cannot specify more than one trigger configuration.";
|
||||
private static final String MULTIPLE_TRIGGER_DEFINITIONS =
|
||||
"A <poller> cannot specify more than one trigger configuration.";
|
||||
|
||||
private static final String NO_TRIGGER_DEFINITIONS = "A <poller> must have one and only one trigger configuration.";
|
||||
|
||||
@Override
|
||||
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) throws BeanDefinitionStoreException {
|
||||
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
|
||||
throws BeanDefinitionStoreException {
|
||||
|
||||
String id = super.resolveId(element, definition, parserContext);
|
||||
if (element.getAttribute("default").equals("true")) {
|
||||
if (parserContext.getRegistry().isBeanNameInUse(PollerMetadata.DEFAULT_POLLER_METADATA_BEAN_NAME)) {
|
||||
@@ -102,29 +105,30 @@ public class PollerParser extends AbstractBeanDefinitionParser {
|
||||
|
||||
String errorChannel = element.getAttribute("error-channel");
|
||||
if (StringUtils.hasText(errorChannel)) {
|
||||
BeanDefinitionBuilder errorHandler = BeanDefinitionBuilder.genericBeanDefinition(MessagePublishingErrorHandler.class);
|
||||
BeanDefinitionBuilder errorHandler =
|
||||
BeanDefinitionBuilder.genericBeanDefinition(MessagePublishingErrorHandler.class);
|
||||
errorHandler.addPropertyReference("defaultErrorChannel", errorChannel);
|
||||
metadataBuilder.addPropertyValue("errorHandler", errorHandler.getBeanDefinition());
|
||||
}
|
||||
return metadataBuilder.getBeanDefinition();
|
||||
}
|
||||
|
||||
private void configureTrigger(Element pollerElement, BeanDefinitionBuilder targetBuilder, ParserContext parserContext) {
|
||||
private void configureTrigger(Element pollerElement, BeanDefinitionBuilder targetBuilder,
|
||||
ParserContext parserContext) {
|
||||
|
||||
String triggerAttribute = pollerElement.getAttribute("trigger");
|
||||
String fixedRateAttribute = pollerElement.getAttribute("fixed-rate");
|
||||
String fixedDelayAttribute = pollerElement.getAttribute("fixed-delay");
|
||||
String cronAttribute = pollerElement.getAttribute("cron");
|
||||
String timeUnit = pollerElement.getAttribute("time-unit");
|
||||
|
||||
List<String> triggerBeanNames = new ArrayList<String>();
|
||||
List<String> triggerBeanNames = new ArrayList<>();
|
||||
if (StringUtils.hasText(triggerAttribute)) {
|
||||
trigger(pollerElement, parserContext, triggerAttribute, timeUnit, triggerBeanNames);
|
||||
}
|
||||
if (StringUtils.hasText(fixedRateAttribute)) {
|
||||
fixedRate(parserContext, fixedRateAttribute, timeUnit, triggerBeanNames);
|
||||
}
|
||||
if (StringUtils.hasText(fixedDelayAttribute)) {
|
||||
fixedDelay(parserContext, fixedDelayAttribute, timeUnit, triggerBeanNames);
|
||||
if (StringUtils.hasText(fixedRateAttribute) || StringUtils.hasText(fixedDelayAttribute)) {
|
||||
period(parserContext, fixedDelayAttribute, fixedRateAttribute, pollerElement.getAttribute("initial-delay"),
|
||||
timeUnit, triggerBeanNames);
|
||||
}
|
||||
if (StringUtils.hasText(cronAttribute)) {
|
||||
cron(pollerElement, parserContext, cronAttribute, timeUnit, triggerBeanNames);
|
||||
@@ -142,33 +146,20 @@ public class PollerParser extends AbstractBeanDefinitionParser {
|
||||
List<String> triggerBeanNames) {
|
||||
|
||||
if (StringUtils.hasText(timeUnit)) {
|
||||
parserContext.getReaderContext().error("The 'time-unit' attribute cannot be used with a 'trigger' reference.", pollerElement);
|
||||
parserContext.getReaderContext()
|
||||
.error("The 'time-unit' attribute cannot be used with a 'trigger' reference.", pollerElement);
|
||||
}
|
||||
triggerBeanNames.add(triggerAttribute);
|
||||
}
|
||||
|
||||
private void fixedRate(ParserContext parserContext, String fixedRateAttribute, String timeUnit,
|
||||
List<String> triggerBeanNames) {
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTrigger.class);
|
||||
builder.addConstructorArgValue(fixedRateAttribute);
|
||||
if (StringUtils.hasText(timeUnit)) {
|
||||
builder.addConstructorArgValue(timeUnit);
|
||||
}
|
||||
builder.addPropertyValue("fixedRate", Boolean.TRUE);
|
||||
String triggerBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
|
||||
builder.getBeanDefinition(), parserContext.getRegistry());
|
||||
triggerBeanNames.add(triggerBeanName);
|
||||
}
|
||||
private void period(ParserContext parserContext, String fixedDelayAttribute, String fixedRateAttribute,
|
||||
String initialDelayAttribute, String timeUnit, List<String> triggerBeanNames) {
|
||||
|
||||
private void fixedDelay(ParserContext parserContext, String fixedDelayAttribute, String timeUnit,
|
||||
List<String> triggerBeanNames) {
|
||||
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTrigger.class);
|
||||
builder.addConstructorArgValue(fixedDelayAttribute);
|
||||
if (StringUtils.hasText(timeUnit)) {
|
||||
builder.addConstructorArgValue(timeUnit);
|
||||
}
|
||||
builder.addPropertyValue("fixedRate", Boolean.FALSE);
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTriggerFactoryBean.class);
|
||||
builder.addPropertyValue("fixedDelayValue", fixedDelayAttribute);
|
||||
builder.addPropertyValue("fixedRateValue", fixedRateAttribute);
|
||||
builder.addPropertyValue("timeUnit", timeUnit);
|
||||
builder.addPropertyValue("initialDelayValue", initialDelayAttribute);
|
||||
String triggerBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
|
||||
builder.getBeanDefinition(), parserContext.getRegistry());
|
||||
triggerBeanNames.add(triggerBeanName);
|
||||
@@ -187,4 +178,5 @@ public class PollerParser extends AbstractBeanDefinitionParser {
|
||||
builder.getBeanDefinition(), parserContext.getRegistry());
|
||||
triggerBeanNames.add(triggerBeanName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1983,7 +1983,7 @@
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="fixed-delay" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>Fixed delay trigger (in milliseconds).</xsd:documentation>
|
||||
<xsd:documentation>Fixed delay trigger (decimal for time unit or Duration string).</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="ref" type="xsd:string" use="optional">
|
||||
@@ -1997,7 +1997,14 @@
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="fixed-rate" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>Fixed rate trigger (in milliseconds).</xsd:documentation>
|
||||
<xsd:documentation>Fixed rate trigger (decimal for time unit or Duration string).</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="initial-delay" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Periodic trigger initial delay (decimal for time unit or Duration string).
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="time-unit">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package org.springframework.integration.config.xml;
|
||||
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.aopalliance.aop.Advice;
|
||||
@@ -112,7 +112,9 @@ public class PollerParserTests {
|
||||
PollerMetadata metadata = (PollerMetadata) poller;
|
||||
assertThat(metadata.getReceiveTimeout()).isEqualTo(1234);
|
||||
PeriodicTrigger trigger = (PeriodicTrigger) metadata.getTrigger();
|
||||
assertThat(TestUtils.getPropertyValue(trigger, "chronoUnit")).isEqualTo(ChronoUnit.SECONDS);
|
||||
assertThat(trigger.getPeriodDuration()).isEqualTo(Duration.ofSeconds(5));
|
||||
assertThat(trigger.isFixedRate()).isTrue();
|
||||
assertThat(trigger.getInitialDelayDuration()).isEqualTo(Duration.ofSeconds(45));
|
||||
context.close();
|
||||
}
|
||||
|
||||
@@ -123,7 +125,7 @@ public class PollerParserTests {
|
||||
Object poller = context.getBean("poller");
|
||||
assertThat(poller).isNotNull();
|
||||
PollerMetadata metadata = (PollerMetadata) poller;
|
||||
assertThat(metadata.getTrigger() instanceof TestTrigger).isTrue();
|
||||
assertThat(metadata.getTrigger()).isInstanceOf(TestTrigger.class);
|
||||
context.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
<beans:prop key="seconds">SECONDS</beans:prop>
|
||||
</util:properties>
|
||||
|
||||
<poller id="poller" receive-timeout="1234" fixed-rate="5" time-unit="${seconds}"/>
|
||||
<poller id="poller" receive-timeout="1234" fixed-rate="5" time-unit="${seconds}" initial-delay="PT45S"/>
|
||||
|
||||
</beans:beans>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
message.history.tracked.components=input, publishedChannel, annotationTestService*
|
||||
poller.maxMessagesPerPoll=10
|
||||
poller.interval=100
|
||||
poller.interval=PT0.1S
|
||||
poller.receiveTimeout=10000
|
||||
global.wireTap.pattern=input
|
||||
|
||||
Reference in New Issue
Block a user