GH-8680: Check DB for table on start (#8690)

* GH-8680: Check DB for table on start

Fixes https://github.com/spring-projects/spring-integration/issues/8680

If database is not initialized properly before application start, we may lose messages
at runtime when we fail to insert data into DB

* Implement `SmartLifecycle` on `JdbcMessageStore`, `JdbcChannelMessageStore`,
`JdbcMetadataStore`, and `DefaultLockRepository` to perform `SELECT COUNT` query in `start()`
to fail fast if no required table is present.
* Refactor `AbstractJdbcChannelMessageStoreTests` into JUnit 5 and use `MySqlContainerTest`
for more coverage
* Fix newly failed tests which had DB not initialized before
* Exclude `commons-logging` from `commons-dbcp2` dependency to avoid
classpath conflict
* Document the new feature

* * Fix HTTP URL in the `DataSource-mysql-context.xml`

* Fix language in docs

Co-authored-by: Gary Russell <grussell@vmware.com>

* * Add `setCheckDatabaseOnStart(false)` to disable the check query for all the SI JDBC components

* Fix language in Javadocs

Co-authored-by: Gary Russell <grussell@vmware.com>

---------

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2023-07-28 14:47:49 -04:00
committed by GitHub
parent 0c7d40d1e2
commit 7dcc0bb125
25 changed files with 414 additions and 104 deletions

View File

@@ -740,7 +740,9 @@ project('spring-integration-jdbc') {
testImplementation "org.apache.derby:derbyclient:$derbyVersion"
testImplementation "org.postgresql:postgresql:$postgresVersion"
testImplementation "mysql:mysql-connector-java:$mysqlVersion"
testImplementation "org.apache.commons:commons-dbcp2:$commonsDbcp2Version"
testImplementation ("org.apache.commons:commons-dbcp2:$commonsDbcp2Version") {
exclude group: 'commons-logging'
}
testImplementation 'org.testcontainers:mysql'
testImplementation 'org.testcontainers:postgresql'

View File

@@ -20,6 +20,7 @@ import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sql.DataSource;
@@ -29,6 +30,8 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.log.LogAccessor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@@ -45,6 +48,12 @@ import org.springframework.util.Assert;
* Otherwise, it opens a possibility to break {@link java.util.concurrent.locks.Lock} contract,
* where {@link JdbcLockRegistry} uses non-shared {@link java.util.concurrent.locks.ReentrantLock}s
* for local synchronizations.
* <p>
* This class implements {@link SmartLifecycle} and calls
* {@code SELECT COUNT(REGION) FROM %sLOCK} query
* according to the provided prefix on {@link #start()} to check if required table is present in DB.
* The application context will fail to start if the table is not present.
* This check can be disabled via {@link #setCheckDatabaseOnStart(boolean)}.
*
* @author Dave Syer
* @author Artem Bilan
@@ -56,7 +65,10 @@ import org.springframework.util.Assert;
* @since 4.3
*/
public class DefaultLockRepository
implements LockRepository, InitializingBean, ApplicationContextAware, SmartInitializingSingleton {
implements LockRepository, InitializingBean, ApplicationContextAware, SmartInitializingSingleton,
SmartLifecycle {
private static final LogAccessor LOGGER = new LogAccessor(DefaultLockRepository.class);
/**
* Default value for the table prefix property.
@@ -72,6 +84,8 @@ public class DefaultLockRepository
private final JdbcTemplate template;
private final AtomicBoolean started = new AtomicBoolean();
private Duration ttl = DEFAULT_TTL;
private String prefix = DEFAULT_TABLE_PREFIX;
@@ -116,6 +130,10 @@ public class DefaultLockRepository
WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?
""";
private String countAllQuery = """
SELECT COUNT(REGION) FROM %sLOCK
""";
private ApplicationContext applicationContext;
private PlatformTransactionManager transactionManager;
@@ -126,6 +144,8 @@ public class DefaultLockRepository
private TransactionTemplate serializableTransactionTemplate;
private boolean checkDatabaseOnStart = true;
/**
* Constructor that initializes the client id that will be associated for
* all the locks persisted by the store instance to a random {@link UUID}.
@@ -293,6 +313,7 @@ public class DefaultLockRepository
this.insertQuery = String.format(this.insertQuery, this.prefix);
this.countQuery = String.format(this.countQuery, this.prefix);
this.renewQuery = String.format(this.renewQuery, this.prefix);
this.countAllQuery = String.format(this.countAllQuery, this.prefix);
}
@Override
@@ -325,6 +346,41 @@ public class DefaultLockRepository
this.serializableTransactionTemplate = new TransactionTemplate(this.transactionManager, transactionDefinition);
}
/**
* The flag to perform a database check query on start or not.
* @param checkDatabaseOnStart false to not perform the database check.
* @since 6.2
*/
public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) {
this.checkDatabaseOnStart = checkDatabaseOnStart;
if (!checkDatabaseOnStart) {
LOGGER.info("The 'DefaultLockRepository' won't be started automatically " +
"and required table is not going be checked.");
}
}
@Override
public boolean isAutoStartup() {
return this.checkDatabaseOnStart;
}
@Override
public void start() {
if (this.started.compareAndSet(false, true) && this.checkDatabaseOnStart) {
this.template.queryForObject(this.countAllQuery, Integer.class); // If no table in DB, an exception is thrown
}
}
@Override
public void stop() {
this.started.set(false);
}
@Override
public boolean isRunning() {
return this.started.get();
}
@Override
public void close() {
this.defaultTransactionTemplate.executeWithoutResult(

View File

@@ -16,9 +16,13 @@
package org.springframework.integration.jdbc.metadata;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.log.LogAccessor;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.integration.metadata.ConcurrentMetadataStore;
@@ -34,6 +38,12 @@ import org.springframework.util.Assert;
* where <code>*</code> is the target database type.
* <p>
* The transaction management is required to use this {@link ConcurrentMetadataStore}.
* <p>
* This class implements {@link SmartLifecycle} and calls
* {@code SELECT COUNT(METADATA_KEY) FROM %sMETADATA_STORE} query
* according to the provided prefix on {@link #start()} to check if required table is present in DB.
* The application context will fail to start if the table is not present.
* This check can be disabled via {@link #setCheckDatabaseOnStart(boolean)}.
*
* @author Bojan Vukasovic
* @author Artem Bilan
@@ -41,7 +51,9 @@ import org.springframework.util.Assert;
*
* @since 5.0
*/
public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingBean {
public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingBean, SmartLifecycle {
private static final LogAccessor LOGGER = new LogAccessor(JdbcMetadataStore.class);
private static final String KEY_CANNOT_BE_NULL = "'key' cannot be null";
@@ -52,6 +64,8 @@ public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingB
private final JdbcOperations jdbcTemplate;
private final AtomicBoolean started = new AtomicBoolean();
private String tablePrefix = DEFAULT_TABLE_PREFIX;
private String region = "DEFAULT";
@@ -93,6 +107,12 @@ public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingB
HAVING COUNT(*)=0
""";
private String countQuery = """
SELECT COUNT(METADATA_KEY) FROM %sMETADATA_STORE
""";
private boolean checkDatabaseOnStart = true;
/**
* Instantiate a {@link JdbcMetadataStore} using provided dataSource {@link DataSource}.
* @param dataSource a {@link DataSource}
@@ -137,7 +157,7 @@ public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingB
* Specify a row lock hint for the query in the lock-based operations.
* Defaults to {@code FOR UPDATE}. Can be specified as an empty string,
* if the target RDBMS doesn't support locking on tables from queries.
* The value depends from RDBMS vendor, e.g. SQL Server requires {@code WITH (ROWLOCK)}.
* The value depends on the RDBMS vendor, e.g. SQL Server requires {@code WITH (ROWLOCK)}.
* @param lockHint the RDBMS vendor-specific lock hint.
* @since 5.0.7
*/
@@ -154,6 +174,42 @@ public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingB
this.replaceValueByKeyQuery = String.format(this.replaceValueByKeyQuery, this.tablePrefix);
this.removeValueQuery = String.format(this.removeValueQuery, this.tablePrefix);
this.putIfAbsentValueQuery = String.format(this.putIfAbsentValueQuery, this.tablePrefix, this.tablePrefix);
this.countQuery = String.format(this.putIfAbsentValueQuery, this.tablePrefix);
}
/**
* The flag to perform a database check query on start or not.
* @param checkDatabaseOnStart false to not perform the database check.
* @since 6.2
*/
public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) {
this.checkDatabaseOnStart = checkDatabaseOnStart;
if (!checkDatabaseOnStart) {
LOGGER.info("The 'DefaultLockRepository' won't be started automatically " +
"and required table is not going be checked.");
}
}
@Override
public boolean isAutoStartup() {
return this.checkDatabaseOnStart;
}
@Override
public void start() {
if (this.started.compareAndSet(false, true) && this.checkDatabaseOnStart) {
this.jdbcTemplate.queryForObject(this.countQuery, Integer.class); // If no table in DB, an exception is thrown
}
}
@Override
public void stop() {
this.started.set(false);
}
@Override
public boolean isRunning() {
return this.started.get();
}
@Override
@@ -162,7 +218,7 @@ public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingB
Assert.notNull(key, KEY_CANNOT_BE_NULL);
Assert.notNull(value, "'value' cannot be null");
while (true) {
//try to insert if does not exists
//try to insert if the entry does not exist
int affectedRows = tryToPutIfAbsent(key, value);
if (affectedRows > 0) {
//it was not in the table, so we have just inserted
@@ -218,7 +274,7 @@ public class JdbcMetadataStore implements ConcurrentMetadataStore, InitializingB
Assert.notNull(key, KEY_CANNOT_BE_NULL);
Assert.notNull(value, "'value' cannot be null");
while (true) {
//try to insert if does not exist, if exists we will try to update it
//try to insert if the entry does not exist, if it exists we will try to update it
int affectedRows = tryToPutIfAbsent(key, value);
if (affectedRows == 0) {
//since value is not inserted, means it is already present

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -31,6 +32,7 @@ import java.util.function.Supplier;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.log.LogAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.core.serializer.Deserializer;
@@ -73,6 +75,11 @@ import org.springframework.util.StringUtils;
* The SQL scripts for creating the table are packaged
* under {@code org/springframework/integration/jdbc/schema-*.sql},
* where {@code *} denotes the target database type.
* <p>
* This class implements {@link SmartLifecycle} and calls {@link #getMessageGroupCount()}
* on {@link #start()} to check if required table is present in DB.
* The application context will fail to start if the table is not present.
* This check can be disabled via {@link #setCheckDatabaseOnStart(boolean)}.
*
* @author Gunnar Hillert
* @author Artem Bilan
@@ -83,7 +90,7 @@ import org.springframework.util.StringUtils;
* @since 2.2
*/
@ManagedResource
public class JdbcChannelMessageStore implements PriorityCapableChannelMessageStore, InitializingBean {
public class JdbcChannelMessageStore implements PriorityCapableChannelMessageStore, InitializingBean, SmartLifecycle {
private static final LogAccessor LOGGER = new LogAccessor(JdbcChannelMessageStore.class);
@@ -121,6 +128,8 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
private final Lock idCacheWriteLock = this.idCacheLock.writeLock();
private final AtomicBoolean started = new AtomicBoolean();
private ChannelMessageStoreQueryProvider channelMessageStoreQueryProvider;
private String region = DEFAULT_REGION;
@@ -145,6 +154,8 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
private boolean priorityEnabled;
private boolean checkDatabaseOnStart = true;
/**
* Convenient constructor for configuration use.
*/
@@ -411,6 +422,41 @@ public class JdbcChannelMessageStore implements PriorityCapableChannelMessageSto
this.jdbcTemplate.afterPropertiesSet();
}
/**
* The flag to perform a database check query on start or not.
* @param checkDatabaseOnStart false to not perform the database check.
* @since 6.2
*/
public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) {
this.checkDatabaseOnStart = checkDatabaseOnStart;
if (!checkDatabaseOnStart) {
LOGGER.info("The 'DefaultLockRepository' won't be started automatically " +
"and required table is not going be checked.");
}
}
@Override
public boolean isAutoStartup() {
return this.checkDatabaseOnStart;
}
@Override
public void start() {
if (this.started.compareAndSet(false, true) && this.checkDatabaseOnStart) {
getMessageGroupCount(); // If no table in DB, an exception is thrown
}
}
@Override
public void stop() {
this.started.set(false);
}
@Override
public boolean isRunning() {
return this.started.get();
}
/**
* Store a message in the database. The groupId identifies the channel for which
* the message is to be stored.

View File

@@ -26,11 +26,13 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.serializer.Deserializer;
import org.springframework.core.serializer.Serializer;
import org.springframework.core.serializer.support.SerializingConverter;
@@ -66,6 +68,11 @@ import org.springframework.util.StringUtils;
* please consider using the channel-specific {@link JdbcChannelMessageStore} instead.
* This implementation is intended for correlation components (e.g. {@code <aggregator>}),
* {@code <delayer>} and similar.
* <p>
* This class implements {@link SmartLifecycle} and calls {@link #getMessageGroupCount()}
* on {@link #start()} to check if required tables are present in DB.
* The application context will fail to start if the table is not present.
* This check can be disabled via {@link #setCheckDatabaseOnStart(boolean)}.
*
* @author Dave Syer
* @author Oleg Zhurakousky
@@ -77,7 +84,8 @@ import org.springframework.util.StringUtils;
*
* @since 2.0
*/
public class JdbcMessageStore extends AbstractMessageGroupStore implements MessageStore, BeanClassLoaderAware {
public class JdbcMessageStore extends AbstractMessageGroupStore
implements MessageStore, BeanClassLoaderAware, SmartLifecycle {
/**
* Default value for the table prefix property.
@@ -234,6 +242,8 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
private final Map<Query, String> queryCache = new ConcurrentHashMap<>();
private final AtomicBoolean started = new AtomicBoolean();
private String region = "DEFAULT";
private String tablePrefix = DEFAULT_TABLE_PREFIX;
@@ -247,6 +257,8 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
private LobHandler lobHandler = new DefaultLobHandler();
private boolean checkDatabaseOnStart = true;
/**
* Create a {@link MessageStore} with all mandatory properties.
* @param dataSource a {@link DataSource}
@@ -331,6 +343,41 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
this.deserializer.addAllowedPatterns(patterns);
}
/**
* The flag to perform a database check query on start or not.
* @param checkDatabaseOnStart false to not perform the database check.
* @since 6.2
*/
public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) {
this.checkDatabaseOnStart = checkDatabaseOnStart;
if (!checkDatabaseOnStart) {
logger.info("The 'DefaultLockRepository' won't be started automatically " +
"and required table is not going be checked.");
}
}
@Override
public boolean isAutoStartup() {
return this.checkDatabaseOnStart;
}
@Override
public void start() {
if (this.started.compareAndSet(false, true) && this.checkDatabaseOnStart) {
getMessageGroupCount(); // If no table in DB, an exception is thrown
}
}
@Override
public void stop() {
this.started.set(false);
}
@Override
public boolean isRunning() {
return this.started.get();
}
@Override
public Message<?> removeMessage(UUID id) {
Message<?> message = getMessage(id);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 the original author or authors.
* Copyright 2016-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +20,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.junit.After;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.serializer.DefaultDeserializer;
@@ -81,7 +81,7 @@ public class JdbcMessageStoreParserTests {
assertThat(ReflectionTestUtils.getField(store, "lobHandler")).isEqualTo(context.getBean(LobHandler.class));
}
@After
@AfterEach
public void tearDown() {
if (context != null) {
context.close();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,8 +19,8 @@ package org.springframework.integration.jdbc.config;
import java.sql.Types;
import java.util.List;
import org.junit.After;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ConfigurableApplicationContext;
@@ -42,6 +42,8 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Gunnar Hillert
* @author Gary Russell
* @author Artem Bilan
*
* @since 2.1
*
*/
@@ -54,7 +56,7 @@ public class StoredProcMessageHandlerParserTests {
private static volatile int adviceCalled;
@Test
public void testProcedureNameIsSet() throws Exception {
public void testProcedureNameIsSet() {
setUp("basicStoredProcOutboundChannelAdapterTest.xml", getClass());
DirectFieldAccessor accessor = new DirectFieldAccessor(this.consumer);
@@ -72,7 +74,7 @@ public class StoredProcMessageHandlerParserTests {
@SuppressWarnings("unchecked")
@Test
public void testProcedurepParametersAreSet() throws Exception {
public void testProcedureParametersAreSet() {
setUp("basicStoredProcOutboundChannelAdapterTest.xml", getClass());
DirectFieldAccessor accessor = new DirectFieldAccessor(this.consumer);
@@ -114,7 +116,7 @@ public class StoredProcMessageHandlerParserTests {
@SuppressWarnings("unchecked")
@Test
public void testSqlParametersAreSet() throws Exception {
public void testSqlParametersAreSet() {
setUp("basicStoredProcOutboundChannelAdapterTest.xml", getClass());
DirectFieldAccessor accessor = new DirectFieldAccessor(this.consumer);
@@ -161,15 +163,15 @@ public class StoredProcMessageHandlerParserTests {
}
@Test
public void adviceCalled() throws Exception {
public void adviceCalled() {
setUp("advisedStoredProcOutboundChannelAdapterTest.xml", getClass());
MessageHandler handler = TestUtils.getPropertyValue(this.consumer, "handler", MessageHandler.class);
handler.handleMessage(new GenericMessage<String>("foo"));
handler.handleMessage(new GenericMessage<>("foo"));
assertThat(adviceCalled).isEqualTo(1);
}
@After
@AfterEach
public void tearDown() {
if (context != null) {
context.close();

View File

@@ -12,9 +12,7 @@
<int:channel id="target"/>
<jdbc:embedded-database id="dataSource" type="HSQL"/>
<int-jdbc:message-store id="messageStore" data-source="dataSource"/>
<int-jdbc:stored-proc-outbound-channel-adapter id="storedProcedureOutboundChannelAdapter"
<int-jdbc:stored-proc-outbound-channel-adapter id="storedProcedureOutboundChannelAdapter"
data-source="dataSource" channel="target"
stored-procedure-name="testProcedure1">
<int-jdbc:sql-parameter-definition name="username" direction="IN" type="VARCHAR"/>

View File

@@ -1,30 +1,29 @@
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/jdbc https://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<int:channel id="target"/>
<jdbc:embedded-database id="dataSource" type="HSQL"/>
<int-jdbc:message-store id="messageStore" data-source="dataSource"/>
<int:channel id="target"/>
<int-jdbc:stored-proc-outbound-channel-adapter id="storedProcedureOutboundChannelAdapter"
data-source="dataSource" channel="target"
stored-procedure-name="testProcedure1">
<int-jdbc:sql-parameter-definition name="username" direction="IN" type="VARCHAR"/>
<int-jdbc:sql-parameter-definition name="password" direction="OUT" />
<int-jdbc:sql-parameter-definition name="age" direction="INOUT" type="INTEGER" scale="5"/>
<int-jdbc:sql-parameter-definition name="description" />
<int-jdbc:parameter name="username" value="kenny" type="java.lang.String"/>
<int-jdbc:parameter name="description" value="Who killed Kenny?"/>
<int-jdbc:parameter name="password" expression="payload.username"/>
<int-jdbc:parameter name="age" value="30" type="java.lang.Integer"/>
<jdbc:embedded-database id="dataSource" type="HSQL"/>
<int-jdbc:stored-proc-outbound-channel-adapter id="storedProcedureOutboundChannelAdapter"
data-source="dataSource" channel="target"
stored-procedure-name="testProcedure1">
<int-jdbc:sql-parameter-definition name="username" direction="IN" type="VARCHAR"/>
<int-jdbc:sql-parameter-definition name="password" direction="OUT"/>
<int-jdbc:sql-parameter-definition name="age" direction="INOUT" type="INTEGER" scale="5"/>
<int-jdbc:sql-parameter-definition name="description"/>
<int-jdbc:parameter name="username" value="kenny" type="java.lang.String"/>
<int-jdbc:parameter name="description" value="Who killed Kenny?"/>
<int-jdbc:parameter name="password" expression="payload.username"/>
<int-jdbc:parameter name="age" value="30" type="java.lang.Integer"/>
</int-jdbc:stored-proc-outbound-channel-adapter>
</beans>

View File

@@ -7,7 +7,10 @@
http://www.springframework.org/schema/integration/jdbc https://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<jdbc:embedded-database id="dataSource" type="HSQL"/>
<jdbc:embedded-database id="dataSource">
<jdbc:script location="org/springframework/integration/jdbc/schema-drop-h2.sql"/>
<jdbc:script location="org/springframework/integration/jdbc/schema-h2.sql" />
</jdbc:embedded-database>
<int-jdbc:message-store id="messageStore" data-source="dataSource"/>

View File

@@ -7,8 +7,11 @@
http://www.springframework.org/schema/integration/jdbc https://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<jdbc:embedded-database id="dataSource" type="HSQL"/>
<jdbc:embedded-database id="dataSource">
<jdbc:script location="org/springframework/integration/jdbc/schema-drop-h2.sql"/>
<jdbc:script location="org/springframework/integration/jdbc/schema-h2.sql" />
</jdbc:embedded-database>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg ref="dataSource"/>
</bean>

View File

@@ -5,7 +5,10 @@
http://www.springframework.org/schema/integration/jdbc https://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<jdbc:embedded-database id="dataSource" type="HSQL" />
<jdbc:embedded-database id="dataSource">
<jdbc:script location="org/springframework/integration/jdbc/schema-drop-h2.sql"/>
<jdbc:script location="org/springframework/integration/jdbc/schema-h2.sql" />
</jdbc:embedded-database>
<bean id="serializer" class="org.springframework.integration.jdbc.config.JdbcMessageStoreParserTests$EnhancedSerializer" />

View File

@@ -2,15 +2,19 @@
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/integration/jdbc https://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<jdbc:embedded-database id="dataSource" type="HSQL"/>
<int-jdbc:message-store id="messageStore" data-source="dataSource" lob-handler="lobHandler" region="FOO" table-prefix="BAR_"/>
<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

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 the original author or authors.
* Copyright 2016-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,17 +29,20 @@ import java.util.concurrent.locks.Lock;
import javax.sql.DataSource;
import org.h2.jdbc.JdbcSQLSyntaxErrorException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.integration.util.UUIDConverter;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.PlatformTransactionManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -68,6 +71,9 @@ public class JdbcLockRegistryTests {
@Autowired
private DataSource dataSource;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private ApplicationContext context;
@@ -483,6 +489,20 @@ public class JdbcLockRegistryTests {
toUUID("foo:5"));
}
@Test
void noTableThrowsExceptionOnStart() {
try (TestUtils.TestApplicationContext testApplicationContext = TestUtils.createTestApplicationContext()) {
DefaultLockRepository client = new DefaultLockRepository(this.dataSource);
client.setPrefix("TEST_");
client.setTransactionManager(this.transactionManager);
testApplicationContext.registerBean("client", client);
assertThatExceptionOfType(ApplicationContextException.class)
.isThrownBy(testApplicationContext::refresh)
.withRootCauseExactlyInstanceOf(JdbcSQLSyntaxErrorException.class)
.withStackTraceContaining("Table \"TEST_LOCK\" not found");
}
}
@SuppressWarnings("unchecked")
private static Map<String, Lock> getRegistryLocks(JdbcLockRegistry registry) {
return TestUtils.getPropertyValue(registry, "locks", Map.class);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 the original author or authors.
* Copyright 2017-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,15 +18,19 @@ package org.springframework.integration.jdbc.metadata;
import javax.sql.DataSource;
import org.apache.derby.shared.common.error.StandardException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContextException;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Bojan Vukasovic
@@ -105,4 +109,17 @@ public class JdbcMetadataStoreTests {
assertThat(bar).isEqualTo("bar");
}
@Test
void noTableThrowsExceptionOnStart() {
try (TestUtils.TestApplicationContext testApplicationContext = TestUtils.createTestApplicationContext()) {
JdbcMetadataStore jdbcMetadataStore = new JdbcMetadataStore(this.dataSource);
jdbcMetadataStore.setTablePrefix("TEST_");
testApplicationContext.registerBean("jdbcMetadataStore", jdbcMetadataStore);
assertThatExceptionOfType(ApplicationContextException.class)
.isThrownBy(testApplicationContext::refresh)
.withRootCauseExactlyInstanceOf(StandardException.class)
.withStackTraceContaining("Table/View 'TEST_METADATA_STORE' does not exist");
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 the original author or authors.
* Copyright 2021-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,9 @@
package org.springframework.integration.jdbc.mysql;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.junit.jupiter.api.BeforeAll;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -57,4 +60,13 @@ public interface MySqlContainerTest {
return MY_SQL_CONTAINER.getPassword();
}
static DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(getDriverClassName());
dataSource.setUrl(getJdbcUrl());
dataSource.setUsername(getUsername());
dataSource.setPassword(getPassword());
return dataSource;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import java.util.UUID;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.AfterEach;
@@ -509,12 +508,7 @@ public class MySqlJdbcMessageStoreTests implements MySqlContainerTest {
@Bean
DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(MySqlContainerTest.getDriverClassName());
dataSource.setUrl(MySqlContainerTest.getJdbcUrl());
dataSource.setUsername(MySqlContainerTest.getUsername());
dataSource.setPassword(MySqlContainerTest.getPassword());
return dataSource;
return MySqlContainerTest.dataSource();
}
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,11 +37,13 @@ import org.apache.commons.dbcp2.PoolingDataSource;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.hsqldb.HsqlException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
@@ -51,6 +53,7 @@ import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.predicate.MessagePredicate;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.integration.util.UUIDConverter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
@@ -62,6 +65,7 @@ import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Dave Syer
@@ -577,6 +581,19 @@ public class JdbcMessageStoreTests {
pooledMessageStore.removeMessageGroup(groupId);
}
@Test
void noTableThrowsExceptionOnStart() {
try (TestUtils.TestApplicationContext testApplicationContext = TestUtils.createTestApplicationContext()) {
JdbcMessageStore jdbcMessageStore = new JdbcMessageStore(this.dataSource);
jdbcMessageStore.setTablePrefix("TEST_");
testApplicationContext.registerBean("jdbcMessageStore", jdbcMessageStore);
assertThatExceptionOfType(ApplicationContextException.class)
.isThrownBy(testApplicationContext::refresh)
.withRootCauseExactlyInstanceOf(HsqlException.class)
.withStackTraceContaining("user lacks privilege or object not found: TEST_MESSAGE_GROUP");
}
}
public void methodForCollectionOfPayloads(Collection<String> payloads) {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,9 +21,8 @@ import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.serializer.support.SerializingConverter;
@@ -33,7 +32,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.SpringRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
@@ -50,13 +49,13 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Artem Bilan
*/
@RunWith(SpringRunner.class)
@SpringJUnitConfig
@DirtiesContext
public abstract class AbstractJdbcChannelMessageStoreTests {
protected static final String TEST_MESSAGE_GROUP = "AbstractJdbcChannelMessageStoreTests";
private static final String REGION = "AbstractJdbcChannelMessageStoreTests";
protected static final String REGION = "AbstractJdbcChannelMessageStoreTests";
@Autowired
protected DataSource dataSource;
@@ -69,8 +68,8 @@ public abstract class AbstractJdbcChannelMessageStoreTests {
@Autowired
protected ChannelMessageStoreQueryProvider queryProvider;
@Before
public void init() throws Exception {
@BeforeEach
public void init() {
messageStore = new JdbcChannelMessageStore(dataSource);
messageStore.setRegion(REGION);
messageStore.setChannelMessageStoreQueryProvider(queryProvider);

View File

@@ -1,16 +1,11 @@
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
<!-- <property name="defaultTimeout" value="2000" /> -->
<!-- Postgres 9 does not support send timeouts. You may see an exception like:
"java.sql.SQLFeatureNotSupportedException: Method org.postgresql.jdbc4.Jdbc4PreparedStatement.setQueryTimeout(int) is not yet implemented."
-->
<property name="defaultTimeout" value="2000" />
</bean>
</beans>

View File

@@ -1,22 +1,22 @@
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="
http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<import resource="classpath:org/springframework/integration/jdbc/store/channel/DataSource-common-context.xml" />
<import resource="classpath:org/springframework/integration/jdbc/store/channel/DataSource-common-context.xml"/>
<bean id="dataSource" destroy-method="close"
class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/test" />
<property name="username" value="root" />
<property name="password" value="" />
<property name="initialSize" value="10" />
</bean>
<bean id="dataSource" class="org.springframework.integration.jdbc.mysql.MySqlContainerTest"
factory-method="dataSource"/>
<bean id="queryProvider" class="org.springframework.integration.jdbc.store.channel.MySqlChannelMessageStoreQueryProvider"/>
<jdbc:initialize-database ignore-failures="ALL">
<jdbc:script location="classpath:org/springframework/integration/jdbc/schema-drop-mysql.sql" />
<jdbc:script location="classpath:org/springframework/integration/jdbc/schema-mysql.sql" />
</jdbc:initialize-database>
<bean id="queryProvider"
class="org.springframework.integration.jdbc.store.channel.MySqlChannelMessageStoreQueryProvider"/>
</beans>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,37 @@
package org.springframework.integration.jdbc.store.channel;
import org.h2.jdbc.JdbcSQLSyntaxErrorException;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContextException;
import org.springframework.integration.jdbc.store.JdbcChannelMessageStore;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Gunnar Hillert
* @author Manuel Jordan
* @since 4.3
*/
@ContextConfiguration
public class H2JdbcChannelMessageStoreTests extends AbstractJdbcChannelMessageStoreTests {
@Test
void noTableThrowsExceptionOnStart() {
try (TestUtils.TestApplicationContext testApplicationContext = TestUtils.createTestApplicationContext()) {
JdbcChannelMessageStore jdbcChannelMessageStore = new JdbcChannelMessageStore(this.dataSource);
jdbcChannelMessageStore.setTablePrefix("TEST_");
jdbcChannelMessageStore.setRegion(REGION);
jdbcChannelMessageStore.setChannelMessageStoreQueryProvider(this.queryProvider);
testApplicationContext.registerBean("jdbcChannelMessageStore", jdbcChannelMessageStore);
assertThatExceptionOfType(ApplicationContextException.class)
.isThrownBy(testApplicationContext::refresh)
.withRootCauseExactlyInstanceOf(JdbcSQLSyntaxErrorException.class)
.withStackTraceContaining("Table \"TEST_CHANNEL_MESSAGE\" not found");
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,15 +16,15 @@
package org.springframework.integration.jdbc.store.channel;
import org.junit.Ignore;
import org.springframework.integration.jdbc.mysql.MySqlContainerTest;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Gunnar Hillert
* @author Artem Bilan
*/
@Ignore
@ContextConfiguration
public class MySqlJdbcChannelMessageStoreTests extends AbstractJdbcChannelMessageStoreTests {
public class MySqlJdbcChannelMessageStoreTests extends AbstractJdbcChannelMessageStoreTests
implements MySqlContainerTest {
}

View File

@@ -386,6 +386,7 @@ Furthermore, the index for `PriorityChannel` is commented out because it is not
NOTE: When using the `OracleChannelMessageStoreQueryProvider`, the priority channel index **must** be added because it is included in a hint in the query.
[[jdbc-db-init]]
==== Initializing the Database
Before starting to use JDBC message store components, you should provision a target database with the appropriate objects.
@@ -397,6 +398,10 @@ A common way to use these scripts is to reference them in a https://docs.spring.
Note that the scripts are provided as samples and as specifications of the required table and column names.
You may find that you need to enhance them for production use (for, example, by adding index declarations).
Starting with version 6.2, the `JdbcMessageStore`, `JdbcChannelMessageStore`, `JdbcMetadataStore`, and `DefaultLockRepository` implement `SmartLifecycle` and perform a`SELECT COUNT` query, on their respective tables, in the `start()` method to ensure that the required table (according to the provided prefix) is present in the target database.
If the required table does not exist, the application context fails to start.
The check can be disabled via `setCheckDatabaseOnStart(false)`.
[[jdbc-message-store-generic]]
==== The Generic JDBC Message Store

View File

@@ -45,3 +45,9 @@ See <<./web-sockets.adoc#websocket-client-container-attributes, WebSockets Suppo
The `KafkaMessageSource` now extracts an `ErrorHandlingDeserializer` configuration from the consumer properties and re-throws `DeserializationException` extracted from failed record headers.
See <<./kafka.adoc#kafka-inbound-pollable, Kafka Inbound Channel Adapter>> for more information.
[[x6.2-jdbc]]
=== JDBC Support Changes
The `JdbcMessageStore`, `JdbcChannelMessageStore`, `JdbcMetadataStore`, and `DefaultLockRepository` implement `SmartLifecycle` and perform a`SELECT COUNT` query, on their respective tables, in the `start()` method to ensure that the required table (according to the provided prefix) is present in the target database.
See <<./jdbc.adoc#jdbc-db-init, Initializing the Database>> for more information.