Resolvendo Exceção de Parse de Data em Requisições Feign entre Microserviços Spring Cloud

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.

Tags: Feign Spring Cloud Jackson SimpleDateFormat ThreadLocal

Publicado em 7-8 21:36