Skip to content

Commit 3ad8828

Browse files
committed
GH-6380: Support Map as input in BeanPropertySqlParameterSourceFactory
Fixes: #6380 The `MapSqlParameterSource` is much faster, then reflection or SpEL, so, it would be great to have such an interaction when we evaluate values for SQL queries * Enhance `BeanPropertySqlParameterSourceFactory` to use `MapSqlParameterSource` if `input` is a `Map` * Expose `JdbcMessageHandler.usePayloadAsParameterSource` for convenience with `SqlParameterSourceFactory`, especially when the payload is a map
1 parent 96ddc01 commit 3ad8828

File tree

11 files changed

+111
-63
lines changed

11 files changed

+111
-63
lines changed

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/BeanPropertySqlParameterSourceFactory.java

+15-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,17 +16,17 @@
1616

1717
package org.springframework.integration.jdbc;
1818

19-
import java.util.Collections;
2019
import java.util.HashMap;
2120
import java.util.Map;
2221

2322
import org.springframework.jdbc.core.namedparam.AbstractSqlParameterSource;
2423
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
24+
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
2525
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
2626

2727
/**
2828
* A default implementation of {@link SqlParameterSourceFactory} which creates an {@link SqlParameterSource} to
29-
* reference bean properties in its input.
29+
* reference bean properties (or map keys) in its input.
3030
*
3131
* @author Dave Syer
3232
* @author Gary Russell
@@ -36,16 +36,15 @@
3636
*/
3737
public class BeanPropertySqlParameterSourceFactory implements SqlParameterSourceFactory {
3838

39-
private Map<String, Object> staticParameters = Collections.unmodifiableMap(new HashMap<>());
39+
private final Map<String, Object> staticParameters = new HashMap<>();
4040

4141
/**
4242
* If the input is a List or a Map, the output is a map parameter source, and in that case some static parameters
4343
* can be added (default is empty). If the input is not a List or a Map then this value is ignored.
44-
*
4544
* @param staticParameters the static parameters to set
4645
*/
4746
public void setStaticParameters(Map<String, Object> staticParameters) {
48-
this.staticParameters = staticParameters;
47+
this.staticParameters.putAll(staticParameters);
4948
}
5049

5150
@Override
@@ -55,19 +54,25 @@ public SqlParameterSource createParameterSource(Object input) {
5554

5655
private static final class StaticBeanPropertySqlParameterSource extends AbstractSqlParameterSource {
5756

58-
private final BeanPropertySqlParameterSource input;
57+
private final SqlParameterSource input;
5958

6059
private final Map<String, Object> staticParameters;
6160

61+
@SuppressWarnings("unchecked")
6262
StaticBeanPropertySqlParameterSource(Object input, Map<String, Object> staticParameters) {
63-
this.input = new BeanPropertySqlParameterSource(input);
63+
this.input =
64+
input instanceof Map
65+
? new MapSqlParameterSource((Map<String, Object>) input)
66+
: new BeanPropertySqlParameterSource(input);
67+
6468
this.staticParameters = staticParameters;
6569
}
6670

6771
@Override
6872
public Object getValue(String paramName) throws IllegalArgumentException {
69-
return this.staticParameters.containsKey(paramName) ? this.staticParameters.get(paramName) : this.input
70-
.getValue(paramName);
73+
return this.staticParameters.containsKey(paramName)
74+
? this.staticParameters.get(paramName)
75+
: this.input.getValue(paramName);
7176
}
7277

7378
@Override

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java

+32-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -70,7 +70,7 @@
7070
* {@link JdbcOperations#batchUpdate(String, BatchPreparedStatementSetter)} function.
7171
* <p>
7272
* NOTE: The batch update is not supported when {@link #keysGenerated} is in use.
73-
*
73+
* <p>
7474
* N.B. do not use quotes to escape the header keys. The default SQL parameter source (from Spring JDBC) can also handle
7575
* headers with dotted names (e.g. <code>business.id</code>)
7676
*
@@ -97,6 +97,8 @@ public class JdbcMessageHandler extends AbstractMessageHandler {
9797

9898
private MessagePreparedStatementSetter preparedStatementSetter;
9999

100+
private boolean usePayloadAsParameterSource;
101+
100102
/**
101103
* Constructor taking {@link DataSource} from which the DB Connection can be obtained and the select query to
102104
* execute to retrieve new rows.
@@ -137,6 +139,18 @@ public void setSqlParameterSourceFactory(SqlParameterSourceFactory sqlParameterS
137139
this.sqlParameterSourceFactory = sqlParameterSourceFactory;
138140
}
139141

142+
/**
143+
* If set to 'true', the payload of the Message will be used as a source for
144+
* providing parameters. If false the entire {@link Message} will be available
145+
* as a source for parameters.
146+
* Makes sense only if {@link #setPreparedStatementSetter(MessagePreparedStatementSetter)} is not provided.
147+
* @param usePayloadAsParameterSource false for the entire {@link Message} as parameter source.
148+
* @since 6.5
149+
*/
150+
public void setUsePayloadAsParameterSource(boolean usePayloadAsParameterSource) {
151+
this.usePayloadAsParameterSource = usePayloadAsParameterSource;
152+
}
153+
140154
/**
141155
* Specify a {@link MessagePreparedStatementSetter} to populate parameters on the
142156
* {@link PreparedStatement} with the {@link Message} context.
@@ -210,21 +224,23 @@ protected List<? extends Map<String, Object>> executeUpdateQuery(final Message<?
210224
}
211225
else {
212226
KeyHolder keyHolder = new GeneratedKeyHolder();
227+
Object parameterSource = this.usePayloadAsParameterSource ? message.getPayload() : message;
213228
this.jdbcOperations.update(this.updateSql,
214-
this.sqlParameterSourceFactory.createParameterSource(message), keyHolder);
229+
this.sqlParameterSourceFactory.createParameterSource(parameterSource), keyHolder);
215230
return keyHolder.getKeyList();
216231
}
217232
}
218233
else {
219-
if (message.getPayload() instanceof Iterable) {
220-
Stream<? extends Message<?>> messageStream =
221-
StreamSupport.stream(((Iterable<?>) message.getPayload()).spliterator(), false)
222-
.map(payload -> payloadToMessage(payload, message.getHeaders()));
234+
if (message.getPayload() instanceof Iterable<?> iterable) {
235+
Stream<?> payloadStream = StreamSupport.stream(iterable.spliterator(), false);
223236

224237
int[] updates;
225238

226239
if (this.preparedStatementSetter != null) {
227-
Message<?>[] messages = messageStream.toArray(Message<?>[]::new);
240+
Message<?>[] messages =
241+
payloadStream
242+
.map(payload -> payloadToMessage(payload, message.getHeaders()))
243+
.toArray(Message<?>[]::new);
228244

229245
updates = this.jdbcOperations.getJdbcOperations()
230246
.batchUpdate(this.updateSql, new BatchPreparedStatementSetter() {
@@ -243,7 +259,12 @@ public int getBatchSize() {
243259
}
244260
else {
245261
SqlParameterSource[] sqlParameterSources =
246-
messageStream.map(this.sqlParameterSourceFactory::createParameterSource)
262+
payloadStream
263+
.map((payload) ->
264+
this.usePayloadAsParameterSource
265+
? payload :
266+
payloadToMessage(payload, message.getHeaders()))
267+
.map(this.sqlParameterSourceFactory::createParameterSource)
247268
.toArray(SqlParameterSource[]::new);
248269

249270
updates = this.jdbcOperations.batchUpdate(this.updateSql, sqlParameterSources);
@@ -265,8 +286,9 @@ public int getBatchSize() {
265286
.update(this.updateSql, ps -> this.preparedStatementSetter.setValues(ps, message));
266287
}
267288
else {
289+
Object parameterSource = this.usePayloadAsParameterSource ? message.getPayload() : message;
268290
updated = this.jdbcOperations.update(this.updateSql,
269-
this.sqlParameterSourceFactory.createParameterSource(message));
291+
this.sqlParameterSourceFactory.createParameterSource(parameterSource));
270292
}
271293

272294
LinkedCaseInsensitiveMap<Object> map = new LinkedCaseInsensitiveMap<>();

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcMessageHandlerParser.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,15 +30,12 @@
3030
/**
3131
* @author Dave Syer
3232
* @author Artem Bilan
33+
*
3334
* @since 2.0
3435
*
3536
*/
3637
public class JdbcMessageHandlerParser extends AbstractOutboundChannelAdapterParser {
3738

38-
protected boolean shouldGenerateId() {
39-
return false;
40-
}
41-
4239
protected boolean shouldGenerateIdAsFallback() {
4340
return true;
4441
}
@@ -72,6 +69,8 @@ protected AbstractBeanDefinition parseConsumer(Element element, ParserContext pa
7269
builder.addConstructorArgReference(jdbcOperationsRef);
7370
}
7471
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "sql-parameter-source-factory");
72+
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "payload-as-parameter-source",
73+
"usePayloadAsParameterSource");
7574
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "prepared-statement-setter");
7675
builder.addConstructorArgValue(query);
7776
return builder.getBeanDefinition();

spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/config/spring-integration-jdbc.xsd

+10
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,16 @@
247247
</xsd:appinfo>
248248
</xsd:annotation>
249249
</xsd:attribute>
250+
<xsd:attribute name="payload-as-parameter-source" default="false">
251+
<xsd:annotation>
252+
<xsd:documentation>
253+
Whether to use only payload from request message as parameter source for 'sql-parameter-source-factory'.
254+
</xsd:documentation>
255+
</xsd:annotation>
256+
<xsd:simpleType>
257+
<xsd:union memberTypes="xsd:boolean xsd:string"/>
258+
</xsd:simpleType>
259+
</xsd:attribute>
250260
<xsd:attribute name="prepared-statement-setter" type="xsd:string">
251261
<xsd:annotation>
252262
<xsd:appinfo>
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<beans xmlns="http://www.springframework.org/schema/beans"
3-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4-
xmlns:int="http://www.springframework.org/schema/integration"
5-
xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
6-
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns:int="http://www.springframework.org/schema/integration"
5+
xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
6+
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
77
http://www.springframework.org/schema/integration/jdbc https://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
88
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
99

@@ -12,48 +12,45 @@
1212
<int:poller id="defaultPoller" default="true" fixed-rate="5000"/>
1313

1414
<int:gateway id="startGateway" default-request-channel="startChannel"
15-
service-interface="org.springframework.integration.jdbc.storedproc.CreateUser" />
15+
service-interface="org.springframework.integration.jdbc.storedproc.CreateUser"/>
1616

1717
<int:channel id="startChannel"/>
1818

1919
<int-jdbc:stored-proc-outbound-gateway request-channel="startChannel"
20-
stored-procedure-name="CREATE_USER_RETURN_ALL" data-source="dataSource"
21-
auto-startup="true"
22-
id="gateway"
23-
ignore-column-meta-data="false"
24-
is-function="false"
25-
expect-single-result="true"
26-
reply-channel="outputChannel">
20+
stored-procedure-name="CREATE_USER_RETURN_ALL" data-source="dataSource"
21+
auto-startup="true"
22+
id="gateway"
23+
expect-single-result="true"
24+
reply-channel="outputChannel">
2725
<int-jdbc:parameter name="username" expression="payload.username"/>
2826
<int-jdbc:parameter name="password" expression="payload.password"/>
29-
<int-jdbc:parameter name="email" expression="@testService.quote(payload.email)"/>
30-
<int-jdbc:returning-resultset name="out" row-mapper="org.springframework.integration.jdbc.storedproc.UserMapper" />
27+
<int-jdbc:parameter name="email" expression="@testService.quote(payload.email)"/>
28+
<int-jdbc:returning-resultset name="out"
29+
row-mapper="org.springframework.integration.jdbc.storedproc.UserMapper"/>
3130
</int-jdbc:stored-proc-outbound-gateway>
3231

3332

3433
<int:channel id="outputChannel"/>
3534

36-
<int:service-activator id="consumerEndpoint" input-channel="outputChannel" ref="consumer" />
37-
<bean id="consumer" class="org.springframework.integration.jdbc.StoredProcOutboundGatewayWithNamespaceIntegrationTests$Consumer"/>
35+
<int:service-activator id="consumerEndpoint" input-channel="outputChannel" ref="consumer"/>
36+
<bean id="consumer"
37+
class="org.springframework.integration.jdbc.StoredProcOutboundGatewayWithNamespaceIntegrationTests$Consumer"/>
3838

3939
<int:logging-channel-adapter channel="errorChannel" log-full-message="true"/>
4040

4141
<int:chain input-channel="storedProcOutboundGatewayInsideChain" output-channel="replyChannel">
4242
<int-jdbc:stored-proc-outbound-gateway stored-procedure-name="CREATE_USER_RETURN_ALL" data-source="dataSource"
43-
ignore-column-meta-data="false"
44-
is-function="false"
4543
expect-single-result="true">
46-
<int-jdbc:parameter name="username" expression="payload.username"/>
47-
<int-jdbc:parameter name="password" expression="payload.password"/>
48-
<int-jdbc:parameter name="email" expression="payload.email"/>
49-
<int-jdbc:returning-resultset name="out" row-mapper="org.springframework.integration.jdbc.storedproc.UserMapper" />
44+
<int-jdbc:returning-resultset name="out"
45+
row-mapper="org.springframework.integration.jdbc.storedproc.UserMapper"/>
5046
</int-jdbc:stored-proc-outbound-gateway>
5147
</int:chain>
5248

5349
<int:channel id="replyChannel">
5450
<int:queue/>
5551
</int:channel>
5652

57-
<bean id="testService" class="org.springframework.integration.jdbc.StoredProcOutboundGatewayWithNamespaceIntegrationTests$TestService"/>
53+
<bean id="testService"
54+
class="org.springframework.integration.jdbc.StoredProcOutboundGatewayWithNamespaceIntegrationTests$TestService"/>
5855

5956
</beans>

spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/StoredProcOutboundGatewayWithNamespaceIntegrationTests.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.Collection;
2121
import java.util.List;
22+
import java.util.Map;
2223
import java.util.concurrent.BlockingQueue;
2324
import java.util.concurrent.LinkedBlockingQueue;
2425
import java.util.concurrent.TimeUnit;
@@ -97,7 +98,11 @@ public void test() throws Exception {
9798

9899
@Test
99100
public void testStoredProcOutboundGatewayInsideChain() {
100-
Message<User> requestMessage = new GenericMessage<>(new User("myUsername", "myPassword", "myEmail"));
101+
Message<Map<String, String>> requestMessage =
102+
new GenericMessage<>(
103+
Map.of("username", "myUsername",
104+
"password", "myPassword",
105+
"email", "myEmail"));
101106

102107
storedProcOutboundGatewayInsideChain.send(requestMessage);
103108

spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/JdbcMessageHandlerParserTests.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.Collections;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.UUID;
2223

2324
import javax.sql.DataSource;
2425

@@ -85,10 +86,11 @@ public void testDollarHeaderOutboundChannelAdapter() {
8586
public void testMapPayloadOutboundChannelAdapter() {
8687
setUp("handlingMapPayloadJdbcOutboundChannelAdapterTest.xml", getClass());
8788
assertThat(context.containsBean("jdbcAdapter")).isTrue();
88-
Message<?> message = MessageBuilder.withPayload(Collections.singletonMap("foo", "bar")).build();
89+
UUID testId = UUID.randomUUID();
90+
Message<?> message = MessageBuilder.withPayload(Map.of("id", testId, "foo", "bar")).build();
8991
channel.send(message);
9092
Map<String, Object> map = this.jdbcTemplate.queryForMap("SELECT * from FOOS");
91-
assertThat(map.get("ID")).as("Wrong id").isEqualTo(message.getHeaders().getId().toString());
93+
assertThat(map.get("ID")).as("Wrong id").isEqualTo(testId.toString());
9294
assertThat(map.get("name")).as("Wrong name").isEqualTo("bar");
9395
}
9496

Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<beans:beans xmlns="http://www.springframework.org/schema/integration/jdbc"
3-
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:si="http://www.springframework.org/schema/integration"
4-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
5-
xsi:schemaLocation="http://www.springframework.org/schema/beans
3+
xmlns:beans="http://www.springframework.org/schema/beans"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://www.springframework.org/schema/beans
66
https://www.springframework.org/schema/beans/spring-beans.xsd
7-
http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
8-
http://www.springframework.org/schema/integration
9-
https://www.springframework.org/schema/integration/spring-integration.xsd
107
http://www.springframework.org/schema/integration/jdbc
118
https://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd">
129

13-
<outbound-channel-adapter id="jdbcAdapter" query="insert into foos (id, status, name) values (:headers[id], 0, :payload[foo])"
14-
channel="target" data-source="dataSource"/>
10+
<outbound-channel-adapter id="jdbcAdapter" query="insert into foos (id, status, name) values (:id, 0, :foo)"
11+
payload-as-parameter-source="true"
12+
channel="target" data-source="dataSource"/>
1513

16-
<beans:import resource="jdbcOutboundChannelAdapterCommonConfig.xml" />
14+
<beans:import resource="jdbcOutboundChannelAdapterCommonConfig.xml"/>
1715

1816
</beans:beans>

src/reference/antora/modules/ROOT/pages/jdbc/outbound-channel-adapter.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ You can inject a different `SqlParameterSourceFactory` to get different behavior
2121

2222
The outbound adapter requires a reference to either a `DataSource` or a `JdbcTemplate`.
2323
You can also inject a `SqlParameterSourceFactory` to control the binding of each incoming message to a query.
24+
To make use of `SqlParameterSourceFactory` (especially default `BeanPropertySqlParameterSourceFactory` with its `MapSqlParameterSource`) more smooth, starting with version 6.5, the `JdbcMessageHandler` exposes a `usePayloadAsParameterSource` flag to indicate whether the whole message should be passed as parameter source input.
2425

2526
If the input channel is a direct channel, the outbound adapter runs its query in the same thread and, therefore, the same transaction (if there is one) as the sender of the message.
2627

0 commit comments

Comments
 (0)