Skip to content

Commit a432da5

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 a432da5

File tree

15 files changed

+1088
-7
lines changed

15 files changed

+1088
-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+
* By default, 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: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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.jspecify.annotations.Nullable;
24+
25+
import org.springframework.integration.support.json.JacksonJsonObjectMapper;
26+
import org.springframework.integration.support.json.JacksonMessagingUtils;
27+
import org.springframework.integration.support.json.JsonObjectMapper;
28+
import org.springframework.jdbc.core.RowMapper;
29+
import org.springframework.messaging.Message;
30+
import org.springframework.util.Assert;
31+
32+
/**
33+
* A {@link RowMapper} implementation that deserializes {@link Message} objects from
34+
* JSON format stored in the database.
35+
* <p>
36+
* This mapper works in conjunction with {@link JsonChannelMessageStorePreparedStatementSetter}
37+
* to provide JSON serialization for Spring Integration's JDBC Channel Message Store.
38+
* <p>
39+
* Unlike the default {@link MessageRowMapper} which uses Java serialization,
40+
* this implementation uses JSON to deserialize message strings from the MESSAGE_CONTENT column.
41+
* <p>
42+
* The underlying {@link JsonObjectMapper} validates all deserialized classes against a
43+
* trusted package list to prevent security vulnerabilities.
44+
* <p>
45+
* <b>Usage Example:</b>
46+
* <pre>{@code
47+
* &#64;Bean
48+
* JdbcChannelMessageStore messageStore(DataSource dataSource) {
49+
* JdbcChannelMessageStore store = new JdbcChannelMessageStore(dataSource);
50+
* store.setChannelMessageStoreQueryProvider(new PostgresChannelMessageStoreQueryProvider());
51+
*
52+
* // Enable JSON serialization
53+
* store.setPreparedStatementSetter(
54+
* new JsonChannelMessageStorePreparedStatementSetter());
55+
* store.setMessageRowMapper(
56+
* new JsonMessageRowMapper("com.example"));
57+
*
58+
* return store;
59+
* }
60+
* }</pre>
61+
*
62+
* @author Yoobin Yoon
63+
*
64+
* @since 7.0
65+
*/
66+
public class JsonMessageRowMapper implements RowMapper<Message<?>> {
67+
68+
private final JsonObjectMapper<?, ?> jsonObjectMapper;
69+
70+
/**
71+
* Create a new {@link JsonMessageRowMapper} with additional trusted packages
72+
* for deserialization.
73+
* <p>
74+
* The provided packages are appended to the default trusted packages,
75+
* enabling deserialization of custom payload types while maintaining security.
76+
* If no packages are provided, only the default trusted packages are used.
77+
* @param trustedPackages the additional packages to trust for deserialization.
78+
* Can be {@code null} or empty to use only default packages
79+
*/
80+
public JsonMessageRowMapper(String @Nullable ... trustedPackages) {
81+
this(new JacksonJsonObjectMapper(
82+
JacksonMessagingUtils.messagingAwareMapper(trustedPackages)));
83+
}
84+
85+
/**
86+
* Create a new {@link JsonMessageRowMapper} with a custom {@link JsonObjectMapper}.
87+
* <p>
88+
* This constructor allows full control over the JSON deserialization configuration.
89+
* <p>
90+
* <b>Note:</b> The same JsonObjectMapper configuration should be used in the corresponding
91+
* {@link JsonChannelMessageStorePreparedStatementSetter} for consistent
92+
* serialization and deserialization.
93+
* @param jsonObjectMapper the {@link JsonObjectMapper} to use for JSON deserialization
94+
*/
95+
public JsonMessageRowMapper(JsonObjectMapper<?, ?> jsonObjectMapper) {
96+
Assert.notNull(jsonObjectMapper, "'jsonObjectMapper' must not be null");
97+
this.jsonObjectMapper = jsonObjectMapper;
98+
}
99+
100+
@Override
101+
@SuppressWarnings("NullAway")
102+
public Message<?> mapRow(ResultSet rs, int rowNum) throws SQLException {
103+
try {
104+
String json = rs.getString("MESSAGE_CONTENT");
105+
106+
if (json == null) {
107+
throw new SQLException("MESSAGE_CONTENT column is null at row " + rowNum);
108+
}
109+
110+
return this.jsonObjectMapper.fromJson(json, Message.class);
111+
}
112+
catch (IOException ex) {
113+
throw new SQLException(
114+
"Failed to deserialize message from JSON at row " + rowNum + ". "
115+
+ "Ensure the JSON and the configured JsonObjectMapper use compatible type handling.",
116+
ex);
117+
}
118+
}
119+
120+
}

0 commit comments

Comments
 (0)