GH-9192: Deprecate LobHandler usage

Fixes: #9192

With modern drivers we don't need BLOB-specific handling anymore.
The regular `PreparedStatement.setBytes()` and `ResultSet.getBytes()`
are enough for our serialized messages
This commit is contained in:
Artem Bilan
2024-06-04 17:28:17 -04:00
parent 753916ca24
commit d76174edb1
10 changed files with 610 additions and 77 deletions

View File

@@ -143,8 +143,6 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
private SerializingConverter serializer;
private LobHandler lobHandler = new DefaultLobHandler();
private MessageRowMapper messageRowMapper;
private ChannelMessageStorePreparedStatementSetter preparedStatementSetter;
@@ -231,10 +229,10 @@ 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}
* @deprecated since 6.4 (for removal) (with no replacement) in favor of plain JDBC driver support for byte arrays.
*/
@Deprecated(forRemoval = true, since = "6.4")
public void setLobHandler(LobHandler lobHandler) {
Assert.notNull(lobHandler, "The provided LobHandler must not be null.");
this.lobHandler = lobHandler;
}
/**
@@ -396,8 +394,8 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
* and {@link ChannelMessageStorePreparedStatementSetter} was explicitly set using
* {@link #setMessageRowMapper(MessageRowMapper)} and
* {@link #setPreparedStatementSetter(ChannelMessageStorePreparedStatementSetter)} respectively, the default
* {@link MessageRowMapper} and {@link ChannelMessageStorePreparedStatementSetter} will be instantiate using the
* specified {@link #deserializer} and {@link #lobHandler}.
* {@link MessageRowMapper} and {@link ChannelMessageStorePreparedStatementSetter} will be instantiated using the
* specified {@link #deserializer}.
* 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
@@ -409,7 +407,7 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
Assert.notNull(this.channelMessageStoreQueryProvider, "A channelMessageStoreQueryProvider must be provided.");
if (this.messageRowMapper == null) {
this.messageRowMapper = new MessageRowMapper(this.deserializer, this.lobHandler);
this.messageRowMapper = new MessageRowMapper(this.deserializer);
}
if (this.jdbcTemplate.getFetchSize() != 1) {
@@ -417,8 +415,7 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
}
if (this.preparedStatementSetter == null) {
this.preparedStatementSetter = new ChannelMessageStorePreparedStatementSetter(this.serializer,
this.lobHandler);
this.preparedStatementSetter = new ChannelMessageStorePreparedStatementSetter(this.serializer);
}
this.jdbcTemplate.afterPropertiesSet();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,8 +16,6 @@
package org.springframework.integration.jdbc.store;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Collection;
@@ -38,6 +36,7 @@ import org.springframework.core.serializer.Serializer;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.integration.jdbc.store.channel.MessageRowMapper;
import org.springframework.integration.store.AbstractMessageGroupStore;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.store.MessageGroupMetadata;
@@ -49,9 +48,7 @@ import org.springframework.integration.util.FunctionIterator;
import org.springframework.integration.util.UUIDConverter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.lang.Nullable;
@@ -253,8 +250,6 @@ public class JdbcMessageStore extends AbstractMessageGroupStore
}
}
private final MessageMapper mapper = new MessageMapper();
private final JdbcOperations jdbcTemplate;
private final Map<Query, String> queryCache = new ConcurrentHashMap<>();
@@ -270,9 +265,9 @@ public class JdbcMessageStore extends AbstractMessageGroupStore
private boolean deserializerExplicitlySet;
private SerializingConverter serializer;
private MessageRowMapper mapper = new MessageRowMapper(this.deserializer);
private LobHandler lobHandler = new DefaultLobHandler();
private SerializingConverter serializer;
private boolean checkDatabaseOnStart = true;
@@ -325,9 +320,11 @@ public class JdbcMessageStore extends AbstractMessageGroupStore
* 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}
* @deprecated since 6.4 (for removal) (with no replacement) in favor of plain JDBC driver support for byte arrays.
*/
@Deprecated(forRemoval = true, since = "6.4")
public void setLobHandler(LobHandler lobHandler) {
this.lobHandler = lobHandler;
}
/**
@@ -347,6 +344,7 @@ public class JdbcMessageStore extends AbstractMessageGroupStore
public void setDeserializer(Deserializer<? extends Message<?>> deserializer) {
this.deserializer = new AllowListDeserializingConverter((Deserializer) deserializer);
this.deserializerExplicitlySet = true;
this.mapper = new MessageRowMapper(this.deserializer);
}
/**
@@ -459,8 +457,7 @@ public class JdbcMessageStore extends AbstractMessageGroupStore
ps.setString(1, messageId); // NOSONAR - magic number
ps.setString(2, this.region); // NOSONAR - magic number
ps.setTimestamp(3, new Timestamp(System.currentTimeMillis())); // NOSONAR - magic number
this.lobHandler.getLobCreator().setBlobAsBytes(ps, 4, messageBytes); // NOSONAR - magic number
ps.setBytes(4, messageBytes); // NOSONAR - magic number
});
}
catch (DataIntegrityViolationException ex) {
@@ -780,23 +777,4 @@ public class JdbcMessageStore extends AbstractMessageGroupStore
return input == null ? null : UUIDConverter.getUUID(input).toString();
}
/**
* Convenience class to be used to unpack a message from a result set row. Uses column named in the result set to
* extract the required data, so that select clause ordering is unimportant.
*/
private final class MessageMapper implements RowMapper<Message<?>> {
@Override
public Message<?> mapRow(ResultSet rs, int rowNum) throws SQLException {
byte[] messageBytes = JdbcMessageStore.this.lobHandler.getBlobAsBytes(rs, "MESSAGE_BYTES");
if (messageBytes == null) {
return null;
}
else {
return (Message<?>) JdbcMessageStore.this.deserializer.convert(messageBytes);
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2024 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.
@@ -39,7 +39,7 @@ import org.springframework.util.Assert;
* <p>
* 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
* In this case the {@link #serializer} is {@code null} to avoid
* extra serialization actions if the target custom behavior doesn't imply them.
*
* @author Meherzad Lahewala
@@ -53,8 +53,6 @@ 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.
@@ -62,24 +60,35 @@ public class ChannelMessageStorePreparedStatementSetter {
* the request message
* @param lobHandler the {@link LobHandler} to store {@code byte[]} of the request
* message to prepared statement
* @deprecated since 6.4 (for removal) (if favor of {@link #ChannelMessageStorePreparedStatementSetter(SerializingConverter)})
* with a plain JDBC driver support for byte arrays.
*/
@Deprecated(forRemoval = true, since = "6.4")
public ChannelMessageStorePreparedStatementSetter(SerializingConverter serializer, LobHandler lobHandler) {
this(serializer);
}
/**
* 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
* @since 6.4
*/
public ChannelMessageStorePreparedStatementSetter(SerializingConverter serializer) {
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,
* The {@link #serializer} is {@code 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;
}
/**
@@ -91,7 +100,7 @@ public class ChannelMessageStorePreparedStatementSetter {
* <li>3 - region
* <li>4 - createdDate
* <li>5 - priority if enabled, otherwise null
* <li>6 - serialized message if {@link #serializer} and {@link #lobHandler} are provided.
* <li>6 - serialized message if {@link #serializer} is provided.
* </ul>
* An inheritor may consider to call this method for population common properties and perform
* custom message serialization logic for the parameter #6.
@@ -99,7 +108,7 @@ public class ChannelMessageStorePreparedStatementSetter {
* @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 region the region in the target table to distinguish different database clients
* @param priorityEnabled the flag to indicate if priority has to be stored
* @throws SQLException the exception throws during data population
*/
@@ -126,7 +135,7 @@ public class ChannelMessageStorePreparedStatementSetter {
if (this.serializer != null) {
byte[] messageBytes = this.serializer.convert(requestMessage);
this.lobHandler.getLobCreator().setBlobAsBytes(preparedStatement, 6, messageBytes); // NOSONAR magic number
preparedStatement.setBytes(6, messageBytes); // NOSONAR magic number
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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,6 +23,7 @@ import org.springframework.integration.support.converter.AllowListDeserializingC
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* Convenience class to be used to unpack a {@link Message} from a result set
@@ -40,22 +41,32 @@ public class MessageRowMapper implements RowMapper<Message<?>> {
private final AllowListDeserializingConverter deserializer;
private final LobHandler lobHandler;
/**
* Construct an instance based on the provided {@link AllowListDeserializingConverter}
* and {@link LobHandler}.
* @param deserializer the {@link AllowListDeserializingConverter} to use.
* @param lobHandler the {@link LobHandler} to use.
* @deprecated since 6.4 (for removal) (if favor of {@link #MessageRowMapper(AllowListDeserializingConverter)})
* with a plain JDBC driver support for byte arrays.
*/
@Deprecated(forRemoval = true, since = "6.4")
public MessageRowMapper(AllowListDeserializingConverter deserializer, LobHandler lobHandler) {
this(deserializer);
}
/**
* Construct an instance based on the provided {@link AllowListDeserializingConverter}.
* @param deserializer the {@link AllowListDeserializingConverter} to use.
* @since 6.4
*/
public MessageRowMapper(AllowListDeserializingConverter deserializer) {
Assert.notNull(deserializer, "'deserializer' must not be null");
this.deserializer = deserializer;
this.lobHandler = lobHandler;
}
@Override
public Message<?> mapRow(ResultSet rs, int rowNum) throws SQLException {
byte[] blobAsBytes = this.lobHandler.getBlobAsBytes(rs, "MESSAGE_BYTES");
byte[] blobAsBytes = rs.getBytes("MESSAGE_BYTES");
if (blobAsBytes == null) {
return null;
}

View File

@@ -32,7 +32,6 @@ import org.springframework.integration.jdbc.store.JdbcMessageStore;
import org.springframework.integration.store.MessageStore;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.messaging.Message;
import org.springframework.test.util.ReflectionTestUtils;
@@ -52,14 +51,14 @@ public class JdbcMessageStoreParserTests {
public void testSimpleMessageStoreWithDataSource() {
setUp("defaultJdbcMessageStore.xml", getClass());
MessageStore store = context.getBean("messageStore", MessageStore.class);
assertThat(store instanceof JdbcMessageStore).isTrue();
assertThat(store).isInstanceOf(JdbcMessageStore.class);
}
@Test
public void testSimpleMessageStoreWithTemplate() {
setUp("jdbcOperationsJdbcMessageStore.xml", getClass());
MessageStore store = context.getBean("messageStore", MessageStore.class);
assertThat(store instanceof JdbcMessageStore).isTrue();
assertThat(store).isInstanceOf(JdbcMessageStore.class);
}
@Test
@@ -67,9 +66,9 @@ public class JdbcMessageStoreParserTests {
setUp("serializerJdbcMessageStore.xml", getClass());
MessageStore store = context.getBean("messageStore", MessageStore.class);
Object serializer = TestUtils.getPropertyValue(store, "serializer.serializer");
assertThat(serializer instanceof EnhancedSerializer).isTrue();
assertThat(serializer).isInstanceOf(EnhancedSerializer.class);
Object deserializer = TestUtils.getPropertyValue(store, "deserializer.deserializer");
assertThat(deserializer instanceof EnhancedSerializer).isTrue();
assertThat(deserializer).isInstanceOf(EnhancedSerializer.class);
}
@Test
@@ -78,7 +77,6 @@ public class JdbcMessageStoreParserTests {
MessageStore store = context.getBean("messageStore", MessageStore.class);
assertThat(ReflectionTestUtils.getField(store, "region")).isEqualTo("FOO");
assertThat(ReflectionTestUtils.getField(store, "tablePrefix")).isEqualTo("BAR_");
assertThat(ReflectionTestUtils.getField(store, "lobHandler")).isEqualTo(context.getBean(LobHandler.class));
}
@AfterEach
@@ -99,8 +97,7 @@ public class JdbcMessageStoreParserTests {
private final Deserializer<Object> targetDeserializer = new DefaultDeserializer();
public Object deserialize(InputStream inputStream) throws IOException {
Message<?> message = (Message<?>) targetDeserializer.deserialize(inputStream);
return message;
return targetDeserializer.deserialize(inputStream);
}
public void serialize(Object object, OutputStream outputStream) throws IOException {

View File

@@ -9,12 +9,9 @@
<bean id="messageStore" class="org.springframework.integration.jdbc.store.JdbcMessageStore">
<constructor-arg ref="dataSource"/>
<property name="lobHandler" ref="lobHandler"/>
<property name="region" value="FOO"/>
<property name="tablePrefix" value="BAR_"/>
<property name="checkDatabaseOnStart" value="false"/>
</bean>
<bean id="lobHandler" class="org.springframework.jdbc.support.lob.DefaultLobHandler"/>
</beans>

View File

@@ -0,0 +1,542 @@
/*
* Copyright 2024 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.jdbc.oracle;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Properties;
import java.util.UUID;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.jdbc.store.JdbcMessageStore;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.predicate.MessagePredicate;
import org.springframework.integration.util.UUIDConverter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.Repeat;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Artem Bilan
*
* @since 6.4
*/
@SpringJUnitConfig
@DirtiesContext
public class OracleJdbcMessageStoreTests implements OracleContainerTest {
private static final Log LOG = LogFactory.getLog(OracleJdbcMessageStoreTests.class);
@Autowired
private DataSource dataSource;
private JdbcMessageStore messageStore;
@Autowired
private PlatformTransactionManager transactionManager;
@BeforeEach
public void init() {
messageStore = new JdbcMessageStore(dataSource);
messageStore.setRegion("JdbcMessageStoreTests");
}
@AfterEach
public void afterTest() {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
new TransactionTemplate(this.transactionManager).execute(status -> {
final int deletedGroupToMessageRows = jdbcTemplate.update("delete from INT_GROUP_TO_MESSAGE");
final int deletedMessages = jdbcTemplate.update("delete from INT_MESSAGE");
final int deletedMessageGroups = jdbcTemplate.update("delete from INT_MESSAGE_GROUP");
LOG.info(String.format("Cleaning Database - Deleted Messages: %s, " +
"Deleted GroupToMessage Rows: %s, Deleted Message Groups: %s",
deletedMessages, deletedGroupToMessageRows, deletedMessageGroups));
return null;
});
}
@Test
@Transactional
public void testGetNonExistent() {
Message<?> result = messageStore.getMessage(UUID.randomUUID());
assertThat(result).isNull();
}
@Test
@Transactional
public void testAddAndGet() {
Message<String> message = MessageBuilder.withPayload("foo").build();
Message<String> saved = messageStore.addMessage(message);
Message<?> result = messageStore.getMessage(saved.getHeaders().getId());
assertThat(result).isNotNull();
assertThat(saved).matches(new MessagePredicate(result));
}
@Test
@Transactional
public void testWithMessageHistory() {
Message<?> message = new GenericMessage<>("Hello");
DirectChannel fooChannel = new DirectChannel();
fooChannel.setBeanName("fooChannel");
DirectChannel barChannel = new DirectChannel();
barChannel.setBeanName("barChannel");
message = MessageHistory.write(message, fooChannel);
message = MessageHistory.write(message, barChannel);
messageStore.addMessage(message);
message = messageStore.getMessage(message.getHeaders().getId());
MessageHistory messageHistory = MessageHistory.read(message);
assertThat(messageHistory).isNotNull();
assertThat(messageHistory.size()).isEqualTo(2);
Properties fooChannelHistory = messageHistory.get(0);
assertThat(fooChannelHistory.get("name")).isEqualTo("fooChannel");
assertThat(fooChannelHistory.get("type")).isEqualTo("channel");
}
@Test
@Transactional
public void testSize() {
Message<String> message = MessageBuilder.withPayload("foo").build();
messageStore.addMessage(message);
assertThat(messageStore.getMessageCount()).isEqualTo(1);
}
@Test
@Transactional
public void testSerializer() {
// N.B. these serializers are not realistic (just for test purposes)
messageStore.setSerializer((object, outputStream) -> {
outputStream.write(object.getPayload().toString().getBytes());
outputStream.flush();
});
messageStore.setDeserializer(inputStream -> {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
return new GenericMessage<>(reader.readLine());
});
Message<String> message = MessageBuilder.withPayload("foo").build();
Message<String> saved = messageStore.addMessage(message);
assertThat(messageStore.getMessage(message.getHeaders().getId())).isNotNull();
Message<?> result = messageStore.getMessage(saved.getHeaders().getId());
assertThat(result).isNotNull();
assertThat(result.getPayload()).isEqualTo("foo");
}
@Test
@Transactional
public void testAddAndGetWithDifferentRegion() {
Message<String> message = MessageBuilder.withPayload("foo").build();
Message<String> saved = messageStore.addMessage(message);
messageStore.setRegion("FOO");
Message<?> result = messageStore.getMessage(saved.getHeaders().getId());
assertThat(result).isNull();
}
@Test
@Transactional
public void testAddAndUpdate() {
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId("X").build();
message = messageStore.addMessage(message);
message = MessageBuilder.fromMessage(message).setCorrelationId("Y").build();
message = messageStore.addMessage(message);
assertThat(new IntegrationMessageHeaderAccessor(messageStore.getMessage(message.getHeaders().getId()))
.getCorrelationId()).isEqualTo("Y");
}
@Test
@Transactional
public void testAddAndUpdateAlreadySaved() {
Message<String> message = MessageBuilder.withPayload("foo").build();
message = messageStore.addMessage(message);
Message<String> result = messageStore.addMessage(message);
assertThat(result).isEqualTo(message);
}
@Test
@Transactional
public void testAddAndUpdateAlreadySavedAndCopied() {
Message<String> message = MessageBuilder.withPayload("foo").build();
Message<String> saved = messageStore.addMessage(message);
Message<String> copy = MessageBuilder.fromMessage(saved).build();
Message<String> result = messageStore.addMessage(copy);
assertThat(result).isEqualTo(copy);
assertThat(result).isEqualTo(saved);
assertThat(messageStore.getMessage(saved.getHeaders().getId())).isNotNull();
}
@Test
@Transactional
public void testAddAndUpdateWithChange() {
Message<String> message = MessageBuilder.withPayload("foo").build();
Message<String> saved = messageStore.addMessage(message);
Message<String> copy = MessageBuilder.fromMessage(saved).setHeader("newHeader", 1).build();
Message<String> result = messageStore.addMessage(copy);
assertThat(result).isNotSameAs(saved);
assertThat(saved).matches(new MessagePredicate(result, "newHeader"));
assertThat(messageStore.getMessage(saved.getHeaders().getId())).isNotNull();
}
@Test
@Transactional
public void testAddAndRemoveMessageGroup() {
Message<String> message = MessageBuilder.withPayload("foo").build();
message = messageStore.addMessage(message);
assertThat(messageStore.removeMessage(message.getHeaders().getId())).isNotNull();
}
@Test
@Transactional
public void testAddAndGetMessageGroup() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
long now = System.currentTimeMillis();
messageStore.addMessageToGroup(groupId, message);
MessageGroup group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(1);
assertThat(group.getTimestamp() >= now).as("Timestamp too early: " + group.getTimestamp() + "<" + now).isTrue();
}
@Test
@Transactional
public void testAddAndRemoveMessageFromMessageGroup() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
messageStore.addMessageToGroup(groupId, message);
messageStore.removeMessagesFromGroup(groupId, message);
MessageGroup group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(0);
}
@Test
@Transactional
public void testRemoveMessageGroup() {
JdbcTemplate template = new JdbcTemplate(dataSource);
template.afterPropertiesSet();
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
messageStore.addMessageToGroup(groupId, message);
messageStore.removeMessageGroup(groupId);
MessageGroup group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(0);
String uuidGroupId = UUIDConverter.getUUID(groupId).toString();
assertThat(template.queryForList(
"SELECT * from INT_GROUP_TO_MESSAGE where GROUP_KEY = '" + uuidGroupId + "'").size() == 0).isTrue();
}
@Test
@Transactional
public void testCompleteMessageGroup() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
messageStore.addMessageToGroup(groupId, message);
messageStore.completeGroup(groupId);
MessageGroup group = messageStore.getMessageGroup(groupId);
assertThat(group.isComplete()).isTrue();
assertThat(group.size()).isEqualTo(1);
}
@Test
@Transactional
public void testUpdateLastReleasedSequence() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
messageStore.addMessageToGroup(groupId, message);
messageStore.setLastReleasedSequenceNumberForGroup(groupId, 5);
MessageGroup group = messageStore.getMessageGroup(groupId);
assertThat(group.getLastReleasedMessageSequenceNumber()).isEqualTo(5);
}
@Test
@Transactional
public void testMessageGroupCount() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").build();
messageStore.addMessageToGroup(groupId, message);
assertThat(messageStore.getMessageGroupCount()).isEqualTo(1);
}
@Test
@Transactional
public void testMessageGroupSizes() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").build();
messageStore.addMessageToGroup(groupId, message);
assertThat(messageStore.getMessageCountForAllMessageGroups()).isEqualTo(1);
}
@Test
@Transactional
public void testOrderInMessageGroup() throws Exception {
String groupId = "X";
messageStore.addMessageToGroup(groupId, MessageBuilder.withPayload("foo").setCorrelationId(groupId).build());
Thread.sleep(1);
messageStore.addMessageToGroup(groupId, MessageBuilder.withPayload("bar").setCorrelationId(groupId).build());
MessageGroup group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(2);
assertThat(messageStore.pollMessageFromGroup(groupId).getPayload()).isEqualTo("foo");
assertThat(messageStore.pollMessageFromGroup(groupId).getPayload()).isEqualTo("bar");
}
@Test
@Transactional
@DisabledIfEnvironmentVariable(named = "bamboo_buildKey", matches = ".*?",
disabledReason = "Timing is too short for CI")
public void testExpireMessageGroupOnCreateOnly() throws Exception {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
messageStore.addMessageToGroup(groupId, message);
messageStore.registerMessageGroupExpiryCallback(
(messageGroupStore, group) -> messageGroupStore.removeMessageGroup(group.getGroupId()));
Thread.sleep(1000);
messageStore.expireMessageGroups(2000);
MessageGroup group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(1);
messageStore.addMessageToGroup(groupId, MessageBuilder.withPayload("bar").setCorrelationId(groupId).build());
Thread.sleep(2001);
messageStore.expireMessageGroups(2000);
group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(0);
}
@Test
@Transactional
@DisabledIfEnvironmentVariable(named = "bamboo_buildKey", matches = ".*?",
disabledReason = "Timing is too short for CI")
public void testExpireMessageGroupOnIdleOnly() throws Exception {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
messageStore.setTimeoutOnIdle(true);
messageStore.addMessageToGroup(groupId, message);
messageStore.registerMessageGroupExpiryCallback(
(messageGroupStore, group) -> messageGroupStore.removeMessageGroup(group.getGroupId()));
Thread.sleep(1000);
messageStore.expireMessageGroups(2000);
MessageGroup group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(1);
Thread.sleep(2000);
messageStore.addMessageToGroup(groupId, MessageBuilder.withPayload("bar").setCorrelationId(groupId).build());
group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(2);
Thread.sleep(2000);
messageStore.expireMessageGroups(2000);
group = messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(0);
}
@Test
@Transactional
public void testMessagePollingFromTheGroup() throws Exception {
final String groupX = "X";
messageStore.addMessageToGroup(groupX, MessageBuilder.withPayload("foo").setCorrelationId(groupX).build());
Thread.sleep(100);
messageStore.addMessageToGroup(groupX, MessageBuilder.withPayload("bar").setCorrelationId(groupX).build());
Thread.sleep(100);
messageStore.addMessageToGroup(groupX, MessageBuilder.withPayload("baz").setCorrelationId(groupX).build());
Thread.sleep(100);
messageStore.addMessageToGroup("Y", MessageBuilder.withPayload("barA").setCorrelationId(groupX).build());
Thread.sleep(100);
messageStore.addMessageToGroup("Y", MessageBuilder.withPayload("bazA").setCorrelationId(groupX).build());
Thread.sleep(100);
MessageGroup group = messageStore.getMessageGroup(groupX);
assertThat(group.size()).isEqualTo(3);
Message<?> message1 = messageStore.pollMessageFromGroup(groupX);
assertThat(message1).isNotNull();
assertThat(message1.getPayload()).isEqualTo("foo");
group = messageStore.getMessageGroup(groupX);
assertThat(group.size()).isEqualTo(2);
Message<?> message2 = messageStore.pollMessageFromGroup(groupX);
assertThat(message2).isNotNull();
assertThat(message2.getPayload()).isEqualTo("bar");
group = messageStore.getMessageGroup(groupX);
assertThat(group.size()).isEqualTo(1);
}
@Test
@Transactional
@Rollback(false)
@Repeat(20)
public void testSameMessageToMultipleGroups() {
final String group1Id = "group1";
final String group2Id = "group2";
final Message<String> message = MessageBuilder.withPayload("foo").build();
final MessageBuilder<String> builder1 = MessageBuilder.fromMessage(message);
final MessageBuilder<String> builder2 = MessageBuilder.fromMessage(message);
builder1.setSequenceNumber(1);
builder2.setSequenceNumber(2);
final Message<?> message1 = builder1.build();
final Message<?> message2 = builder2.build();
messageStore.addMessageToGroup(group1Id, message1);
messageStore.addMessageToGroup(group2Id, message2);
final Message<?> messageFromGroup1 = messageStore.pollMessageFromGroup(group1Id);
final Message<?> messageFromGroup2 = messageStore.pollMessageFromGroup(group2Id);
assertThat(messageFromGroup1).isNotNull();
assertThat(messageFromGroup2).isNotNull();
LOG.info("messageFromGroup1: " + messageFromGroup1.getHeaders().getId()
+ "; Sequence #: " + new IntegrationMessageHeaderAccessor(messageFromGroup1).getSequenceNumber());
LOG.info("messageFromGroup2: " + messageFromGroup2.getHeaders().getId()
+ "; Sequence #: " + new IntegrationMessageHeaderAccessor(messageFromGroup2).getSequenceNumber());
assertThat(messageFromGroup1.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER))
.isEqualTo(1);
assertThat(messageFromGroup2.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER))
.isEqualTo(2);
}
@Test
@Transactional
@Rollback(false)
@Repeat(20)
public void testSameMessageAndGroupToMultipleRegions() {
final String groupId = "myGroup";
final String region1 = "region1";
final String region2 = "region2";
final JdbcMessageStore messageStore1 = new JdbcMessageStore(dataSource);
messageStore1.setRegion(region1);
final JdbcMessageStore messageStore2 = new JdbcMessageStore(dataSource);
messageStore1.setRegion(region2);
final Message<String> message = MessageBuilder.withPayload("foo").build();
final MessageBuilder<String> builder1 = MessageBuilder.fromMessage(message);
final MessageBuilder<String> builder2 = MessageBuilder.fromMessage(message);
builder1.setSequenceNumber(1);
builder2.setSequenceNumber(2);
final Message<?> message1 = builder1.build();
final Message<?> message2 = builder2.build();
messageStore1.addMessageToGroup(groupId, message1);
messageStore2.addMessageToGroup(groupId, message2);
final Message<?> messageFromRegion1 = messageStore1.pollMessageFromGroup(groupId);
final Message<?> messageFromRegion2 = messageStore2.pollMessageFromGroup(groupId);
assertThat(messageFromRegion1).isNotNull();
assertThat(messageFromRegion2).isNotNull();
LOG.info("messageFromRegion1: " + messageFromRegion1.getHeaders().getId()
+ "; Sequence #: " + new IntegrationMessageHeaderAccessor(messageFromRegion1).getSequenceNumber());
LOG.info("messageFromRegion2: " + messageFromRegion2.getHeaders().getId()
+ "; Sequence #: " + new IntegrationMessageHeaderAccessor(messageFromRegion2).getSequenceNumber());
assertThat(messageFromRegion1.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER))
.isEqualTo(1);
assertThat(messageFromRegion2.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER))
.isEqualTo(2);
}
@Test
public void testMessageGroupCondition() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").build();
this.messageStore.addMessagesToGroup(groupId, message);
this.messageStore.setGroupCondition(groupId, "testCondition");
assertThat(this.messageStore.getMessageGroup(groupId).getCondition()).isEqualTo("testCondition");
}
@Test
public void sameMessageInTwoGroupsNotRemovedByFirstGroup() {
GenericMessage<String> testMessage = new GenericMessage<>("test data");
messageStore.addMessageToGroup("1", testMessage);
messageStore.addMessageToGroup("2", testMessage);
messageStore.removeMessageGroup("1");
assertThat(messageStore.getMessageCount()).isEqualTo(1);
messageStore.removeMessageGroup("2");
assertThat(messageStore.getMessageCount()).isEqualTo(0);
}
@Test
public void removeMessagesFromGroupDontRemoveSameMessageInOtherGroup() {
GenericMessage<String> testMessage = new GenericMessage<>("test data");
messageStore.addMessageToGroup("1", testMessage);
messageStore.addMessageToGroup("2", testMessage);
messageStore.removeMessagesFromGroup("1", testMessage);
assertThat(messageStore.getMessageCount()).isEqualTo(1);
assertThat(messageStore.messageGroupSize("1")).isEqualTo(0);
assertThat(messageStore.messageGroupSize("2")).isEqualTo(1);
}
@Configuration(proxyBeanMethods = false)
public static class Config {
@Bean
DataSource dataSource() {
return OracleContainerTest.dataSource();
}
@Bean
PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,8 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.integration.jdbc.store.JdbcChannelMessageStore;
import org.springframework.integration.support.MessageBuilder;
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.junit.jupiter.SpringJUnitConfig;
@@ -135,14 +133,12 @@ public abstract class AbstractJdbcChannelMessageStoreTests {
private SerializingConverter serializer = new SerializingConverter();
private LobHandler lobHandler = new DefaultLobHandler();
@Override
public void setValues(PreparedStatement preparedStatement, Message<?> requestMessage, Object groupId,
String region, boolean priorityEnabled) throws SQLException {
super.setValues(preparedStatement, requestMessage, groupId, region, priorityEnabled);
byte[] messageBytes = this.serializer.convert(requestMessage);
this.lobHandler.getLobCreator().setBlobAsBytes(preparedStatement, 6, messageBytes);
preparedStatement.setBytes(6, messageBytes);
}
};

View File

@@ -46,11 +46,10 @@ The following example shows some other optional attributes:
[source,xml]
----
<int-jdbc:message-store id="messageStore" data-source="dataSource"
lob-handler="lobHandler" table-prefix="MY_INT_"/>
<int-jdbc:message-store id="messageStore" data-source="dataSource" table-prefix="MY_INT_"/>
----
In the preceding example, we have specified a `LobHandler` for dealing with messages as large objects (which is often necessary for Oracle) and a prefix for the table names in the queries generated by the store.
In the preceding example, we have specified a prefix for the table names in the queries generated by the store.
The table name prefix defaults to `INT_`.
[[jdbc-message-store-channels]]

View File

@@ -23,4 +23,11 @@ In general the project has been moved to the latest dependency versions.
=== Remote File Adapters Changes
The `AbstractRemoteFileStreamingMessageSource` has now a convenient `clearFetchedCache()` API to remove references from cache for not processed remote files.
The references stay in cache because polling configuration does not allow to process all the fetched in one cycle, and the target `SessionFactory` might be changed between polling cycles, e.g. via `RotatingServerAdvice`.
The references stay in cache because polling configuration does not allow to process all the fetched in one cycle, and the target `SessionFactory` might be changed between polling cycles, e.g. via `RotatingServerAdvice`.
[[x6.4-jdbc-changes]]
=== JDBC Changes
The `LobHandler` (and respective API) has been deprecated for removal in Spring Framework `6.2`.
Respective option on `JdbcMessageStore` (and similar) have been deprecated as well.
The byte array handling for serialized message is fully deferred to JDBC driver.