Skip to content

Commit 1e0dfcb

Browse files
committed
Add Jackson JSON serialization for JdbcChannelMessageStore
- Add JacksonChannelMessageStorePreparedStatementSetter for serialization - Add JacksonMessageRowMapper for deserialization with trusted package validation - Support PostgreSQL (JSONB), MySQL (JSON), and H2 (CLOB) databases - Add comprehensive test coverage and documentation Fixes: gh-9312 Signed-off-by: Yoobin Yoon <[email protected]>
1 parent d4ff307 commit 1e0dfcb

File tree

15 files changed

+1091
-7
lines changed

15 files changed

+1091
-7
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,7 @@ project('spring-integration-jdbc') {
679679
dependencies {
680680
api 'org.springframework:spring-jdbc'
681681
optionalApi "org.postgresql:postgresql:$postgresVersion"
682+
optionalApi 'tools.jackson.core:jackson-databind'
682683

683684
testImplementation "com.h2database:h2:$h2Version"
684685
testImplementation "org.hsqldb:hsqldb:$hsqldbVersion"

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.springframework.integration.support.converter.AllowListDeserializingConverter;
5353
import org.springframework.integration.util.UUIDConverter;
5454
import org.springframework.jdbc.core.JdbcTemplate;
55+
import org.springframework.jdbc.core.RowMapper;
5556
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
5657
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
5758
import org.springframework.jmx.export.annotation.ManagedAttribute;
@@ -89,6 +90,7 @@
8990
* @author Trung Pham
9091
* @author Johannes Edmeier
9192
* @author Ngoc Nhan
93+
* @author Yoobin Yoon
9294
*
9395
* @since 2.2
9496
*/
@@ -148,7 +150,7 @@ private enum Query {
148150
private SerializingConverter serializer;
149151

150152
@SuppressWarnings("NullAway.Init")
151-
private MessageRowMapper messageRowMapper;
153+
private RowMapper<Message<?>> messageRowMapper;
152154

153155
@SuppressWarnings("NullAway.Init")
154156
private ChannelMessageStorePreparedStatementSetter preparedStatementSetter;
@@ -232,13 +234,13 @@ public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
232234
}
233235

234236
/**
235-
* Allow for passing in a custom {@link MessageRowMapper}. The {@link MessageRowMapper}
236-
* is used to convert the selected database row representing the persisted
237-
* message into the actual {@link Message} object.
237+
* Allow for passing in a custom {@link RowMapper} for {@link Message}.
238+
* The {@link RowMapper} is used to convert the selected database row
239+
* representing the persisted message into the actual {@link Message} object.
238240
* @param messageRowMapper Must not be null
239241
*/
240-
public void setMessageRowMapper(MessageRowMapper messageRowMapper) {
241-
Assert.notNull(messageRowMapper, "The provided MessageRowMapper must not be null.");
242+
public void setMessageRowMapper(RowMapper<Message<?>> messageRowMapper) {
243+
Assert.notNull(messageRowMapper, "The provided RowMapper must not be null.");
242244
this.messageRowMapper = messageRowMapper;
243245
}
244246

@@ -388,7 +390,7 @@ protected MessageGroupFactory getMessageGroupFactory() {
388390
* Check mandatory properties ({@link DataSource} and
389391
* {@link #setChannelMessageStoreQueryProvider(ChannelMessageStoreQueryProvider)}). If no {@link MessageRowMapper}
390392
* and {@link ChannelMessageStorePreparedStatementSetter} was explicitly set using
391-
* {@link #setMessageRowMapper(MessageRowMapper)} and
393+
* {@link #setMessageRowMapper(RowMapper)} and
392394
* {@link #setPreparedStatementSetter(ChannelMessageStorePreparedStatementSetter)} respectively, the default
393395
* {@link MessageRowMapper} and {@link ChannelMessageStorePreparedStatementSetter} will be instantiated using the
394396
* specified {@link #deserializer}.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.jdbc.store.channel;
18+
19+
import java.io.IOException;
20+
import java.sql.PreparedStatement;
21+
import java.sql.SQLException;
22+
import java.sql.Types;
23+
24+
import org.springframework.integration.support.json.JacksonJsonObjectMapper;
25+
import org.springframework.integration.support.json.JacksonMessagingUtils;
26+
import org.springframework.integration.support.json.JsonObjectMapper;
27+
import org.springframework.messaging.Message;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* A {@link ChannelMessageStorePreparedStatementSetter} implementation that uses JSON
32+
* serialization for {@link Message} objects instead of Java serialization.
33+
* <p>
34+
* This implementation stores the entire message (including headers and payload) as JSON,
35+
* with type information embedded using Jackson's {@code @class} property for proper deserialization.
36+
* <p>
37+
* <b>IMPORTANT:</b> JSON serialization exposes message content in text format in the database.
38+
* Ensure proper database access controls and encryption for sensitive data.
39+
* Consider the security implications before using this in production with sensitive information.
40+
* <p>
41+
* <b>Database Requirements:</b>
42+
* This implementation requires modifying the MESSAGE_CONTENT column to a text-based type:
43+
* <ul>
44+
* <li>PostgreSQL: Change from {@code BYTEA} to {@code JSONB}</li>
45+
* <li>MySQL: Change from {@code BLOB} to {@code JSON}</li>
46+
* <li>H2: Change from {@code LONGVARBINARY} to {@code CLOB}</li>
47+
* </ul>
48+
* See the reference documentation for schema migration instructions.
49+
* <p>
50+
* <b>Usage Example:</b>
51+
* <pre>{@code
52+
* &#64;Bean
53+
* JdbcChannelMessageStore messageStore(DataSource dataSource) {
54+
* JdbcChannelMessageStore store = new JdbcChannelMessageStore(dataSource);
55+
* store.setChannelMessageStoreQueryProvider(new PostgresChannelMessageStoreQueryProvider());
56+
*
57+
* // Enable JSON serialization (requires schema modification)
58+
* store.setPreparedStatementSetter(
59+
* new JsonChannelMessageStorePreparedStatementSetter());
60+
* store.setMessageRowMapper(
61+
* new JsonMessageRowMapper("com.example"));
62+
*
63+
* return store;
64+
* }
65+
* }</pre>
66+
*
67+
* @author Yoobin Yoon
68+
*
69+
* @since 7.0
70+
*/
71+
public class JsonChannelMessageStorePreparedStatementSetter extends ChannelMessageStorePreparedStatementSetter {
72+
73+
private final JsonObjectMapper<?, ?> jsonObjectMapper;
74+
75+
/**
76+
* Create a new {@link JsonChannelMessageStorePreparedStatementSetter} with the
77+
* default {@link JsonObjectMapper} configured for Spring Integration message serialization.
78+
* <p>
79+
* This constructor is suitable when serializing standard Spring Integration
80+
* and Java classes. Custom payload types will require their package to be added to the
81+
* corresponding {@link JsonMessageRowMapper}.
82+
*/
83+
public JsonChannelMessageStorePreparedStatementSetter() {
84+
this(new JacksonJsonObjectMapper(JacksonMessagingUtils.messagingAwareMapper()));
85+
}
86+
87+
/**
88+
* Create a new {@link JsonChannelMessageStorePreparedStatementSetter} with a
89+
* custom {@link JsonObjectMapper}.
90+
* <p>
91+
* This constructor allows full control over the JSON serialization configuration.
92+
* <p>
93+
* <b>Note:</b> The same JsonObjectMapper configuration should be used in the corresponding
94+
* {@link JsonMessageRowMapper} for consistent serialization and deserialization.
95+
* @param jsonObjectMapper the {@link JsonObjectMapper} to use for JSON serialization
96+
*/
97+
public JsonChannelMessageStorePreparedStatementSetter(JsonObjectMapper<?, ?> jsonObjectMapper) {
98+
super();
99+
Assert.notNull(jsonObjectMapper, "'jsonObjectMapper' must not be null");
100+
this.jsonObjectMapper = jsonObjectMapper;
101+
}
102+
103+
@Override
104+
public void setValues(PreparedStatement preparedStatement, Message<?> requestMessage,
105+
Object groupId, String region, boolean priorityEnabled) throws SQLException {
106+
107+
super.setValues(preparedStatement, requestMessage, groupId, region, priorityEnabled);
108+
109+
try {
110+
String json = this.jsonObjectMapper.toJson(requestMessage);
111+
112+
String dbProduct = preparedStatement.getConnection().getMetaData().getDatabaseProductName();
113+
114+
if ("PostgreSQL".equalsIgnoreCase(dbProduct)) {
115+
preparedStatement.setObject(6, json, Types.OTHER); // NOSONAR magic number
116+
}
117+
else {
118+
preparedStatement.setString(6, json); // NOSONAR magic number
119+
}
120+
}
121+
catch (IOException ex) {
122+
throw new SQLException("Failed to serialize message to JSON: " + requestMessage, ex);
123+
}
124+
}
125+
126+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.jdbc.store.channel;
18+
19+
import java.io.IOException;
20+
import java.sql.ResultSet;
21+
import java.sql.SQLException;
22+
23+
import org.springframework.integration.support.json.JacksonJsonObjectMapper;
24+
import org.springframework.integration.support.json.JacksonMessagingUtils;
25+
import org.springframework.integration.support.json.JsonObjectMapper;
26+
import org.springframework.jdbc.core.RowMapper;
27+
import org.springframework.messaging.Message;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* A {@link RowMapper} implementation that deserializes {@link Message} objects from
32+
* JSON format stored in the database.
33+
* <p>
34+
* This mapper works in conjunction with {@link JsonChannelMessageStorePreparedStatementSetter}
35+
* to provide JSON serialization for Spring Integration's JDBC Channel Message Store.
36+
* <p>
37+
* Unlike the default {@link MessageRowMapper} which uses Java serialization,
38+
* this implementation uses JSON to deserialize message strings from the MESSAGE_CONTENT column.
39+
* <p>
40+
* The underlying {@link JsonObjectMapper} validates all deserialized classes against a
41+
* trusted package list to prevent security vulnerabilities.
42+
* <p>
43+
* <b>Usage Example:</b>
44+
* <pre>{@code
45+
* &#64;Bean
46+
* JdbcChannelMessageStore messageStore(DataSource dataSource) {
47+
* JdbcChannelMessageStore store = new JdbcChannelMessageStore(dataSource);
48+
* store.setChannelMessageStoreQueryProvider(new PostgresChannelMessageStoreQueryProvider());
49+
*
50+
* // Enable JSON serialization
51+
* store.setPreparedStatementSetter(
52+
* new JsonChannelMessageStorePreparedStatementSetter());
53+
* store.setMessageRowMapper(
54+
* new JsonMessageRowMapper("com.example"));
55+
*
56+
* return store;
57+
* }
58+
* }</pre>
59+
*
60+
* @author Yoobin Yoon
61+
*
62+
* @since 7.0
63+
*/
64+
public class JsonMessageRowMapper implements RowMapper<Message<?>> {
65+
66+
private final JsonObjectMapper<?, ?> jsonObjectMapper;
67+
68+
/**
69+
* Create a new {@link JsonMessageRowMapper} with the default {@link JsonObjectMapper}
70+
* configured for Spring Integration message deserialization.
71+
* <p>
72+
* This constructor is suitable when deserializing standard Spring Integration
73+
* and Java classes. Custom payload types will require their package to be
74+
* specified using {@link #JsonMessageRowMapper(String...)}.
75+
*/
76+
public JsonMessageRowMapper() {
77+
this(new JacksonJsonObjectMapper(JacksonMessagingUtils.messagingAwareMapper()));
78+
}
79+
80+
/**
81+
* Create a new {@link JsonMessageRowMapper} with additional trusted packages
82+
* for deserialization.
83+
* <p>
84+
* The provided packages are appended to the default trusted packages,
85+
* enabling deserialization of custom payload types while maintaining security.
86+
* @param trustedPackages the additional packages to trust for deserialization
87+
*/
88+
public JsonMessageRowMapper(String... trustedPackages) {
89+
this(new JacksonJsonObjectMapper(
90+
JacksonMessagingUtils.messagingAwareMapper(trustedPackages)));
91+
}
92+
93+
/**
94+
* Create a new {@link JsonMessageRowMapper} with a custom {@link JsonObjectMapper}.
95+
* <p>
96+
* This constructor allows full control over the JSON deserialization configuration.
97+
* <p>
98+
* <b>Note:</b> The same JsonObjectMapper configuration should be used in the corresponding
99+
* {@link JsonChannelMessageStorePreparedStatementSetter} for consistent
100+
* serialization and deserialization.
101+
* @param jsonObjectMapper the {@link JsonObjectMapper} to use for JSON deserialization
102+
*/
103+
public JsonMessageRowMapper(JsonObjectMapper<?, ?> jsonObjectMapper) {
104+
Assert.notNull(jsonObjectMapper, "'jsonObjectMapper' must not be null");
105+
this.jsonObjectMapper = jsonObjectMapper;
106+
}
107+
108+
@Override
109+
@SuppressWarnings("NullAway")
110+
public Message<?> mapRow(ResultSet rs, int rowNum) throws SQLException {
111+
try {
112+
String json = rs.getString("MESSAGE_CONTENT");
113+
114+
if (json == null) {
115+
throw new SQLException("MESSAGE_CONTENT column is null at row " + rowNum);
116+
}
117+
118+
return this.jsonObjectMapper.fromJson(json, Message.class);
119+
}
120+
catch (IOException ex) {
121+
throw new SQLException(
122+
"Failed to deserialize message from JSON at row " + rowNum + ". "
123+
+ "Ensure the JSON was created by JsonChannelMessageStorePreparedStatementSetter "
124+
+ "and contains proper @class type information.",
125+
ex);
126+
}
127+
}
128+
129+
}

0 commit comments

Comments
 (0)