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. (;

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