英文:
Spring Cloud Stream: Spring Boot 3.x: JsonProperty, JsonIgnoreProperties behaving unexpectedly with Jackson in Message Converter
问题
I'm here to provide translations of the text you provided. Please let me know which parts you'd like me to translate.
英文:
I am currently working on a Spring Boot application (version 3.0.6) and using Spring Cloud (version 2022.0.2). I have two different endpoints ("/mvc" and "/message") that use two different request objects (DummyMessage1 and DummyMessage2), and I'm having some issues with Jackson's JsonProperty and JsonIgnoreProperties annotations.
In both the cases I'm looking to supress numbers from response and get tokens in response.
I have used JsonIgnoreProperties to suppress numbers from response.
@JsonIgnoreProperties(ignoreUnknown = true, value = {"numbers"}, allowSetters = true)
In the /mvc
endpoint, everything is working as expected. I'm using DummyMessage1
request object for this endpoint. The numbers
field is suppressed from response as expected. Furthermore, the tokens
field, is returned successfully in the JSON response and is not empty as it contains the list of tokens from the request.
The problem arises in the /message
endpoint, which uses DummyMessage2
to post a message to kafka. DummyMessage2
is identical to DummyMessage1
.
When I attempted to post a JSON to the Kafka topic, an exception was thrown:
Full stacktrace can be found here
java.lang.ClassCastException: class [B cannot be cast to class com.example.marshaller.model.DummyMessage2 ([B is in module java.base of loader 'bootstrap'; com.example.marshaller.model.DummyMessage2 is in unnamed module of loader 'app')
at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.invokeConsumer(SimpleFunctionRegistry.java:990) ~[spring-cloud-function-context-4.0.2.jar:4.0.2]
Here are the request objects:
DummyMessage1
:
@EqualsAndHashCode(callSuper = false)
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true, value = {"numbers"}, allowSetters = true)
public class DummyMessage1 extends BaseRequest {
private String numbers;
//@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public List<String> getTokens() {
if (StringUtils.isBlank(this.numbers)) return Collections.emptyList();
return List.of(this.numbers.split(";\\s*"));
}
}
DummyMessage2
:
@EqualsAndHashCode(callSuper = false)
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true, value = {"numbers"}, allowSetters = true)
public class DummyMessage2 extends BaseRequest {
private String numbers;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public List<String> getTokens() {
if (StringUtils.isBlank(this.numbers)) return Collections.emptyList();
return List.of(this.numbers.split(";\\s*"));
}
}
Next, to work around the the exception, I added the annotation @JsonProperty(access = JsonProperty.Access.READ_ONLY) on getTokens() method in DummyMessage2. However, this did not result in the desired outcome. The JSON posted to Kafka now did not include the numbers field as required, but also, the tokens field was as empty, which is not expected.
Why is this happening? I would expect the tokens field to be populated as it is in the /mvc endpoint. Any help or insights would be greatly appreciated.
Here's repository to reproduce the issue: https://github.com/cricketbackground/marshaller
Please note for security reasons the kafka brokers and kafka zk nodes are purposefully not set in the repo.
Request Body:
{
"numbers":"12345; 3982934823; 3248923492834; 324923434"
}
How to use: please see here
--- Update
Here's the simple working DummyMessage2 by adding a field for the utility getter. In the getter method the field itself is returned if it is not empty, thus retaining the field value across multiple message post hops
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
@EqualsAndHashCode(callSuper = false)
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true, value = {"numbers"}, allowSetters = true)
public class DummyMessage2 extends BaseRequest {
private String numbers;
private List<String> tokens;
public List<String> getTokens() {
if (!CollectionUtils.isEmpty(this.tokens)) return this.tokens;
if (StringUtils.isBlank(this.numbers)) return Collections.emptyList();
return List.of(this.numbers.split(";\\s*"));
}
}
The other solution suggested by Yevhenii Semenov also works but requires setter method logic to be written exactly reverse of getter logic. Setter method complexity increases based on getter method. Also, if there are more fields then writing/maintaining setter methods for each field becomes a problem
答案1
得分: 2
这种行为的原因在于你的 DummyMessage 配置。你有一个 String numbers
字段,但在 Jackson 中忽略了它。相反,你提供了 getToken
方法。因此,Jackson 认为你的对象只有 List<String> tokens
字段,并且在解组时无法理解你的字节。
仔细看一下:
当你调用 /mvc
端点时,只有编组发生,所以它正常工作。你有以下流程:
请求体 (1) -> DummyMessage1 (2) -> 响应体
{
"numbers": "6469344427; 2017586291"
}
(1) ===>
DummyMessage1(
String numbers = "6469344427; 2017586291"
)
(2) ===>
{
"tokens": [
"6469344427",
"2017586291"
]
}
但在你的 /message
端点,流程更长:
请求体 (1) -> DummyMessage2 (2) -> Json (3) -> DummyMessage2
在第 3 步,即在你的 DummyMessageConsumerService
中,Spring 尝试从 JSON 中解组 DummyMessage2
,但无法做到这一点,因为 Json 中只有字段 "tokens",但在对象中没有这样的字段。
为了解决这个问题:
-
从
getTokens()
方法中移除@JsonProperty(access = JsonProperty.Access.READ_ONLY)
-
在你的
DummyMessage2
中添加以下 setter:
public void setTokens(List<String> tokens) {
this.numbers = String.join(";", tokens);
}
或者你可以创建另一个 DTO 对象,像 DummyMessage3
,其中包含 List<String> tokens
而不是 String numbers
。在这种情况下,流程将是 (1) -> DummyMessage2 (2) -> Json (3) -> DummyMessage3
。
英文:
The reason for such behavior is in your DummyMessage configuration. You have the String numbers
field but ignore it for Jackson. Instead, you provide the getToken
method. So Jackson thinks that your object has only the List<String> tokens
field and doesn't understand your bytes when you unmarshal it.
Let's look closer:
When you call your /mvc
endpoint, there is only marshaling happening, so it works. You have the following flow:
Request body (1)-> DummyMessage1 (2)-> Response body
{
"numbers": "6469344427; 2017586291"
}
(1) ====>
DummyMessage1(
String numbers = "6469344427; 2017586291"
)
(2) ====>
{
"tokens": [
"6469344427",
"2017586291"
]
}
But in your /message
endpoint, the flow is longer:
Request body (1)-> DummyMessage2 (2)-> Json (3)-> DummyMessage2
And on step 3, at your DummyMessageConsumerService
, spring tries to unmarshal DummyMessage2
from JSON and can't do this because the Json has only the field "tokens," but there is no such field in the object.
To fix this issue:
-
Remove
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
fromgetTokens()
method -
Add the following setter to your
DummyMessage2
public void setTokens(List<String> tokens) {
this.numbers = String.join(";", tokens);
}
Or you can create another DTO object like DummyMessage3
with List<String> tokens
instead of String numbers
. In that case the flow will be (1) -> DummyMessage2 (2)-> Json (3)-> DummyMessage3
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论