Programador ajudante e aprendiz da comunidade open source.

VRaptor 3 - Evitando CircularReferenceException do XStream

Quem utiliza a serialização JSON do VRaptor 3 em algum momento já se deparou com a
CircularReferenceException ao trabalhar com coleções. O XStream da Thoughtworks é o responsável por essa serialização e hoje iremos falar um pouco dessa biblioteca.

Objetivo:

Entender alguns conceitos do Hibernate, assim como entender e evitar o CircularReferenceException do XStream na serialização JSON de um objeto retornado que possua uma coleção.

Primeiramente vamos entender alguns conceitos:

FetchType.LAZY e FetchType.EAGER

Um objeto que possua uma coleção anotada com FetchType.LAZY não terá esta carregada junto ao objeto no qual pertence, diferentemente se anotássemos com FetchType.EAGER que já carrega a coleção junto ao seu objeto. O LAZY só carrega a coleção de fato, quando a requisitamos através de um método get.

Se eu pesquisasse o seguinte usuário no banco, eu já teria de "prima" a lista de mensagens, mas não a de filmes:

@Entity
public class Usuario {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String nome;

  @OneToMany(mappedBy = "usuario", fetch = FetchType.LAZY)
  private Collection<Filme> filmeList;

  @OneToMany(mappedBy = "usuario", fetch = FetchType.EAGER)
  private Collection<Mensagem> mensagemList;

  // getters and setters

}

A propósito, não é necessário declarar que uma lista é LAZY, pois por padrão é esta sua configuração.

A lista de mensagens já é carregada logo na consulta do usuário, pois esta configurada como EAGER (Ansiosa), porém a de filme é uma lista LAZY (preguiçosa) e só será carregada quando fizermos:

Usuario usuario = usuarioDao.loadById(1l); // Já temos a lista de mensagens carregada.
Collection<Filme> filmeList = usuario.getFilmeList(); // Busca a lista de filmes somente neste momento.

Você pode estar se perguntando: qual era o valor dessa lista anteriormente? Como o Hibernate consegue buscar a lista a partir de um get? E ai que entra um outro conceito.

Hibernate Proxy:

O Hibernate por padrão com a ajuda da CGLIB (Code Generation Library) cria um Proxy para cada classe que você mapea. Esse Proxy é uma representação do seu objeto, contendo a mesma interface e um código para invocar a JDBC.

Quando consultamos nosso objeto Usuario o que o Hibernate nos retorna na verdade é um Proxy e não a nossa lista real. O Proxy implementa PersistentBag, logo ele sabe como invocar métodos no banco para fazer as consultas.

Não se iluda com o FetchType.EAGER, pois ele não evitará a exception. Ele unicamente carregará nosso target evitando a ida no banco posteriormente.

Agora que você já conhece um pouco mais do Hibernate, vamos nos focar na seguinte exception:

com.thoughtworks.xstream.core.TreeMarshaller$CircularReferenceException

Isso ocorre porque o Hibernate nos retorna uma PersistentBag que contém uma referência para a classe na qual ela pertence causando a referência cíclica. O XStream sabe converter apenas algumas implementações como ArrayList, LinkedList, HashSet e poucas outras, mas para o PersistBag ele serializa da forma padrão: cada campo, inclusive o da referência da instância, o que causa a referência cíclica.

Evitando a serialização do Proxy:

Para resolvermos isso precisaremos extender a classe XStreamJSONSerialization e fazer um pequeno ajuste na serialização.

@Component
public class CustomJSONSerialization extends XStreamJSONSerialization {

  public CustomJSONSerialization(HttpServletResponse response, TypeNameExtractor extractor, ProxyInitializer initializer) {
    super(response, extractor, initializer);
  }

  @Override
  protected XStream getXStream() {
    XStream xstream = super.getXStream();

    xstream.registerConverter(new CollectionConverter(xstream.getMapper()) {
      @Override
      @SuppressWarnings("rawtypes")
      public boolean canConvert(Class type) {
        return Collection.class.isAssignableFrom(type);
      }
    });

    return xstream;
  }

}

Criamos um Component que extende XStreamJSONSerialization. Ela recebe suas dependências e sobrescreve o método getXStream().

Recuperamos o método pai "original" e registramos um conversor através do método registerConverter. Este método recebeu uma instância de CollectionConverter que também terá o método canConvert sobrescrito, que por sinal tem o nome bem sugestivo.

Agora nossas coleções só serão serializadas se forem coleções de fato, evitando a tentativa da serialização de um Proxy. Você pode registrar quantos Converters quiser, por exemplo, um para trabalhar com Map:

xstream.registerConverter(new MapConverter(xstream.getMapper()) {
  @Override
  @SuppressWarnings("rawtypes")
  public boolean canConvert(Class type) {
    return Map.class.isAssignableFrom(type);
  }
}

Pronto! Agora você já pode serializar suas listas tranquilamente. (:

Esse problema já foi solucionado na versão 3.1.3, mas vale a pena pelo aprendizado. (;

Link do projeto:

http://github.com/wbotelhos/vraptor-3-evitando-circularreferenceexception-do-xstream

  1. Suporte.Dev 10 Jun 2012 23:36

    Esse converter não tá funcionando na versão 3.4 do vraptor.
    Mesmo adicionando essa classe no component ele continua a gerar a exception.

    1. Washington Botelho autor 25 Jun 2012 20:18

      Oi Suporte.Dev,

      Serialização JSON no VRaptor com vários relacionamentos esta meio instável mesmo.
      O ideal é você criar DTOs, ou seja, classes secas só pra manter e serializar os dados.
      Eu tiro todas as anotações dela, utilizo as coleções, mas não adiciono o objeto do outro lado do relacionamento, que seria o ManyToOne.
      Apesar da repetição dos modelos resolve o problema na certa.

  2. Rhavy 11 Mai 2012 09:07

    Olá Washignton,
    tenho o mesmo problema na hora de realizar o parser do Json utilizando o RestEasy. O PersistentBag cria uma incompatibilidade na transformação. Tens conhecimento desse problema com essa outra API?

    1. Washington Botelho autor 11 Mai 2012 21:44

      Oi Rhavy,

      Nunca utilizei, mas tenta fazer uma gambeta rodando um for nessa lista que irá forçar o proxy a buscar os dados.
      Mas se quiser pesquisar tem uma método estático do Hibernate... para fazer o load da Bag.

  3. Adriano 30 Nov 2011 14:33

    Boa tarde , estou usando o seu código fonte movy parte 4 , e fiz uma alteração no modelo filme , adicionando um capo manytomany com artista. Esta funcionando , porem deu o mesmo problema deste post e a solução foi a mesma.Mas uma parte não entendi no final do post vc diz : "Esse problema já foi solucionado na versão 3.1.3, mas vale a pena pelo aprendizado. (;" , mas não entendi se apartir do Vraptor 3.1.3 ou Xstream 3.1.3 (que acho que nem existe).

    1. Washington Botelho autor 4 Dez 2011 11:46

      Oi Adriano,

      Seria a partir da versão 3.1.3 do VRaptor, mas se deu problema com você tem algo errado no core ainda.
      Eu ainda utilizo esse Serialization em minha aplicação, pois além dessa regra tenho outras, então te dou a dica de deixá-lo sempre. (:

Em resposta:
(cancelar)
Formate seu código utilizando Markdown.