SpringBoot如何实现动态对象蛇形命名转驼峰并容错入DLQ?
Hi there! Since you're new to Spring, I'll walk you through a straightforward, annotation-driven approach to solve both your recursive naming conversion and dead-letter queue (DLQ) needs.
1. Fix Recursive Snake-Case to Camel-Case Conversion for details
The @JsonNaming annotation only applies to the immediate class fields, not nested objects or dynamic structures like your details field. Here's how to make it work recursively:
Step 1: Adjust EventContent for Better Dynamic Handling
First, change the details field type from Object to Map<String, Object>—this makes it easier to process nested key-value pairs:
@ToString @AllArgsConstructor @Getter @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) // Handles top-level fields public class EventContent { private final String dataString1; private final String dataString2; // ... other fields @JsonDeserialize(using = RecursiveSnakeToCamelDeserializer.class) private final Map<String, Object> details; }
Step 2: Create a Recursive Deserializer
Write a custom Jackson deserializer that converts snake-case keys to camel-case recursively (including nested maps and lists):
public class RecursiveSnakeToCamelDeserializer extends StdDeserializer<Map<String, Object>> { private static final Pattern SNAKE_CASE_PATTERN = Pattern.compile("_(.)"); protected RecursiveSnakeToCamelDeserializer() { super(Map.class); } @Override public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); return convertNodeToCamelCaseMap(node); } private Map<String, Object> convertNodeToCamelCaseMap(JsonNode node) { Map<String, Object> result = new HashMap<>(); node.fields().forEachRemaining(entry -> { String camelCaseKey = convertSnakeToCamel(entry.getKey()); JsonNode valueNode = entry.getValue(); result.put(camelCaseKey, convertValueToCamelCase(valueNode)); }); return result; } private Object convertValueToCamelCase(JsonNode valueNode) { if (valueNode.isObject()) { return convertNodeToCamelCaseMap(valueNode); } else if (valueNode.isArray()) { List<Object> list = new ArrayList<>(); for (JsonNode element : valueNode) { list.add(convertValueToCamelCase(element)); } return list; } else { // Return primitive values as-is if (valueNode.isTextual()) return valueNode.asText(); if (valueNode.isBoolean()) return valueNode.asBoolean(); if (valueNode.isNumber()) return valueNode.numberValue(); return valueNode.asText(); } } private String convertSnakeToCamel(String snakeCase) { return SNAKE_CASE_PATTERN.matcher(snakeCase).replaceAll(match -> match.group(1).toUpperCase()); } }
Step 3: Register the Deserializer (Optional)
If you want to use this deserializer globally for all Map fields, register it with Spring's ObjectMapper:
@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return builder -> builder.deserializerByType(Map.class, new RecursiveSnakeToCamelDeserializer()); } }
This way you won't need the @JsonDeserialize annotation on the details field anymore.
2. Enable Fault Tolerance & DLQ for Failed Messages
Spring Cloud Stream has built-in support for Kafka DLQs—no manual message routing needed! Add these configurations to your application.properties (or application.yml):
Properties Configuration
# Enable DLQ for your input channel spring.cloud.stream.kafka.bindings.blaChannel.consumer.enable-dlq=true # Set DLQ topic name (default is <input-topic>.dlq) spring.cloud.stream.kafka.bindings.blaChannel.consumer.dlq-name=your-input-topic.dlq # Number of retries before sending to DLQ (adjust as needed) spring.cloud.stream.kafka.bindings.blaChannel.consumer.max-attempts=3 # Optional: Configure retry backoff spring.cloud.stream.kafka.bindings.blaChannel.consumer.retry.backoff.max-interval=5000 spring.cloud.stream.kafka.bindings.blaChannel.consumer.retry.backoff.initial-interval=1000
Handle Exceptions in Your Listener
To catch conversion errors or business logic failures explicitly and ensure they're routed to DLQ, wrap your doStuffWith logic in a try-catch block:
@StreamListener(MessagingChannels.BLA_CHANNEL) public void onIncomingEvent(EventContent eventContent, @Header(name = "uuid") UUID messageId ) { try { doStuffWith(eventContent); } catch (Exception e) { // Log the error with message ID for debugging log.error("Failed to process event with ID: {}", messageId, e); // Re-throw the exception so Spring Cloud Stream routes it to DLQ throw new RuntimeException(e); } }
Any uncaught exception will trigger the retry mechanism, and after max-attempts failures, the message is sent to your configured DLQ.
3. Simplified Annotation Alternative (No Custom Deserializer)
If you don't want to write a custom deserializer, use Jackson's ObjectMapper directly in your listener to convert the raw JSON payload:
@Autowired private ObjectMapper objectMapper; @StreamListener(MessagingChannels.BLA_CHANNEL) public void onIncomingEvent(@Payload String rawPayload, @Header(name = "uuid") UUID messageId ) { try { // Configure ObjectMapper to convert snake-case to camel-case recursively ObjectMapper camelCaseMapper = objectMapper.copy() .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); EventContent eventContent = camelCaseMapper.readValue(rawPayload, EventContent.class); // Now eventContent.getDetails() has camel-case keys doStuffWith(eventContent); } catch (JsonProcessingException e) { log.error("Failed to deserialize event with ID: {}", messageId, e); throw new RuntimeException(e); } }
This approach accepts the raw JSON payload as a String instead of directly deserializing to EventContent, but avoids writing a custom deserializer.
Final Notes
- Ensure your Kafka broker allows auto-creation of the DLQ topic (or create it manually beforehand).
- For Java 8, use Spring Boot 2.x (Spring Boot 3+ requires Java 17+).
- Test with different snake-case nested structures to verify recursive conversion works as expected.
内容的提问来源于stack exchange,提问作者user117788

