Cenário
Ambiente baseado em Spring Cloud e Spring Cloud OpenFeign, onde um microsserviço consome outro via cliente Feign. Em cargas concorrentes, parte das requisições falha durante a desserialização de campos do tipo Date, gerando uma JsonMappingException encadeada a NumberFormatException.
Sintoma
O erro não ocorre em todas as chamadas; sua frequência aumenta conforme a concorrência. Trecho relevante do stack trace:
feign.codec.DecodeException: Error while extracting response for type [...]
nested exception is org.springframework.http.converter.HttpMessageNotReadableException:
JSON parse error: For input string: "743E2.743E2";
nested exception is com.fasterxml.jackson.databind.JsonMappingException:
For input string: "743E2.743E2"
(through reference chain: java.util.ArrayList[0]->com.nhi.common.dto.SectionDto["createTime"])
...
java.lang.NumberFormatException: For input string: "743E2.743E2"
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:...)
Causa Raiz
A aplicação possuía um serializador/desserializador customizado registrado com @JsonComponent que mantinha uma instância estática de SimpleDateFormat. Como o JDK documenta explicitamente, SimpleDateFormat não é thread-safe. Sob paralelismo, múltiplas threads acabam compartilhando o mesmo Calendar interno do formatador, corrompendo o buffer de parsing e produzindo strings numéricas inválidas como "743E2.743E2".
Solução
Evite compartilhar o formatador. A alternativa mais simples é criar uma instância local dentro dos métodos serialize e deserialize, garantindo isolamento por chamada:
package com.nhi.common.jackson;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonComponent;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@JsonComponent
public class DateSerializationConfig {
private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";
public static class DateSerializer extends JsonSerializer<Date> {
@Override
public void serialize(Date value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
SimpleDateFormat fmt = new SimpleDateFormat(PATTERN);
gen.writeString(fmt.format(value));
}
}
public static class DateDeserializer extends JsonDeserializer<Date> {
@Override
public Date deserialize(JsonParser parser, DeserializationContext ctx)
throws IOException {
SimpleDateFormat fmt = new SimpleDateFormat(PATTERN);
try {
return fmt.parse(parser.getText());
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
}
Se o impacto de alocar SimpleDateFormat a cada serialização for relevante, use ThreadLocal para reter uma instância por thread:
@JsonComponent
public class ThreadSafeDateConfig {
private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final ThreadLocal<SimpleDateFormat> FORMATTER =
ThreadLocal.withInitial(() -> new SimpleDateFormat(PATTERN));
public static class DateSerializer extends JsonSerializer<Date> {
@Override
public void serialize(Date value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeString(FORMATTER.get().format(value));
}
}
public static class DateDeserializer extends JsonDeserializer<Date> {
@Override
public Date deserialize(JsonParser parser, DeserializationContext ctx)
throws IOException {
try {
return FORMATTER.get().parse(parser.getText());
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
}
Alternativa Moderna
Em projetos com Java 8 ou superior, prefira substituir java.util.Date por java.time.LocalDateTime e registrar o módulo JSR-310 no Jackson. Dessa forma, elimina-se totalmente a necessidade de SimpleDateFormat:
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setDateFormat(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
return mapper;
}
Considerações Finais
Após aplicar o isolamento do formatador ou migrar para java.time, o problema intermitente deixa de ocorrer. O ponto central é nunca confiar em SimpleDateFormat compratilhado em ambiente multithread.