Skip to content

Commit 38f850d

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 38f850d

File tree

14 files changed

+1076
-7
lines changed

14 files changed

+1076
-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,133 @@
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.sql.PreparedStatement;
20+
import java.sql.SQLException;
21+
import java.sql.Types;
22+
23+
import tools.jackson.core.JacksonException;
24+
import tools.jackson.databind.ObjectMapper;
25+
26+
import org.springframework.integration.support.json.JacksonMessagingUtils;
27+
import org.springframework.messaging.Message;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* A {@link ChannelMessageStorePreparedStatementSetter} implementation that uses Jackson
32+
* to serialize {@link Message} objects to JSON format 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.
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+
* The {@link ObjectMapper} is configured using {@link JacksonMessagingUtils#messagingAwareMapper(String...)}
42+
* which includes custom serializers/deserializers for Spring Integration message types
43+
* and embeds class type information for secure deserialization.
44+
* <p>
45+
* <b>Database Requirements:</b>
46+
* This implementation requires modifying the MESSAGE_CONTENT column to a text-based type:
47+
* <ul>
48+
* <li>PostgreSQL: Change from {@code BYTEA} to {@code JSONB}</li>
49+
* <li>MySQL: Change from {@code BLOB} to {@code JSON}</li>
50+
* <li>H2: Change from {@code LONGVARBINARY} to {@code CLOB}</li>
51+
* </ul>
52+
* See the reference documentation for schema migration instructions.
53+
* <p>
54+
* <b>Usage Example:</b>
55+
* <pre>{@code
56+
* &#64;Bean
57+
* JdbcChannelMessageStore messageStore(DataSource dataSource) {
58+
* JdbcChannelMessageStore store = new JdbcChannelMessageStore(dataSource);
59+
* store.setChannelMessageStoreQueryProvider(new PostgresChannelMessageStoreQueryProvider());
60+
*
61+
* // Enable JSON serialization (requires schema modification)
62+
* store.setPreparedStatementSetter(
63+
* new JacksonChannelMessageStorePreparedStatementSetter());
64+
* store.setMessageRowMapper(
65+
* new JacksonMessageRowMapper("com.example"));
66+
*
67+
* return store;
68+
* }
69+
* }</pre>
70+
*
71+
* @author Yoobin Yoon
72+
*
73+
* @since 7.0
74+
*/
75+
public class JacksonChannelMessageStorePreparedStatementSetter extends ChannelMessageStorePreparedStatementSetter {
76+
77+
private final ObjectMapper objectMapper;
78+
79+
/**
80+
* Create a new {@link JacksonChannelMessageStorePreparedStatementSetter} with the
81+
* default trusted packages from {@link JacksonMessagingUtils#DEFAULT_TRUSTED_PACKAGES}.
82+
* <p>
83+
* This constructor is suitable when you only need to serialize standard Spring Integration
84+
* and Java classes. Custom payload types will require their package to be added to the
85+
* corresponding {@link JacksonMessageRowMapper}.
86+
*/
87+
public JacksonChannelMessageStorePreparedStatementSetter() {
88+
super();
89+
this.objectMapper = JacksonMessagingUtils.messagingAwareMapper();
90+
}
91+
92+
/**
93+
* Create a new {@link JacksonChannelMessageStorePreparedStatementSetter} with a
94+
* custom {@link ObjectMapper}.
95+
* <p>
96+
* This constructor allows full control over the JSON serialization configuration.
97+
* The provided mapper should be configured appropriately for Message serialization,
98+
* typically using {@link JacksonMessagingUtils#messagingAwareMapper(String...)}.
99+
* <p>
100+
* <b>Note:</b> The same ObjectMapper configuration should be used in the corresponding
101+
* {@link JacksonMessageRowMapper} for consistent serialization and deserialization.
102+
* @param objectMapper the {@link ObjectMapper} to use for JSON serialization
103+
*/
104+
public JacksonChannelMessageStorePreparedStatementSetter(ObjectMapper objectMapper) {
105+
super();
106+
Assert.notNull(objectMapper, "'objectMapper' must not be null");
107+
this.objectMapper = objectMapper;
108+
}
109+
110+
@Override
111+
public void setValues(PreparedStatement preparedStatement, Message<?> requestMessage,
112+
Object groupId, String region, boolean priorityEnabled) throws SQLException {
113+
114+
super.setValues(preparedStatement, requestMessage, groupId, region, priorityEnabled);
115+
116+
try {
117+
String json = this.objectMapper.writeValueAsString(requestMessage);
118+
119+
String dbProduct = preparedStatement.getConnection().getMetaData().getDatabaseProductName();
120+
121+
if ("PostgreSQL".equalsIgnoreCase(dbProduct)) {
122+
preparedStatement.setObject(6, json, Types.OTHER); // NOSONAR magic number
123+
}
124+
else {
125+
preparedStatement.setString(6, json); // NOSONAR magic number
126+
}
127+
}
128+
catch (JacksonException ex) {
129+
throw new SQLException("Failed to serialize message to JSON: " + requestMessage, ex);
130+
}
131+
}
132+
133+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.sql.ResultSet;
20+
import java.sql.SQLException;
21+
22+
import tools.jackson.core.JacksonException;
23+
import tools.jackson.core.type.TypeReference;
24+
import tools.jackson.databind.ObjectMapper;
25+
26+
import org.springframework.integration.support.json.JacksonMessagingUtils;
27+
import org.springframework.jdbc.core.RowMapper;
28+
import org.springframework.messaging.Message;
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 JacksonChannelMessageStorePreparedStatementSetter}
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 Jackson to deserialize JSON strings from the MESSAGE_CONTENT column.
39+
* <p>
40+
* The {@link ObjectMapper} is configured using {@link JacksonMessagingUtils#messagingAwareMapper(String...)}
41+
* which validates all deserialized classes against a trusted package list to prevent
42+
* security vulnerabilities.
43+
* <p>
44+
* <b>Usage Example:</b>
45+
* <pre>{@code
46+
* &#64;Bean
47+
* JdbcChannelMessageStore messageStore(DataSource dataSource) {
48+
* JdbcChannelMessageStore store = new JdbcChannelMessageStore(dataSource);
49+
* store.setChannelMessageStoreQueryProvider(new PostgresChannelMessageStoreQueryProvider());
50+
*
51+
* // Enable JSON serialization
52+
* store.setPreparedStatementSetter(
53+
* new JacksonChannelMessageStorePreparedStatementSetter());
54+
* store.setMessageRowMapper(
55+
* new JacksonMessageRowMapper("com.example"));
56+
*
57+
* return store;
58+
* }
59+
* }</pre>
60+
*
61+
* @author Yoobin Yoon
62+
*
63+
* @since 7.0
64+
*/
65+
public class JacksonMessageRowMapper implements RowMapper<Message<?>> {
66+
67+
private final ObjectMapper objectMapper;
68+
69+
/**
70+
* Create a new {@link JacksonMessageRowMapper} with trusted packages for deserialization.
71+
* @param trustedPackages the packages to trust for deserialization
72+
*/
73+
public JacksonMessageRowMapper(String... trustedPackages) {
74+
this.objectMapper = JacksonMessagingUtils.messagingAwareMapper(trustedPackages);
75+
}
76+
77+
/**
78+
* Create a new {@link JacksonMessageRowMapper} with a custom {@link ObjectMapper}.
79+
* @param objectMapper the ObjectMapper configured for message serialization
80+
*/
81+
public JacksonMessageRowMapper(ObjectMapper objectMapper) {
82+
this.objectMapper = objectMapper;
83+
}
84+
85+
@Override
86+
public Message<?> mapRow(ResultSet rs, int rowNum) throws SQLException {
87+
try {
88+
String json = rs.getString("MESSAGE_CONTENT");
89+
90+
if (json == null) {
91+
throw new SQLException("MESSAGE_CONTENT column is null at row " + rowNum);
92+
}
93+
94+
return this.objectMapper.readValue(json, new TypeReference<Message<?>>() {
95+
96+
});
97+
}
98+
catch (JacksonException ex) {
99+
throw new SQLException(
100+
"Failed to deserialize message from JSON at row " + rowNum + ". "
101+
+ "Ensure the JSON was created by JacksonChannelMessageStorePreparedStatementSetter "
102+
+ "and contains proper @class type information.",
103+
ex);
104+
}
105+
}
106+
107+
}

0 commit comments

Comments
 (0)