From d7d56a04445a04e0b8e9757e8bdb2dc7ee4bce4a Mon Sep 17 00:00:00 2001
From: Artem Bilan Consider using this property when polling the database transactionally
* using multiple parallel threads, meaning when the configured poller is configured
* using a task executor. The issue is that the {@link #pollMessageFromGroup(Object)} looks for the
* oldest entry for a giving channel (groupKey) and region ({@link #setRegion(String)}).
* If you do that with multiple threads and you are using transactions, other
* threads may be waiting for that same locked row. If using the provided {@link OracleChannelMessageStoreQueryProvider}, don't set {@link #usingIdCache}
* to true, as the Oracle query will ignore locked rows. Using the id cache, the {@link JdbcChannelMessageStore} will store each
* message id in an in-memory collection for the duration of processing. With
* that, any polling threads will explicitly exclude those messages from
* being polled. For this to work, you must setup the corresponding
* {@link TransactionSynchronizationFactory}:1
* and with {@link JdbcTemplate#setMaxRows(int)} set to 1.
- *
* @param dataSource a {@link DataSource}
*/
public JdbcChannelMessageStore(DataSource dataSource) {
this();
this.jdbcTemplate = new JdbcTemplate(dataSource);
-
this.jdbcTemplate.setFetchSize(1);
this.jdbcTemplate.setMaxRows(1);
}
@@ -189,22 +181,19 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
* The passed-in {@link DataSource} is used to instantiate a {@link JdbcTemplate}
* with {@link JdbcTemplate#setFetchSize(int)} set to 1
* and with {@link JdbcTemplate#setMaxRows(int)} set to 1.
- *
* @param dataSource a {@link DataSource}
*/
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
-
this.jdbcTemplate.setFetchSize(1);
this.jdbcTemplate.setMaxRows(1);
}
/**
* A converter for deserializing byte arrays to messages.
- *
* @param deserializer the deserializer to set
*/
- @SuppressWarnings({"unchecked", "rawtypes"})
+ @SuppressWarnings({ "unchecked", "rawtypes" })
public void setDeserializer(Deserializer extends Message>> deserializer) {
this.deserializer = new WhiteListDeserializingConverter((Deserializer) deserializer);
}
@@ -223,11 +212,9 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
/**
* The {@link JdbcOperations} to use when interacting with the database. Either
* this property can be set or the {@link #setDataSource(DataSource) dataSource}.
- *
* Please consider passing in a {@link JdbcTemplate} with a fetchSize property
* of 1. This is particularly important for Oracle to ensure First In, First Out (FIFO)
* message retrieval characteristics.
- *
* @param jdbcTemplate a {@link JdbcOperations}
*/
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
@@ -238,7 +225,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
/**
* Override the {@link LobHandler} that is used to create and unpack large objects in SQL queries. The default is
* fine for almost all platforms, but some Oracle drivers require a native implementation.
- *
* @param lobHandler a {@link LobHandler}
*/
public void setLobHandler(LobHandler lobHandler) {
@@ -250,7 +236,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
* Allows for passing in a custom {@link MessageRowMapper}. The {@link MessageRowMapper}
* is used to convert the selected database row representing the persisted
* message into the actual {@link Message} object.
- *
* @param messageRowMapper Must not be null
*/
public void setMessageRowMapper(MessageRowMapper messageRowMapper) {
@@ -259,17 +244,15 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
}
/**
- * Allows for passing in a custom {@link MessageGroupPreparedStatementSetter}.
- * The {@link MessageGroupPreparedStatementSetter} is used to insert message into the database.
- *
- * @param messageGroupPreparedStatementSetter Must not be null
+ * Set a {@link ChannelMessageStorePreparedStatementSetter} to insert message into the database.
+ * @param preparedStatementSetter {@link ChannelMessageStorePreparedStatementSetter} to use.
+ * Must not be null
* @since 5.0
*/
- public void setMessageGroupPreparedStatementSetter(
- MessageGroupPreparedStatementSetter messageGroupPreparedStatementSetter) {
- Assert.notNull(messageGroupPreparedStatementSetter,
- "The provided MessageGroupPreparedStatementSetter must not be null.");
- this.messageGroupPreparedStatementSetter = messageGroupPreparedStatementSetter;
+ public void setPreparedStatementSetter(ChannelMessageStorePreparedStatementSetter preparedStatementSetter) {
+ Assert.notNull(preparedStatementSetter,
+ "The provided ChannelMessageStorePreparedStatementSetter must not be null.");
+ this.preparedStatementSetter = preparedStatementSetter;
}
/**
@@ -291,7 +274,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
* A unique grouping identifier for all messages persisted with this store.
* Using multiple regions allows the store to be partitioned (if necessary)
* for different purposes. Defaults to {@link #DEFAULT_REGION}.
- *
* @param region the region name to set
*/
public void setRegion(String region) {
@@ -300,7 +282,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
/**
* A converter for serializing messages to byte arrays for storage.
- *
* @param serializer The serializer to set
*/
@SuppressWarnings("unchecked")
@@ -312,7 +293,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
/**
* Public setter for the table prefix property. This will be prefixed to all the table names before queries are
* executed. Defaults to {@link #DEFAULT_TABLE_PREFIX}.
- *
* @param tablePrefix the tablePrefix to set
*/
public void setTablePrefix(String tablePrefix) {
@@ -323,23 +303,18 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
*
* {@code
*
- *
* This {@link TransactionSynchronizationFactory} is then referenced in the
* transaction configuration of the poller:
- *
*
* {@code
*
- *
* @param usingIdCache When true the id cache will be used.
*/
public void setUsingIdCache(boolean usingIdCache) {
@@ -393,25 +365,18 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
return this.messageGroupFactory;
}
- @Override
- public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
- this.beanFactory = beanFactory;
- }
-
/**
* Check mandatory properties ({@link DataSource} and
* {@link #setChannelMessageStoreQueryProvider(ChannelMessageStoreQueryProvider)}). If no {@link MessageRowMapper}
- * and {@link MessageGroupPreparedStatementSetter} was explicitly set using
+ * and {@link ChannelMessageStorePreparedStatementSetter} was explicitly set using
* {@link #setMessageRowMapper(MessageRowMapper)} and
- * {@link #setMessageGroupPreparedStatementSetter(MessageGroupPreparedStatementSetter)} respectively, the default
- * {@link MessageRowMapper} and {@link MessageGroupPreparedStatementSetter} will be instantiate using the
+ * {@link #setPreparedStatementSetter(ChannelMessageStorePreparedStatementSetter)} respectively, the default
+ * {@link MessageRowMapper} and {@link ChannelMessageStorePreparedStatementSetter} will be instantiate using the
* specified {@link #deserializer} and {@link #lobHandler}.
- *
* Also, if the jdbcTemplate's fetchSize property ({@link JdbcTemplate#getFetchSize()})
* is not 1, a warning will be logged. When using the {@link JdbcChannelMessageStore}
* with Oracle, the fetchSize value of 1 is needed to ensure FIFO characteristics
* of polled messages. Please see the Oracle {@link ChannelMessageStoreQueryProvider} for more details.
- *
* @throws Exception Any Exception.
*/
@Override
@@ -427,8 +392,8 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
logger.warn("The jdbcTemplate's fetch size is not 1. This may cause FIFO issues with Oracle databases.");
}
- if (this.messageGroupPreparedStatementSetter == null) {
- this.messageGroupPreparedStatementSetter = new MessageGroupPreparedStatementSetter(this.serializer,
+ if (this.preparedStatementSetter == null) {
+ this.preparedStatementSetter = new ChannelMessageStorePreparedStatementSetter(this.serializer,
this.lobHandler);
}
this.jdbcTemplate.afterPropertiesSet();
@@ -437,9 +402,7 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
/**
* Store a message in the database. The groupId identifies the channel for which
* the message is to be stored.
- *
* Keep in mind that the actual groupId (Channel Identifier) is converted to a String-based UUID identifier.
- *
* @param groupId the group id to store the message under
* @param message a message
*/
@@ -447,7 +410,7 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
public MessageGroup addMessageToGroup(Object groupId, final Message> message) {
try {
this.jdbcTemplate.update(getQuery(this.channelMessageStoreQueryProvider.getCreateMessageQuery()),
- ps -> this.messageGroupPreparedStatementSetter.setValues(ps, message, groupId, this.region,
+ ps -> this.preparedStatementSetter.setValues(ps, message, groupId, this.region,
this.priorityEnabled));
}
catch (DuplicateKeyException e) {
@@ -462,7 +425,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
/**
* Helper method that converts the channel id to a UUID using
* {@link UUIDConverter#getUUID(Object)}.
- *
* @param input Parameter may be null
* @return Returns null when the input is null otherwise the UUID as String.
*/
@@ -480,13 +442,13 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
/**
* Method not implemented.
- *
* @return The message group count.
* @throws UnsupportedOperationException Method not supported.
*/
@ManagedAttribute
public int getMessageGroupCount() {
- return this.jdbcTemplate.queryForObject(this.getQuery("SELECT COUNT(DISTINCT GROUP_KEY) from %PREFIX%CHANNEL_MESSAGE where REGION = ?"),
+ return this.jdbcTemplate.queryForObject(
+ getQuery("SELECT COUNT(DISTINCT GROUP_KEY) from %PREFIX%CHANNEL_MESSAGE where REGION = ?"),
Integer.class, this.region);
}
@@ -494,7 +456,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
* Replace patterns in the input to produce a valid SQL query. This implementation lazily initializes a
* simple map-based cache, only replacing the table prefix on the first access to a named query. Further
* accesses will be resolved from the cache.
- *
* @param sqlQuery The SQL query to be transformed.
* @return A transformed query with replacements.
*/
@@ -512,7 +473,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
/**
* Returns the number of messages persisted for the specified channel id (groupId)
* and the specified region ({@link #setRegion(String)}).
- *
* @return The message group size.
*/
@Override
@@ -552,7 +512,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
* This method executes a call to the DB to get the oldest Message in the
* MessageGroup which in the context of the {@link JdbcChannelMessageStore}
* means the channel identifier.
- *
* @param groupIdKey String representation of message group (Channel) ID
* @return a message; could be null if query produced no Messages
*/
@@ -624,7 +583,8 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
final UUID id = messageToRemove.getHeaders().getId();
int updated = this.jdbcTemplate.update(getQuery(this.channelMessageStoreQueryProvider.getDeleteMessageQuery()),
- new Object[]{getKey(id), getKey(groupId), this.region}, new int[]{Types.VARCHAR, Types.VARCHAR, Types.VARCHAR});
+ new Object[] { getKey(id), getKey(groupId), this.region },
+ new int[] { Types.VARCHAR, Types.VARCHAR, Types.VARCHAR });
boolean result = updated != 0;
if (result) {
@@ -644,7 +604,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
* rolled back.
Only applicable if {@link #setUsingIdCache(boolean)} is set to
* true
+ * Behavior is same as standard {@link org.springframework.jdbc.core.PreparedStatementSetter}, + * it takes in additional {@code Message> requestMessage}, {@code Object groupId}, + * {@code String region} and {@code boolean priorityEnabled} parameters used + * for {@code addMessageToGroup} method + * in the {@link org.springframework.integration.jdbc.store.JdbcChannelMessageStore}. + *
+ * This class can be extended for any custom data structure or columns types. + * For this purpose the {@code protected} constructor is provided for inheritors. + * In this case the {@link #serializer} and {@link #lobHandler} are null to avoid + * extra serialization actions if the target custom behavior doesn't imply them. + * + * @author Meherzad Lahewala + * @author Artem Bilan + * + * @since 5.0 + * + * @see org.springframework.jdbc.core.PreparedStatementSetter + */ +public class ChannelMessageStorePreparedStatementSetter { + + private final SerializingConverter serializer; + + private final LobHandler lobHandler; + + /** + * Instantiate a {@link ChannelMessageStorePreparedStatementSetter} with the provided + * serializer and lobHandler, which both must not be null. + * @param serializer the {@link SerializingConverter} to build {@code byte[]} from + * the request message + * @param lobHandler the {@link LobHandler} to store {@code byte[]} of the request + * message to prepared statement + */ + public ChannelMessageStorePreparedStatementSetter(SerializingConverter serializer, LobHandler lobHandler) { + Assert.notNull(serializer, "'serializer' must not be null"); + Assert.notNull(lobHandler, "'lobHandler' must not be null"); + this.serializer = serializer; + this.lobHandler = lobHandler; + } + + /** + * The default constructor for inheritors who are not interested in the message + * serialization to {@code byte[]}. + * The {@link #serializer} and {@link #lobHandler} are null from this constructor, + * therefore any serialization isn't happened in the default {@link #setValues} implementation. + * A target implementor must ensure the proper custom logic for storing message. + */ + protected ChannelMessageStorePreparedStatementSetter() { + this.serializer = null; + this.lobHandler = null; + } + + /** + * Perform a preparedStatement parameters population according provided arguments. + * The default functionality is (parameter - data): + *
+ *
+ * An inheritor may consider to call this method for population common properties and perform + * custom message serialization logic for the parameter #6. + * Any custom data structure population can be achieved with full overriding of this method. + * @param preparedStatement the {@link PreparedStatement} to populate columns based on the provided arguments + * @param requestMessage the {@link Message} to store + * @param groupId the group id for the message to store + * @param region the region in the target table to distinguish different data base clients + * @param priorityEnabled the flag to indicate if priority has to be stored + * @throws SQLException the exception throws during data population + */ + public void setValues(PreparedStatement preparedStatement, Message> requestMessage, Object groupId, String region, + boolean priorityEnabled) throws SQLException { + + String groupKey = Objects.toString(UUIDConverter.getUUID(groupId), null); + long createdDate = System.currentTimeMillis(); + String messageId = Objects.toString(UUIDConverter.getUUID(requestMessage.getHeaders().getId()), null); + + preparedStatement.setString(1, messageId); + preparedStatement.setString(2, groupKey); + preparedStatement.setString(3, region); + preparedStatement.setLong(4, createdDate); + + Integer priority = requestMessage.getHeaders().get(IntegrationMessageHeaderAccessor.PRIORITY, Integer.class); + + if (priorityEnabled && priority != null) { + preparedStatement.setInt(5, priority); + } + else { + preparedStatement.setNull(5, Types.NUMERIC); + } + + if (this.serializer != null) { + byte[] messageBytes = this.serializer.convert(requestMessage); + this.lobHandler.getLobCreator().setBlobAsBytes(preparedStatement, 6, messageBytes); + } + } + +} diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/channel/MessageGroupPreparedStatementSetter.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/channel/MessageGroupPreparedStatementSetter.java deleted file mode 100644 index 6b6d670cfb..0000000000 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/channel/MessageGroupPreparedStatementSetter.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2002-2017 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.jdbc.store.channel; - -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Types; - -import org.springframework.core.serializer.support.SerializingConverter; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.util.UUIDConverter; -import org.springframework.jdbc.support.lob.LobHandler; -import org.springframework.messaging.Message; - -/** - * Callback to be used with {@link org.springframework.integration.jdbc.store.JdbcChannelMessageStore}. - *
- * Behavior is same as standard {@link org.springframework.jdbc.core.PreparedStatementSetter}, it takes in
- * extra {@code Message> requestMessage}, {@code Object groupId},
- * {@code String region} and {@code boolean priorityEnabled} as parameters used
- * for {@code addMessageToGroup} method in {@link org.springframework.integration.jdbc.store.JdbcChannelMessageStore}.
- *
- * @author Meherzad Lahewala
- * @since 5.0
- * @see org.springframework.jdbc.core.PreparedStatementSetter
- */
-public class MessageGroupPreparedStatementSetter {
-
- private final SerializingConverter serializer;
-
- private final LobHandler lobHandler;
-
- public MessageGroupPreparedStatementSetter(SerializingConverter serializer, LobHandler lobHandler) {
- this.serializer = serializer;
- this.lobHandler = lobHandler;
- }
-
- protected MessageGroupPreparedStatementSetter() {
- this.serializer = null;
- this.lobHandler = null;
- }
-
- public void setValues(PreparedStatement preparedStatement, Message> requestMessage, Object groupId, String region,
- boolean priorityEnabled) throws SQLException {
- String groupKey = getKey(groupId);
- long createdDate = System.currentTimeMillis();
- String messageId = getKey(requestMessage.getHeaders().getId());
- byte[] messageBytes = null;
- if (this.serializer != null) {
- messageBytes = this.serializer.convert(requestMessage);
- }
-
- preparedStatement.setString(1, messageId);
- preparedStatement.setString(2, groupKey);
- preparedStatement.setString(3, region);
- preparedStatement.setLong(4, createdDate);
-
- Integer priority = requestMessage.getHeaders().get(IntegrationMessageHeaderAccessor.PRIORITY, Integer.class);
-
- if (priorityEnabled && priority != null) {
- preparedStatement.setInt(5, priority);
- }
- else {
- preparedStatement.setNull(5, Types.NUMERIC);
- }
- if (this.lobHandler != null) {
- this.lobHandler.getLobCreator().setBlobAsBytes(preparedStatement, 6, messageBytes);
- }
- }
-
- protected String getKey(Object input) {
- return input == null ? null : UUIDConverter.getUUID(input).toString();
- }
-}
diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/store/channel/AbstractJdbcChannelMessageStoreTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/store/channel/AbstractJdbcChannelMessageStoreTests.java
index 42d8439fbc..76aee8b5a3 100644
--- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/store/channel/AbstractJdbcChannelMessageStoreTests.java
+++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/store/channel/AbstractJdbcChannelMessageStoreTests.java
@@ -37,7 +37,7 @@ import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.messaging.Message;
import org.springframework.test.annotation.DirtiesContext;
-import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
@@ -49,10 +49,11 @@ import org.springframework.transaction.support.TransactionTemplate;
* @author Gunnar Hillert
* @author Gary Russell
* @author Meherzad Lahewala
+ * @author Artem Bilan
*/
-@RunWith(SpringJUnit4ClassRunner.class)
-@DirtiesContext // close at the end after class
+@RunWith(SpringRunner.class)
+@DirtiesContext
public abstract class AbstractJdbcChannelMessageStoreTests {
protected static final String TEST_MESSAGE_GROUP = "AbstractJdbcChannelMessageStoreTests";
@@ -97,6 +98,7 @@ public abstract class AbstractJdbcChannelMessageStoreTests {
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
+
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
messageStore.addMessageToGroup(TEST_MESSAGE_GROUP, message);
@@ -111,7 +113,7 @@ public abstract class AbstractJdbcChannelMessageStoreTests {
@Test
public void testAddAndGetCustomStatementSetter() {
- messageStore.setMessageGroupPreparedStatementSetter(getMessageGroupPreparedStatementSetter());
+ messageStore.setPreparedStatementSetter(getMessageGroupPreparedStatementSetter());
final Message