Ao desenvolver aplicações Spring MVC, é comum enfrentar desafios relacionados à serialização e desserialização de dados entre o backend e o frontend. Dois problemas recorrentes são a perda de precisão em valores numéricos do tipo Long e a formatação inadequada de objetos LocalDateTime durante a conversão para JSON.
Por exemplo, um identificador Long com um valor grande como 17283940582736451 pode ser recebido pelo frontend JavaScript como 17283940582736450, devido às limitações de representação de números inteiros em JavaScript. Da mesma forma, objetos LocalDateTime frequentemente são serializados como timestamps numéricos, em vez de um formato de data e hora legível como yyyy-MM-dd HH:mm:ss. Esses problemas são gerenciados internamente pelo HttpMessageConverter do Spring MVC.
Entendendo o HttpMessageConverter
No ecossistema Spring MVC, o HttpMessageConverter é um componente crucial que atua como tradutor entre as requisições/respostas HTTP e os objetos Java. Quando um método de controller utiliza anotações como @RequestBody (para transformar o corpo da requisição em um objeto Java) ou @ResponseBody (para serializar um objeto Java no corpo da resposta), o Spring invoca uma implementação do HttpMessageConverter.
Implementações Comuns
O Spring oferece várias implementações prontas, sendo as mais utilizadas:
StringHttpMessageConverter: Lida com a conversão de e paraString.MappingJackson2HttpMessageConverter: A base para a manipulação de JSON, utilizando a biblioteca Jackson para serialização e desserialização de objetos Java.
Configuração Unificada para Formatação e Precisão de Long
Para resolver as questões de formatação de LocalDateTime e a perda de precisão de Long, é prática comum personalizar o ObjectMapper da Jackson e integrá-lo ao MappingJackson2HttpMessageConverter.
Passo 1: Criando um Mapeador de Objetos Personalizado
Primeiro, definimos uma classe que estende ObjectMapper para aplicar regras de serialização específicas. Este customizador desabilita a falha em propriedades desconhecidas e configura serilaizadores para LocalDateTime e Long.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
public class CustomJsonMapper extends ObjectMapper {
public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public CustomJsonMapper() {
super();
// Configura para não falhar ao encontrar propriedades desconhecidas
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
// Cria um módulo simples para registrar serializadores e desserializadores
SimpleModule dateTimeAndLongModule = new SimpleModule();
// Serializa LocalDateTime para o formato yyyy-MM-dd HH:mm:ss
dateTimeAndLongModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
// Desserializa LocalDateTime do formato yyyy-MM-dd HH:mm:ss
dateTimeAndLongModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
// Serializa Long para String para evitar perda de precisão no JavaScript
dateTimeAndLongModule.addSerializer(Long.class, ToStringSerializer.instance);
// Registra o módulo no ObjectMapper
this.registerModule(dateTimeAndLongModule);
}
}
Passo 2: Estendendo os Conversores de Mensagem no Spring MVC
Em uma classe de configuração que estende WebMvcConfigurationSupport (ou implementa WebMvcConfigurer e sobrescreve o método), podemos adicionar nosso conversor personalizado à lista de conversores do Spring.
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
@Configuration
public class MvcConfig extends WebMvcConfigurationSupport {
/**
* Estende os conversores de mensagem do framework Spring MVC.
* Adiciona um HttpMessageConverter personalizado para JSON.
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 1. Instancia um novo conversor JSON
MappingJackson2HttpMessageConverter customJsonConverter = new MappingJackson2HttpMessageConverter();
// 2. Associa nosso ObjectMapper personalizado a ele
customJsonConverter.setObjectMapper(new CustomJsonMapper());
// 3. Adiciona o conversor personalizado ao início da lista para garantir sua prioridade
converters.add(0, customJsonConverter);
}
}
Princípios de Funcionamento dos Conversores
Por que extendMessageConverters em vez de configureMessageConverters?
configureMessageConverters(List<HttpMessageConverter<?>> converters): Este método substitui completamente a lista padrão de conversores do Spring. Se você o usar, precisará reconfigurar manualmente todos os conversores padrão que deseja manter, o que pode ser trabalhoso.extendMessageConverters(List<HttpMessageConverter<?>> converters): Este método permite que você modifique a lista de conversores padrão. Ao usarconverters.add(0, customJsonConverter), você insere seu conversor no início da lista, garantindo que ele seja considerado antes dos conversores padrão para tipos de mídia compatíveis.
Mecanismo de Negociação de Conteúdo
O Spring MVC determina qual HttpMessageConverter utilizar através de um processo de negociação de conteúdo. Os pontos chave para essa decisão são:
- Header
Content-Typeda Requisição: Para requisições (@RequestBody), o Spring verifica oContent-Typeno cabeçalho da requisição (ex:application/json) e seleciona um conversor que possa ler esse tipo. - Header
Acceptda Requisição: Para respostas (@ResponseBody), o Spring analisa oAcceptno cabeçalho da requisição (ex:application/xml) para encontrar um conversor que possa escrever o tipo de mídia solicitado. - Tipo de Retorno do Método Controller: O tipo do objeto Java retornado pelo método do controller é avaliado para garantir que o conversor selecionado seja capaz de serializar esse tipo.
- Ordem de Prioridade: Os conversores são iterados na ordem em que estão na lista de conversores configurados. O primeiro conversor que é capaz de lidar com o tipo de mídia e o tipo de objeto é selecionado. A inserção do seu conversor personalizado na posição 0 assegura que ele tenha a maior prioridade.