09 Poupando recursos: lazy dependency injection

por Tomaz Lavieri no blog

Objetivo: Ao final deste artigo espera-se que você saiba como poupar recursos caros, trazendo eles de forma lazy, ou como prefiro chamar Just-in-Time (no momento certo). No VRaptor3 a injeção de dependência ficou bem mais fácil, os interceptadores que eram os responsáveis para injetar a dependência sumiram e agora fica tudo a cargo do container, que pode ser o Spring ou o Pico. A facilidade na injeção de dependencia tem um custo, como não é mais controlado pelo programador que cria o interceptor sempre que declaramos uma dependência no construtor de um @Component, @Resource ou @Intercepts ele é injetado no início, logo na construção, porém às vezes o fluxo de um requisição faz com que não usemos algumas destas injeções de dependência, disperdiçando recursos valiosos. Por exemplo, vamos supor o seguinte @Resource abaixo, que cadastra produtos

import java.util.List;
import org.hibernate.Session;
import br.com.caelum.vraptor.Result;
import br.com.caelum.vraptor.view.Results;

@Resource
public class ProdutoController {
    /**
     * O recurso que queremos poupar.
     */
    private final Session session;
    private final Result result;

    public ProdutoController(final Session session, final Result result) {
        this.session = session;
        this.result = result;
    }

    /**
     * apenas renderiza o formulário
     */
    public void form() {}

    public List<Produto> listar() {
        return session.createCriteria(Produto.class).list();
    }

    public Produto adiciona(Produto produto) {
        session.persist(produto);
        result.use(Results.logic()).redirectTo(getClass()).listar();
        return produto;
    }
}

Sempre que alguem faz uma requisição a qualquer lógica dentro do recurso ProdutoController uma Session é aberta, porem note que abrir o formulário para adicionar produtos não requer sessão com o banco, ele apenas renderiza uma página, cada vez que o formulário de produtos é aberto um importante e caro recurso do sistema esta sendo requerido, e de forma totalmente ociosa.

Nota do editor

É criada no máximo uma Session por requisição. A mesma coisa para qualquer componente Request scoped.

Como agir neste caso? Isolar o formulário poderia resolver este problema mais recairia em outro, da mantenabilidade. O ideal é que este recurso só fosse realmente injetado no tempo certo (Just in Time) como seria possível fazer isso? A solução é usar proxies dinâmicos, enviando uma Session que só realmente abrirá a conexão com o banco quando um de seus métodos for invocado.

import java.lang.reflect.Method;
import javax.annotation.PreDestroy;
import org.hibernate.classic.Session;
import org.hibernate.SessionFactory;
import net.vidageek.mirror.dsl.Mirror;
import br.com.caelum.vraptor.ioc.ApplicationScoped;
import br.com.caelum.vraptor.ioc.Component;
import br.com.caelum.vraptor.ioc.ComponentFactory;
import br.com.caelum.vraptor.ioc.RequestScoped;
import br.com.caelum.vraptor.proxy.MethodInvocation;
import br.com.caelum.vraptor.proxy.Proxifier;
import br.com.caelum.vraptor.proxy.SuperMethod;

/**
* <b>JIT (Just-in-Time) {@link Session} Creator</b> fábrica para o
* componente {@link Session} gerado de forma LAZY ou JIT(Just-in-Time)
* a partir de uma {@link SessionFactory}, que normalmente se encontra
* em um ecopo de aplicativo @{@link ApplicationScoped}.
*
* @author Tomaz Lavieri
* @since 1.0
*/
@Component
@RequestScoped
public class JITSessionCreator implements ComponentFactory<Session> {

    private static final Method CLOSE =
            new Mirror().on(Session.class).reflect().method("close").withoutArgs();
    private static final Method FINALIZE =
            new Mirror().on(Object.class).reflect().method("finalize").withoutArgs();

    private final SessionFactory factory;
    /** Guarda a Proxy Session */
    private final Session proxy;
    /** Guarada a Session real. */
    private Session session;

    public JITSessionCreator(SessionFactory factory, Proxifier proxifier) {
        this.factory = factory;
        this.proxy = proxify(Session.class, proxifier); // *1*
    }

    /**
     * Cria o JIT Session, que repassa a invocação de qualquer método, exceto
     * {@link Object#finalize()} e {@link Session#close()}, para uma session real,
     * criando uma se necessário.
     */
    private Session proxify(Class<? extends Session> target, Proxifier proxifier) {
        return proxifier.proxify(target, new MethodInvocation<Session>() {
            @Override // *2*
            public Object intercept(Session proxy, Method method, Object[] args,
                                                            SuperMethod superMethod) {
                if (method.equals(CLOSE)
                        || (method.equals(FINALIZE) && session == null)) {
                    return null; //skip
                }
                return new Mirror().on(getSession()).invoke().method(method)
                                    .withArgs(args);
            }
        });
    }

    public Session getSession() {
        if (session == null) // *3*
                session = factory.openSession();
        return session;
    }

    @Override
    public Session getInstance() {
        return proxy; // *4*
    }

    @PreDestroy
    public void destroy() { // *5*
        if (session != null && session.isOpen()) {
            session.close();
        }
    }
}

Explicando alguns pontos chaves, comentados com // *N*

Esta mesma abordagem pode ser usada para outros recursos caros do sistema.

Os códigos fonte para os ComponentFactory de EntityManager e Session que utilizo podem ser encontrados no fórum do GUJ.