INT-2285 Implement max-subscribers on Channels
Add a subscriber limit on both unicast and publish-subscribe
channels. This permits detection of inadvertent channel wiring
during context initialization.
Also add a mechanism to globally set these defaults.
Two properties are added to ChannelInitializer. If the
'channelInitializer' bean is declared before a channel, and
these properties are set, it will globally override the
default (Integer.MAX_VALUE) for these properties.
For example:
<bean id="channelInitializer" class="org.springframework.integration.config.xml.ChannelInitializer">
<property name="autoCreate" value="true" />
<property name="defaultMaxUnicastSubscribers" value="1" />
<property name="defaultMaxMulticastSubscribers" value="2" />
</bean>
will make the default max-subscribers 1 and 2 for <channel/> and
<publish-subscribe/> channels respectively.
Also applies to module channels (jms, amqp, redis).
This commit is contained in:
committed by
Gunnar Hillert
parent
a90a7e8e0b
commit
2ecb14de2a
@@ -18,7 +18,6 @@ package org.springframework.integration.amqp.channel;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.amqp.core.AmqpAdmin;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.amqp.core.MessageListener;
|
||||
@@ -36,6 +35,7 @@ import org.springframework.integration.MessageDispatchingException;
|
||||
import org.springframework.integration.MessagingException;
|
||||
import org.springframework.integration.core.MessageHandler;
|
||||
import org.springframework.integration.core.SubscribableChannel;
|
||||
import org.springframework.integration.dispatcher.AbstractDispatcher;
|
||||
import org.springframework.integration.dispatcher.MessageDispatcher;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -56,6 +56,8 @@ abstract class AbstractSubscribableAmqpChannel extends AbstractAmqpChannel imple
|
||||
|
||||
private final boolean isPubSub;
|
||||
|
||||
private volatile int maxSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
public AbstractSubscribableAmqpChannel(String channelName, SimpleMessageListenerContainer container, AmqpTemplate amqpTemplate) {
|
||||
this(channelName, container, amqpTemplate, false);
|
||||
}
|
||||
@@ -71,6 +73,15 @@ abstract class AbstractSubscribableAmqpChannel extends AbstractAmqpChannel imple
|
||||
this.isPubSub = isPubSub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of subscribers supported by the
|
||||
* channel's dispatcher (if it is an {@link AbstractDispatcher}).
|
||||
* @param maxSubscribers
|
||||
*/
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.maxSubscribers = maxSubscribers;
|
||||
}
|
||||
|
||||
public boolean subscribe(MessageHandler handler) {
|
||||
return this.dispatcher.addHandler(handler);
|
||||
}
|
||||
@@ -83,6 +94,9 @@ abstract class AbstractSubscribableAmqpChannel extends AbstractAmqpChannel imple
|
||||
public void onInit() throws Exception {
|
||||
super.onInit();
|
||||
this.dispatcher = this.createDispatcher();
|
||||
if (this.dispatcher instanceof AbstractDispatcher) {
|
||||
((AbstractDispatcher) this.dispatcher).setMaxSubscribers(this.maxSubscribers);
|
||||
}
|
||||
AmqpAdmin admin = new RabbitAdmin(this.container.getConnectionFactory());
|
||||
Queue queue = this.initializeQueue(admin, this.channelName);
|
||||
this.container.setQueues(queue);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -20,7 +20,6 @@ import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.aopalliance.aop.Advice;
|
||||
|
||||
import org.springframework.amqp.core.AcknowledgeMode;
|
||||
import org.springframework.amqp.core.AmqpAdmin;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
@@ -56,8 +55,9 @@ import org.springframework.util.StringUtils;
|
||||
* a FanoutExchange named "si.fanout.[beanName]" and we send to that without any
|
||||
* routing key, and on the receiving side, we create an anonymous Queue that is
|
||||
* bound to that exchange.
|
||||
*
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
*/
|
||||
public class AmqpChannelFactoryBean extends AbstractFactoryBean<AbstractAmqpChannel> implements SmartLifecycle, DisposableBean, BeanNameAware {
|
||||
@@ -121,6 +121,8 @@ public class AmqpChannelFactoryBean extends AbstractFactoryBean<AbstractAmqpChan
|
||||
|
||||
private volatile Integer txSize;
|
||||
|
||||
private volatile int maxSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
public AmqpChannelFactoryBean() {
|
||||
this(true);
|
||||
@@ -283,6 +285,10 @@ public class AmqpChannelFactoryBean extends AbstractFactoryBean<AbstractAmqpChan
|
||||
this.txSize = txSize;
|
||||
}
|
||||
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.maxSubscribers = maxSubscribers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return (this.channel != null) ? this.channel.getClass() : AbstractAmqpChannel.class;
|
||||
@@ -301,6 +307,7 @@ public class AmqpChannelFactoryBean extends AbstractFactoryBean<AbstractAmqpChan
|
||||
if (this.exchange != null) {
|
||||
pubsub.setExchange(this.exchange);
|
||||
}
|
||||
pubsub.setMaxSubscribers(this.maxSubscribers);
|
||||
this.channel = pubsub;
|
||||
}
|
||||
else {
|
||||
@@ -309,6 +316,7 @@ public class AmqpChannelFactoryBean extends AbstractFactoryBean<AbstractAmqpChan
|
||||
if (StringUtils.hasText(this.queueName)) {
|
||||
p2p.setQueueName(this.queueName);
|
||||
}
|
||||
p2p.setMaxSubscribers(this.maxSubscribers);
|
||||
this.channel = p2p;
|
||||
}
|
||||
}
|
||||
@@ -423,6 +431,7 @@ public class AmqpChannelFactoryBean extends AbstractFactoryBean<AbstractAmqpChan
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void destroyInstance(AbstractAmqpChannel instance) throws Exception {
|
||||
if (instance instanceof DisposableBean) {
|
||||
((DisposableBean) this.channel).destroy();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-2012 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,19 +16,19 @@
|
||||
|
||||
package org.springframework.integration.amqp.config;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.integration.config.xml.AbstractChannelParser;
|
||||
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
* Parser for the 'channel' and 'publish-subscribe-channel' elements of the
|
||||
* Spring Integration AMQP namespace.
|
||||
*
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
*/
|
||||
public class AmqpChannelParser extends AbstractChannelParser {
|
||||
@@ -47,10 +47,13 @@ public class AmqpChannelParser extends AbstractChannelParser {
|
||||
builder.addPropertyReference("connectionFactory", connectionFactory);
|
||||
if ("channel".equals(element.getLocalName())) {
|
||||
builder.addPropertyValue("pubSub", false);
|
||||
this.setMaxSubscribersProperty(parserContext, builder, element, IntegrationNamespaceUtils.DEFAULT_MAX_UNICAST_SUBSCRIBERS_PROPERTY_NAME);
|
||||
}
|
||||
else if ("publish-subscribe-channel".equals(element.getLocalName())) {
|
||||
builder.addPropertyValue("pubSub", true);
|
||||
this.setMaxSubscribersProperty(parserContext, builder, element, IntegrationNamespaceUtils.DEFAULT_MAX_BROADCAST_SUBSCRIBERS_PROPERTY_NAME);
|
||||
}
|
||||
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "acknowledge-mode");
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "advice-chain");
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "amqp-admin");
|
||||
|
||||
@@ -336,6 +336,7 @@
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attributeGroup ref="containerAndTemplateAttributes"/>
|
||||
<xsd:attributeGroup ref="integration:subscribersAttributeGroup" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="outboundType">
|
||||
|
||||
@@ -17,4 +17,6 @@
|
||||
|
||||
<bean id="rabbitConnectionFactory" class="org.springframework.integration.amqp.StubRabbitConnectionFactory"/>
|
||||
|
||||
<amqp:channel id="channelWithSubscriberLimit" max-subscribers="1" />
|
||||
|
||||
</beans>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -22,7 +22,6 @@ import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.integration.MessageChannel;
|
||||
@@ -33,6 +32,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@@ -48,6 +48,15 @@ public class AmqpChannelParserTests {
|
||||
List<?> interceptorList = TestUtils.getPropertyValue(channel, "interceptors.interceptors", List.class);
|
||||
assertEquals(1, interceptorList.size());
|
||||
assertEquals(TestInterceptor.class, interceptorList.get(0).getClass());
|
||||
assertEquals(Integer.MAX_VALUE, TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(channel, "dispatcher"), "maxSubscribers", Integer.class).intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscriberLimit() {
|
||||
MessageChannel channel = context.getBean("channelWithSubscriberLimit", MessageChannel.class);
|
||||
assertEquals(1, TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(channel, "dispatcher"), "maxSubscribers", Integer.class).intValue());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -32,15 +32,15 @@ import org.springframework.util.StringUtils;
|
||||
/**
|
||||
* Base implementation of {@link MessageChannel} that invokes the subscribed
|
||||
* {@link MessageHandler handler(s)} by delegating to a {@link MessageDispatcher}.
|
||||
*
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public abstract class AbstractSubscribableChannel extends AbstractMessageChannel implements SubscribableChannel {
|
||||
|
||||
|
||||
private final AtomicInteger handlerCounter = new AtomicInteger();
|
||||
|
||||
|
||||
public boolean subscribe(MessageHandler handler) {
|
||||
MessageDispatcher dispatcher = this.getRequiredDispatcher();
|
||||
boolean added = dispatcher.addHandler(handler);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2009 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -23,11 +23,12 @@ import org.springframework.integration.dispatcher.UnicastingDispatcher;
|
||||
/**
|
||||
* A channel that invokes a single subscriber for each sent Message.
|
||||
* The invocation will occur in the sender's thread.
|
||||
*
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Mark Fisher
|
||||
* @author Iwein Fuld
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class DirectChannel extends AbstractSubscribableChannel {
|
||||
|
||||
@@ -57,6 +58,15 @@ public class DirectChannel extends AbstractSubscribableChannel {
|
||||
this.dispatcher.setFailover(failover);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of subscribers supported by the
|
||||
* channel's dispatcher.
|
||||
* @param maxSubscribers
|
||||
*/
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.dispatcher.setMaxSubscribers(maxSubscribers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UnicastingDispatcher getDispatcher() {
|
||||
return this.dispatcher;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2010 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -37,9 +37,10 @@ import org.springframework.util.ErrorHandler;
|
||||
* {@link Executor} typically does not block the sender's Thread since it
|
||||
* uses another Thread for the dispatch.</emphasis> (SyncTaskExecutor is an
|
||||
* exception but would provide no value for this channel. If synchronous
|
||||
* dispatching is required, a DirectChannel should be used instead).
|
||||
*
|
||||
* dispatching is required, a DirectChannel should be used instead).
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @since 1.0.3
|
||||
*/
|
||||
public class ExecutorChannel extends AbstractSubscribableChannel {
|
||||
@@ -50,6 +51,8 @@ public class ExecutorChannel extends AbstractSubscribableChannel {
|
||||
|
||||
private volatile boolean failover = true;
|
||||
|
||||
private volatile int maxSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
private volatile LoadBalancingStrategy loadBalancingStrategy;
|
||||
|
||||
|
||||
@@ -89,6 +92,16 @@ public class ExecutorChannel extends AbstractSubscribableChannel {
|
||||
this.dispatcher.setFailover(failover);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of subscribers supported by the
|
||||
* channel's dispatcher.
|
||||
* @param maxSubscribers
|
||||
*/
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.maxSubscribers = maxSubscribers;
|
||||
this.dispatcher.setMaxSubscribers(maxSubscribers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UnicastingDispatcher getDispatcher() {
|
||||
return this.dispatcher;
|
||||
@@ -103,6 +116,7 @@ public class ExecutorChannel extends AbstractSubscribableChannel {
|
||||
}
|
||||
this.dispatcher = new UnicastingDispatcher(this.executor);
|
||||
this.dispatcher.setFailover(this.failover);
|
||||
this.dispatcher.setMaxSubscribers(maxSubscribers);
|
||||
if (this.loadBalancingStrategy != null) {
|
||||
this.dispatcher.setLoadBalancingStrategy(this.loadBalancingStrategy);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2010 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -24,10 +24,11 @@ import org.springframework.integration.util.ErrorHandlingTaskExecutor;
|
||||
import org.springframework.util.ErrorHandler;
|
||||
|
||||
/**
|
||||
* A channel that sends Messages to each of its subscribers.
|
||||
*
|
||||
* A channel that sends Messages to each of its subscribers.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class PublishSubscribeChannel extends AbstractSubscribableChannel {
|
||||
|
||||
@@ -41,6 +42,9 @@ public class PublishSubscribeChannel extends AbstractSubscribableChannel {
|
||||
|
||||
private volatile boolean applySequence;
|
||||
|
||||
private volatile int maxSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
@Override
|
||||
public String getComponentType(){
|
||||
return "publish-subscribe-channel";
|
||||
}
|
||||
@@ -56,7 +60,7 @@ public class PublishSubscribeChannel extends AbstractSubscribableChannel {
|
||||
|
||||
/**
|
||||
* Create a PublishSubscribeChannel that will invoke the handlers in the
|
||||
* message sender's thread.
|
||||
* message sender's thread.
|
||||
*/
|
||||
public PublishSubscribeChannel() {
|
||||
this(null);
|
||||
@@ -103,6 +107,15 @@ public class PublishSubscribeChannel extends AbstractSubscribableChannel {
|
||||
this.getDispatcher().setApplySequence(applySequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of subscribers supported by the
|
||||
* channel's dispatcher.
|
||||
* @param maxSubscribers
|
||||
*/
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.maxSubscribers = maxSubscribers;
|
||||
this.getDispatcher().setMaxSubscribers(maxSubscribers);
|
||||
}
|
||||
/**
|
||||
* Callback method for initialization.
|
||||
*/
|
||||
@@ -119,6 +132,7 @@ public class PublishSubscribeChannel extends AbstractSubscribableChannel {
|
||||
this.dispatcher = new BroadcastingDispatcher(this.executor);
|
||||
this.dispatcher.setIgnoreFailures(this.ignoreFailures);
|
||||
this.dispatcher.setApplySequence(this.applySequence);
|
||||
this.dispatcher.setMaxSubscribers(this.maxSubscribers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,11 @@ package org.springframework.integration.config.xml;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.aop.scope.ScopedProxyUtils;
|
||||
import org.springframework.beans.PropertyValue;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
import org.springframework.beans.factory.config.TypedStringValue;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
@@ -85,4 +89,37 @@ public abstract class AbstractChannelParser extends AbstractBeanDefinitionParser
|
||||
*/
|
||||
protected abstract BeanDefinitionBuilder buildBeanDefinition(Element element, ParserContext parserContext);
|
||||
|
||||
protected void setMaxSubscribersProperty(ParserContext parserContext, BeanDefinitionBuilder builder, Element element, String channelInitializerPropertyName) {
|
||||
String maxSubscribers = element.getAttribute("max-subscribers");
|
||||
if (!StringUtils.hasText(maxSubscribers)) {
|
||||
maxSubscribers = getDefaultMaxSubscribers(parserContext, channelInitializerPropertyName);
|
||||
}
|
||||
if (StringUtils.hasText(maxSubscribers)) {
|
||||
builder.addPropertyValue("maxSubscribers", maxSubscribers);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getDefaultMaxSubscribers(ParserContext parserContext, String channelInitializerPropertyName) {
|
||||
String maxSubscribers = null;
|
||||
BeanDefinition channelInitializer = parserContext.getRegistry().getBeanDefinition(
|
||||
AbstractIntegrationNamespaceHandler.CHANNEL_INITIALIZER_BEAN_NAME);
|
||||
if (channelInitializer != null) {
|
||||
PropertyValues propertyValues = channelInitializer.getPropertyValues();
|
||||
if (propertyValues != null) {
|
||||
PropertyValue propertyValue = propertyValues
|
||||
.getPropertyValue(channelInitializerPropertyName);
|
||||
if (propertyValue != null) {
|
||||
Object propertyValueValue = propertyValue.getValue();
|
||||
if (propertyValueValue instanceof TypedStringValue) {
|
||||
maxSubscribers = ((TypedStringValue) propertyValueValue).getValue();
|
||||
}
|
||||
else if (propertyValueValue instanceof String) {
|
||||
maxSubscribers = (String) propertyValueValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxSubscribers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import java.util.Collection;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
@@ -41,11 +40,13 @@ import org.springframework.util.Assert;
|
||||
* {@link AbstractIntegrationNamespaceHandler}.
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
* @since 2.1.1
|
||||
*/
|
||||
final class ChannelInitializer implements BeanFactoryAware, InitializingBean {
|
||||
|
||||
public static String AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME = "$autoCreateChannelCandidates";
|
||||
|
||||
public static String CHANNEL_NAMES_ATTR = "channelNames";
|
||||
|
||||
private Log logger = LogFactory.getLog(this.getClass());
|
||||
@@ -54,6 +55,10 @@ final class ChannelInitializer implements BeanFactoryAware, InitializingBean {
|
||||
|
||||
private volatile boolean autoCreate = true;
|
||||
|
||||
private volatile int defaultMaxUnicastSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
private volatile int defaultMaxBroadcastSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
public void setAutoCreate(boolean autoCreate) {
|
||||
this.autoCreate = autoCreate;
|
||||
}
|
||||
@@ -62,6 +67,32 @@ final class ChannelInitializer implements BeanFactoryAware, InitializingBean {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
public int getDefaultMaxUnicastSubscribers() {
|
||||
return defaultMaxUnicastSubscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default max-subscribers for all unicasting channels that don't have the
|
||||
* attribute set on their dispatcher. Default {@link Integer#MAX_VALUE}.
|
||||
* @param defaultMaxUnicastSubscribers
|
||||
*/
|
||||
public void setDefaultMaxUnicastSubscribers(int defaultMaxUnicastSubscribers) {
|
||||
this.defaultMaxUnicastSubscribers = defaultMaxUnicastSubscribers;
|
||||
}
|
||||
|
||||
public int getDefaultMaxBroadcastSubscribers() {
|
||||
return defaultMaxBroadcastSubscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default max-subscribers for all broadcasting (pub-sub) channels that don't have the
|
||||
* attribute set. Default {@link Integer#MAX_VALUE}.
|
||||
* @param defaultMaxBroadcastSubscribers
|
||||
*/
|
||||
public void setDefaultMaxBroadcastSubscribers(int defaultMaxBroadcastSubscribers) {
|
||||
this.defaultMaxBroadcastSubscribers = defaultMaxBroadcastSubscribers;
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.notNull(this.beanFactory, "'beanFactory' must not be null");
|
||||
if (!autoCreate){
|
||||
|
||||
@@ -55,6 +55,20 @@ public abstract class IntegrationNamespaceUtils {
|
||||
static final String EXPRESSION_ATTRIBUTE = "expression";
|
||||
public static final String HANDLER_ALIAS_SUFFIX = ".handler";
|
||||
|
||||
/**
|
||||
* Property name on ChannelInitializer used to configure the default max subscribers for
|
||||
* unicast channels.
|
||||
*/
|
||||
public static String DEFAULT_MAX_UNICAST_SUBSCRIBERS_PROPERTY_NAME = "defaultMaxUnicastSubscribers";
|
||||
|
||||
/**
|
||||
* Property name on ChannelInitializer used to configure the default max subscribers for
|
||||
* broadcast channels.
|
||||
*/
|
||||
public static String DEFAULT_MAX_BROADCAST_SUBSCRIBERS_PROPERTY_NAME = "defaultMaxBroadcastSubscribers";
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Configures the provided bean definition builder with a property value corresponding to the attribute whose name
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-2012 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,8 +18,6 @@ package org.springframework.integration.config.xml;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.config.TypedStringValue;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
@@ -31,13 +29,15 @@ import org.springframework.integration.channel.RendezvousChannel;
|
||||
import org.springframework.integration.store.MessageGroupQueue;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.xml.DomUtils;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
* Parser for the <channel> element.
|
||||
*
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Iwein Fuld
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class PointToPointChannelParser extends AbstractChannelParser {
|
||||
|
||||
@@ -116,11 +116,16 @@ public class PointToPointChannelParser extends AbstractChannelParser {
|
||||
if ("failover".equals(dispatcherAttribute)) {
|
||||
// round-robin dispatcher is used by default, the "failover" value simply disables it
|
||||
builder.addConstructorArgValue(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (dispatcherElement == null) {
|
||||
// configure the default DirectChannel with a RoundRobinLoadBalancingStrategy
|
||||
builder = BeanDefinitionBuilder.genericBeanDefinition(DirectChannel.class);
|
||||
String maxSubscribers = this.getDefaultMaxSubscribers(parserContext,
|
||||
IntegrationNamespaceUtils.DEFAULT_MAX_UNICAST_SUBSCRIBERS_PROPERTY_NAME);
|
||||
if (maxSubscribers != null) {
|
||||
builder.addPropertyValue("maxSubscribers", maxSubscribers);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// configure either an ExecutorChannel or DirectChannel based on existence of 'task-executor'
|
||||
@@ -137,8 +142,10 @@ public class PointToPointChannelParser extends AbstractChannelParser {
|
||||
String loadBalancer = dispatcherElement.getAttribute("load-balancer");
|
||||
if ("none".equals(loadBalancer)) {
|
||||
builder.addConstructorArgValue(null);
|
||||
}
|
||||
}
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, dispatcherElement, "failover");
|
||||
this.setMaxSubscribersProperty(parserContext, builder, dispatcherElement,
|
||||
IntegrationNamespaceUtils.DEFAULT_MAX_UNICAST_SUBSCRIBERS_PROPERTY_NAME);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2009 the original author or authors.
|
||||
* Copyright 2002-2012 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,23 +16,24 @@
|
||||
|
||||
package org.springframework.integration.config.xml;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.integration.channel.PublishSubscribeChannel;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
* Parser for the <publish-subscribe-channel> element.
|
||||
*
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class PublishSubscribeChannelParser extends AbstractChannelParser {
|
||||
|
||||
@Override
|
||||
protected BeanDefinitionBuilder buildBeanDefinition(Element element, ParserContext parserContext) {
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
|
||||
IntegrationNamespaceUtils.BASE_PACKAGE + ".channel.PublishSubscribeChannel");
|
||||
PublishSubscribeChannel.class);
|
||||
String taskExecutorRef = element.getAttribute("task-executor");
|
||||
if (StringUtils.hasText(taskExecutorRef)) {
|
||||
builder.addConstructorArgReference(taskExecutorRef);
|
||||
@@ -40,6 +41,8 @@ public class PublishSubscribeChannelParser extends AbstractChannelParser {
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-handler");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "ignore-failures");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "apply-sequence");
|
||||
this.setMaxSubscribersProperty(parserContext, builder, element,
|
||||
IntegrationNamespaceUtils.DEFAULT_MAX_BROADCAST_SUBSCRIBERS_PROPERTY_NAME);
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.integration.core.MessageHandler;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -43,9 +42,18 @@ public abstract class AbstractDispatcher implements MessageDispatcher {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
private volatile int maxSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
private final OrderedAwareCopyOnWriteArraySet<MessageHandler> handlers =
|
||||
new OrderedAwareCopyOnWriteArraySet<MessageHandler>();
|
||||
|
||||
/**
|
||||
* Set the maximum subscribers allowed by this dispatcher.
|
||||
* @param maxSubscribers
|
||||
*/
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.maxSubscribers = maxSubscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable {@link Set} of this dispatcher's handlers. This
|
||||
@@ -62,6 +70,7 @@ public abstract class AbstractDispatcher implements MessageDispatcher {
|
||||
*/
|
||||
public boolean addHandler(MessageHandler handler) {
|
||||
Assert.notNull(handler, "handler must not be null");
|
||||
Assert.isTrue(this.handlers.size() < this.maxSubscribers, "Maximum subscribers exceeded");
|
||||
return this.handlers.add(handler);
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attributeGroup ref="subscribersAttributeGroup" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:element name="publish-subscribe-channel">
|
||||
@@ -388,6 +389,7 @@
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attributeGroup ref="subscribersAttributeGroup" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -3729,4 +3731,15 @@ endpoint itself is a Polling Consumer for a channel with a queue.
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup"/>
|
||||
</xsd:attributeGroup>
|
||||
|
||||
<xsd:attributeGroup name="subscribersAttributeGroup">
|
||||
<xsd:attribute name="max-subscribers" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Specifies the maximum subscribers allowed on this channel; defaults to
|
||||
Integer.MAX_VALUE, unless a 'channelInitializer' bean has previously been
|
||||
declared, with a different default.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:attributeGroup>
|
||||
</xsd:schema>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xmlns:task="http://www.springframework.org/schema/task"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-2.2.xsd
|
||||
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.1.xsd
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<int:channel id="defaultChannel" />
|
||||
|
||||
<int:channel id="defaultChannel2">
|
||||
<int:dispatcher />
|
||||
</int:channel>
|
||||
|
||||
<int:channel id="explicitChannel">
|
||||
<int:dispatcher max-subscribers="123" />
|
||||
</int:channel>
|
||||
|
||||
<int:channel id="executorChannel">
|
||||
<int:dispatcher task-executor="taskExecutor" />
|
||||
</int:channel>
|
||||
|
||||
<int:channel id="explicitExecutorChannel">
|
||||
<int:dispatcher task-executor="taskExecutor" max-subscribers="234" />
|
||||
</int:channel>
|
||||
|
||||
<task:executor id="taskExecutor"/>
|
||||
|
||||
<int:publish-subscribe-channel id="pubSubDefaultChannel" />
|
||||
|
||||
<int:publish-subscribe-channel id="pubSubExplicitChannel" max-subscribers="2 "/>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2002-2012 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
|
||||
*
|
||||
* http://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.xml;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.2
|
||||
*
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class DispatcherMaxSubscribersDefaultConfigurationTests extends DispatcherMaxSubscribersTests {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
doTestUnicast(Integer.MAX_VALUE, Integer.MAX_VALUE, 123, Integer.MAX_VALUE, 234);
|
||||
doTestMulticast(Integer.MAX_VALUE, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<!-- Explicit channel initializer, but no specification of the default max subscribers properties -->
|
||||
|
||||
<bean id="channelInitializer" class="org.springframework.integration.config.xml.ChannelInitializer">
|
||||
<property name="autoCreate" value="true" />
|
||||
</bean>
|
||||
|
||||
<import resource="DispatcherMaxSubscribersDefaultConfigurationTests-context.xml" />
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2002-2012 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
|
||||
*
|
||||
* http://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.xml;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.2
|
||||
*
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class DispatcherMaxSubscribersDontOverrideDefaultTests extends DispatcherMaxSubscribersTests {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
doTestUnicast(Integer.MAX_VALUE, Integer.MAX_VALUE, 123, Integer.MAX_VALUE, 234);
|
||||
doTestMulticast(Integer.MAX_VALUE, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-2.2.xsd
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<bean id="channelInitializer" class="org.springframework.integration.config.xml.ChannelInitializer">
|
||||
<property name="autoCreate" value="true" />
|
||||
<property name="defaultMaxUnicastSubscribers" value="456" />
|
||||
<property name="defaultMaxBroadcastSubscribers" value="789" />
|
||||
</bean>
|
||||
|
||||
<import resource="DispatcherMaxSubscribersDefaultConfigurationTests-context.xml" />
|
||||
|
||||
<int:channel id="oneSub">
|
||||
<int:dispatcher max-subscribers="1" />
|
||||
</int:channel>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2002-2012 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
|
||||
*
|
||||
* http://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.xml;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessagingException;
|
||||
import org.springframework.integration.core.MessageHandler;
|
||||
import org.springframework.integration.core.SubscribableChannel;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.2
|
||||
*
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class DispatcherMaxSubscribersOverrideDefaultTests extends DispatcherMaxSubscribersTests {
|
||||
|
||||
@Autowired
|
||||
private SubscribableChannel oneSub;
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
this.doTestUnicast(456, 456, 123, 456, 234);
|
||||
doTestMulticast(789, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceed() {
|
||||
oneSub.subscribe(new MessageHandler() {
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
}
|
||||
});
|
||||
try {
|
||||
oneSub.subscribe(new MessageHandler() {
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
}
|
||||
});
|
||||
fail("Expected Exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertEquals("Maximum subscribers exceeded", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2002-2012 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
|
||||
*
|
||||
* http://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.xml;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.integration.MessageChannel;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.2
|
||||
*
|
||||
*/
|
||||
public abstract class DispatcherMaxSubscribersTests {
|
||||
|
||||
@Autowired
|
||||
private MessageChannel defaultChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel defaultChannel2;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel explicitChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel executorChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel explicitExecutorChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel pubSubDefaultChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel pubSubExplicitChannel;
|
||||
|
||||
public DispatcherMaxSubscribersTests() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected void doTestUnicast(int val1, int val2, int val3, int val4, int val5) {
|
||||
Integer defaultMax = TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(defaultChannel, "dispatcher"), "maxSubscribers", Integer.class);
|
||||
assertEquals(val1, defaultMax.intValue());
|
||||
Integer defaultMax2 = TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(defaultChannel2, "dispatcher"), "maxSubscribers", Integer.class);
|
||||
assertEquals(val2, defaultMax2.intValue());
|
||||
Integer explicitMax = TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(explicitChannel, "dispatcher"), "maxSubscribers", Integer.class);
|
||||
assertEquals(val3, explicitMax.intValue());
|
||||
Integer execMax = TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(executorChannel, "dispatcher"), "maxSubscribers", Integer.class);
|
||||
assertEquals(val4, execMax.intValue());
|
||||
Integer explicitExecMax = TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(explicitExecutorChannel, "dispatcher"), "maxSubscribers", Integer.class);
|
||||
assertEquals(val5, explicitExecMax.intValue());
|
||||
}
|
||||
|
||||
protected void doTestMulticast(int val1, int val2) {
|
||||
Integer defaultMax = TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(pubSubDefaultChannel, "dispatcher"), "maxSubscribers", Integer.class);
|
||||
assertEquals(val1, defaultMax.intValue());
|
||||
Integer explicitMax = TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(pubSubExplicitChannel, "dispatcher"), "maxSubscribers", Integer.class);
|
||||
assertEquals(val2, explicitMax.intValue());
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import javax.jms.MessageListener;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.integration.Message;
|
||||
@@ -29,6 +28,7 @@ import org.springframework.integration.MessageDispatchingException;
|
||||
import org.springframework.integration.MessagingException;
|
||||
import org.springframework.integration.core.MessageHandler;
|
||||
import org.springframework.integration.core.SubscribableChannel;
|
||||
import org.springframework.integration.dispatcher.AbstractDispatcher;
|
||||
import org.springframework.integration.dispatcher.BroadcastingDispatcher;
|
||||
import org.springframework.integration.dispatcher.MessageDispatcher;
|
||||
import org.springframework.integration.dispatcher.RoundRobinLoadBalancingStrategy;
|
||||
@@ -48,16 +48,26 @@ public class SubscribableJmsChannel extends AbstractJmsChannel implements Subscr
|
||||
|
||||
private final AbstractMessageListenerContainer container;
|
||||
|
||||
private volatile MessageDispatcher dispatcher;
|
||||
|
||||
private volatile AbstractDispatcher dispatcher;
|
||||
|
||||
private volatile boolean initialized;
|
||||
|
||||
|
||||
private volatile int maxSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
public SubscribableJmsChannel(AbstractMessageListenerContainer container, JmsTemplate jmsTemplate) {
|
||||
super(jmsTemplate);
|
||||
Assert.notNull(container, "container must not be null");
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of subscribers supported by the
|
||||
* channel's dispatcher.
|
||||
* @param maxSubscribers
|
||||
*/
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.maxSubscribers = maxSubscribers;
|
||||
}
|
||||
|
||||
public boolean subscribe(MessageHandler handler) {
|
||||
Assert.state(this.dispatcher != null, "'MessageDispatcher' must not be null. This channel might not have been initialized");
|
||||
@@ -96,6 +106,7 @@ public class SubscribableJmsChannel extends AbstractJmsChannel implements Subscr
|
||||
unicastingDispatcher.setLoadBalancingStrategy(new RoundRobinLoadBalancingStrategy());
|
||||
this.dispatcher = unicastingDispatcher;
|
||||
}
|
||||
this.dispatcher.setMaxSubscribers(this.maxSubscribers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
private volatile String clientId;
|
||||
|
||||
private volatile String concurrency;
|
||||
|
||||
|
||||
private volatile Integer concurrentConsumers;
|
||||
|
||||
private volatile ConnectionFactory connectionFactory;
|
||||
@@ -110,7 +110,7 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
private volatile Long receiveTimeout;
|
||||
|
||||
private volatile Long recoveryInterval;
|
||||
|
||||
|
||||
private volatile String beanName;
|
||||
|
||||
/**
|
||||
@@ -133,6 +133,8 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
|
||||
private volatile Integer transactionTimeout;
|
||||
|
||||
private volatile int maxSubscribers = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
public JmsChannelFactoryBean() {
|
||||
this(true);
|
||||
@@ -202,7 +204,7 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
public void setConcurrency(String concurrency) {
|
||||
this.concurrency = concurrency;
|
||||
}
|
||||
|
||||
|
||||
public void setConcurrentConsumers(int concurrentConsumers) {
|
||||
this.concurrentConsumers = concurrentConsumers;
|
||||
}
|
||||
@@ -314,6 +316,10 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
this.transactionTimeout = transactionTimeout;
|
||||
}
|
||||
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.maxSubscribers = maxSubscribers;
|
||||
}
|
||||
|
||||
public void setBeanName(String name) {
|
||||
this.beanName = name;
|
||||
}
|
||||
@@ -328,7 +334,9 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
this.initializeJmsTemplate();
|
||||
if (this.messageDriven) {
|
||||
this.container = this.createContainer();
|
||||
this.channel = new SubscribableJmsChannel(this.container, this.jmsTemplate);
|
||||
SubscribableJmsChannel subscribableJmsChannel = new SubscribableJmsChannel(this.container, this.jmsTemplate);
|
||||
subscribableJmsChannel.setMaxSubscribers(this.maxSubscribers);
|
||||
this.channel = subscribableJmsChannel;
|
||||
}
|
||||
else {
|
||||
Assert.isTrue(!Boolean.TRUE.equals(this.pubSubDomain),
|
||||
@@ -388,27 +396,27 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
container.setSessionAcknowledgeMode(this.sessionAcknowledgeMode);
|
||||
container.setSessionTransacted(this.sessionTransacted);
|
||||
container.setSubscriptionDurable(this.subscriptionDurable);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (container instanceof DefaultMessageListenerContainer) {
|
||||
DefaultMessageListenerContainer dmlc = (DefaultMessageListenerContainer) container;
|
||||
if (this.cacheLevelName != null) {
|
||||
dmlc.setCacheLevelName(this.cacheLevelName);
|
||||
}
|
||||
|
||||
|
||||
if (StringUtils.hasText(this.concurrency)){
|
||||
dmlc.setConcurrency(this.concurrency);
|
||||
}
|
||||
|
||||
|
||||
if (this.concurrentConsumers != null){
|
||||
dmlc.setConcurrentConsumers(this.concurrentConsumers);
|
||||
}
|
||||
|
||||
|
||||
if (this.maxConcurrentConsumers != null){
|
||||
dmlc.setMaxConcurrentConsumers(this.maxConcurrentConsumers);
|
||||
}
|
||||
|
||||
|
||||
if (this.idleTaskExecutionLimit != null) {
|
||||
dmlc.setIdleTaskExecutionLimit(this.idleTaskExecutionLimit);
|
||||
}
|
||||
@@ -437,11 +445,11 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
if (StringUtils.hasText(this.concurrency)){
|
||||
smlc.setConcurrency(this.concurrency);
|
||||
}
|
||||
|
||||
|
||||
if (this.concurrentConsumers != null){
|
||||
smlc.setConcurrentConsumers(this.concurrentConsumers);
|
||||
}
|
||||
|
||||
|
||||
smlc.setPubSubNoLocal(this.pubSubNoLocal);
|
||||
smlc.setTaskExecutor(this.taskExecutor);
|
||||
}
|
||||
@@ -485,6 +493,7 @@ public class JmsChannelFactoryBean extends AbstractFactoryBean<AbstractJmsChanne
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void destroyInstance(AbstractJmsChannel instance) throws Exception {
|
||||
if (instance instanceof SubscribableJmsChannel) {
|
||||
((SubscribableJmsChannel) this.channel).destroy();
|
||||
|
||||
@@ -58,9 +58,11 @@ public class JmsChannelParser extends AbstractChannelParser {
|
||||
builder.addPropertyReference("connectionFactory", connectionFactory);
|
||||
if ("channel".equals(element.getLocalName())) {
|
||||
this.parseDestination(element, parserContext, builder, "queue");
|
||||
this.setMaxSubscribersProperty(parserContext, builder, element, IntegrationNamespaceUtils.DEFAULT_MAX_UNICAST_SUBSCRIBERS_PROPERTY_NAME);
|
||||
}
|
||||
else if ("publish-subscribe-channel".equals(element.getLocalName())) {
|
||||
this.parseDestination(element, parserContext, builder, "topic");
|
||||
this.setMaxSubscribersProperty(parserContext, builder, element, IntegrationNamespaceUtils.DEFAULT_MAX_BROADCAST_SUBSCRIBERS_PROPERTY_NAME);
|
||||
}
|
||||
String containerType = element.getAttribute(CONTAINER_TYPE_ATTRIBUTE);
|
||||
String containerClass = element.getAttribute(CONTAINER_CLASS_ATTRIBUTE);
|
||||
|
||||
@@ -376,6 +376,7 @@
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attributeGroup ref="integration:subscribersAttributeGroup" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:element name="message-driven-channel-adapter">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
time-to-live="123"
|
||||
priority="12"/>
|
||||
|
||||
<jms:channel id="queueNameChannel" queue-name="test.queue"/>
|
||||
<jms:channel id="queueNameChannel" queue-name="test.queue" max-subscribers="1" />
|
||||
|
||||
<jms:channel id="queueNameWithResolverChannel" queue-name="foo"
|
||||
destination-resolver="destinationResolver" connection-factory="connFact"/>
|
||||
|
||||
@@ -131,6 +131,8 @@ public class JmsChannelParserTests {
|
||||
assertEquals(DeliveryMode.PERSISTENT, TestUtils.getPropertyValue(jmsTemplate, "deliveryMode"));
|
||||
assertEquals(123L, TestUtils.getPropertyValue(jmsTemplate, "timeToLive"));
|
||||
assertEquals(12, TestUtils.getPropertyValue(jmsTemplate, "priority"));
|
||||
assertEquals(Integer.MAX_VALUE, TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(channel, "dispatcher"), "maxSubscribers", Integer.class).intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,6 +144,8 @@ public class JmsChannelParserTests {
|
||||
AbstractMessageListenerContainer container = (AbstractMessageListenerContainer) accessor.getPropertyValue("container");
|
||||
assertEquals("test.queue", jmsTemplate.getDefaultDestinationName());
|
||||
assertEquals("test.queue", container.getDestinationName());
|
||||
assertEquals(1, TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(channel, "dispatcher"), "maxSubscribers", Integer.class).intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -36,8 +36,8 @@ import org.springframework.integration.channel.AbstractMessageChannel;
|
||||
import org.springframework.integration.channel.MessagePublishingErrorHandler;
|
||||
import org.springframework.integration.core.MessageHandler;
|
||||
import org.springframework.integration.core.SubscribableChannel;
|
||||
import org.springframework.integration.dispatcher.AbstractDispatcher;
|
||||
import org.springframework.integration.dispatcher.BroadcastingDispatcher;
|
||||
import org.springframework.integration.dispatcher.MessageDispatcher;
|
||||
import org.springframework.integration.support.channel.BeanFactoryChannelResolver;
|
||||
import org.springframework.integration.support.converter.MessageConverter;
|
||||
import org.springframework.integration.support.converter.SimpleMessageConverter;
|
||||
@@ -58,16 +58,16 @@ public class SubscribableRedisChannel extends AbstractMessageChannel implements
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
private final RedisTemplate redisTemplate;
|
||||
private final String topicName;
|
||||
|
||||
private final MessageDispatcher dispatcher = new BroadcastingDispatcher(true);
|
||||
|
||||
|
||||
private final AbstractDispatcher dispatcher = new BroadcastingDispatcher(true);
|
||||
|
||||
private volatile boolean initialized;
|
||||
|
||||
|
||||
// defaults
|
||||
private volatile Executor taskExecutor = new SimpleAsyncTaskExecutor();
|
||||
private volatile RedisSerializer<?> serializer = new StringRedisSerializer();
|
||||
private volatile MessageConverter messageConverter = new SimpleMessageConverter();
|
||||
|
||||
|
||||
public SubscribableRedisChannel(RedisConnectionFactory connectionFactory, String topicName) {
|
||||
Assert.notNull(connectionFactory, "'connectionFactory' must not be null");
|
||||
Assert.hasText(topicName, "'topicName' must not be empty");
|
||||
@@ -80,17 +80,26 @@ public class SubscribableRedisChannel extends AbstractMessageChannel implements
|
||||
Assert.notNull(taskExecutor, "'taskExecutor' must not be null");
|
||||
this.taskExecutor = taskExecutor;
|
||||
}
|
||||
|
||||
|
||||
public void setMessageConverter(MessageConverter messageConverter) {
|
||||
Assert.notNull(messageConverter, "'messageConverter' must not be null");
|
||||
this.messageConverter = messageConverter;
|
||||
}
|
||||
|
||||
|
||||
public void setSerializer(RedisSerializer<?> serializer) {
|
||||
Assert.notNull(serializer, "'serializer' must not be null");
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of subscribers supported by the
|
||||
* channel's dispatcher.
|
||||
* @param maxSubscribers
|
||||
*/
|
||||
public void setMaxSubscribers(int maxSubscribers) {
|
||||
this.dispatcher.setMaxSubscribers(maxSubscribers);
|
||||
}
|
||||
|
||||
public boolean subscribe(MessageHandler handler) {
|
||||
return this.dispatcher.addHandler(handler);
|
||||
}
|
||||
@@ -98,7 +107,7 @@ public class SubscribableRedisChannel extends AbstractMessageChannel implements
|
||||
public boolean unsubscribe(MessageHandler handler) {
|
||||
return this.dispatcher.removeHandler(handler);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean doSend(Message<?> message, long arg1) {
|
||||
this.redisTemplate.convertAndSend(this.topicName, this.messageConverter.fromMessage(message));
|
||||
@@ -169,7 +178,7 @@ public class SubscribableRedisChannel extends AbstractMessageChannel implements
|
||||
this.container.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class MessageListenerDelegate {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-2012 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,21 +16,21 @@
|
||||
|
||||
package org.springframework.integration.redis.config;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.integration.config.xml.AbstractChannelParser;
|
||||
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
|
||||
import org.springframework.integration.redis.channel.SubscribableRedisChannel;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
* Parser for the 'channel' and 'publish-subscribe-channel' elements of the
|
||||
* Spring Integration Redis namespace.
|
||||
*
|
||||
*
|
||||
* @author Oleg Zhurakusky
|
||||
* @author Artem Bilan
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
*/
|
||||
public class RedisChannelParser extends AbstractChannelParser {
|
||||
@@ -45,13 +45,14 @@ public class RedisChannelParser extends AbstractChannelParser {
|
||||
builder.addConstructorArgReference(connectionFactory);
|
||||
String topicName = element.getAttribute("topic-name");
|
||||
builder.addConstructorArgValue(topicName);
|
||||
|
||||
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "task-executor");
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-converter");
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "serializer");
|
||||
// The following 2 attributes should be added once configurable on the RedisMessageListenerContainer
|
||||
// IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "phase");
|
||||
// IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-startup");
|
||||
this.setMaxSubscribersProperty(parserContext, builder, element, IntegrationNamespaceUtils.DEFAULT_MAX_BROADCAST_SUBSCRIBERS_PROPERTY_NAME);
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
<!-- ]]></xsd:documentation> -->
|
||||
<!-- </xsd:annotation> -->
|
||||
<!-- </xsd:attribute> -->
|
||||
<xsd:attributeGroup ref="integration:subscribersAttributeGroup" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:element name="inbound-channel-adapter">
|
||||
|
||||
@@ -18,4 +18,7 @@
|
||||
|
||||
<bean id="redisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
|
||||
|
||||
<int-redis:publish-subscribe-channel id="redisChannelWithSubLimit" topic-name="si.test.topic"
|
||||
serializer="redisSerializer" max-subscribers="1" />
|
||||
|
||||
</beans>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-2012 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,9 +16,10 @@
|
||||
|
||||
package org.springframework.integration.redis.config;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
@@ -31,24 +32,28 @@ import org.springframework.integration.redis.rules.RedisAvailable;
|
||||
import org.springframework.integration.redis.rules.RedisAvailableTests;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class RedisChannelParserTests extends RedisAvailableTests{
|
||||
|
||||
@Test
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testPubSubChannelConfig(){
|
||||
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("RedisChannelParserTests-context.xml", this.getClass());
|
||||
SubscribableChannel redisChannel = context.getBean("redisChannel", SubscribableChannel.class);
|
||||
JedisConnectionFactory connectionFactory =
|
||||
JedisConnectionFactory connectionFactory =
|
||||
TestUtils.getPropertyValue(redisChannel, "connectionFactory", JedisConnectionFactory.class);
|
||||
RedisSerializer<?> redisSerializer = TestUtils.getPropertyValue(redisChannel, "serializer", RedisSerializer.class);
|
||||
assertEquals(connectionFactory, context.getBean("redisConnectionFactory"));
|
||||
assertEquals(redisSerializer, context.getBean("redisSerializer"));
|
||||
assertEquals("si.test.topic", TestUtils.getPropertyValue(redisChannel, "topicName"));
|
||||
assertEquals(Integer.MAX_VALUE, TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(redisChannel, "dispatcher"), "maxSubscribers", Integer.class).intValue());
|
||||
redisChannel = context.getBean("redisChannelWithSubLimit", SubscribableChannel.class);
|
||||
assertEquals(1, TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(redisChannel, "dispatcher"), "maxSubscribers", Integer.class).intValue());
|
||||
context.stop();
|
||||
}
|
||||
|
||||
@@ -58,9 +63,9 @@ public class RedisChannelParserTests extends RedisAvailableTests{
|
||||
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("RedisChannelParserTests-context.xml", this.getClass());
|
||||
SubscribableChannel redisChannel = context.getBean("redisChannel", SubscribableChannel.class);
|
||||
final Message<?> m = new GenericMessage<String>("Hello Redis");
|
||||
|
||||
|
||||
final Marker marker = Mockito.mock(Marker.class);
|
||||
redisChannel.subscribe(new MessageHandler() {
|
||||
redisChannel.subscribe(new MessageHandler() {
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
assertEquals(m.getPayload(), message.getPayload());
|
||||
marker.mark();
|
||||
@@ -71,7 +76,7 @@ public class RedisChannelParserTests extends RedisAvailableTests{
|
||||
Mockito.verify(marker, Mockito.times(1)).mark();
|
||||
context.stop();
|
||||
}
|
||||
|
||||
|
||||
interface Marker {
|
||||
void mark();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user