sábado, 11 de setembro de 2010

Compilação Dinâmica de Código Fonte Java

INTRODUÇÃO

Podemos compilar código fonte Java dentro de um programa também compilado em Java. Para isso, vamos usar as ferramentas do pacote javax.tools fornecidas no Java 6. Primeiramente, será explicado o uso de cada ferramenta desse pacote, mas para isso, a seguir temos o arquivo de exemplo para fazer compilação dinâmica.

Arquivo CompilationTest.java
-------------------------------------------------------------------------------------------------------------------
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

public class CompilationTest {

    private static final int MASK = 0x000000FF;
    private static final int MAGIC_CLASS = 0xCAFEBABE;

    private static final int CONSTANT_UTF8_INFO = 1;
    private static final int CONSTANT_INTEGER_INFO = 3;
    private static final int CONSTANT_FLOAT_INFO = 4;
    private static final int CONSTANT_LONG_INFO = 5;
    private static final int CONSTANT_DOUBLE_INFO = 6;
    private static final int CONSTANT_CLASS_INFO = 7;
    private static final int CONSTANT_STRING_INFO = 8;
    private static final int CONSTANT_FIELDREF_INFO = 9;
    private static final int CONSTANT_METHODREF_INFO = 10;
    private static final int CONSTANT_INTERFACE_METHODREF_INFO = 11;
    private static final int CONSTANT_NAMEANDTYPE_INFO = 12;

    /**
     * @param args
     */
    public static void main(String[] args) {

        DiagnosticCollector diagnostics = new DiagnosticCollector();

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(
                diagnostics, null, null);

        Iterable compilationUnits = fileManager
        .getJavaFileObjectsFromStrings(Arrays.asList(args));

        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager,
                diagnostics, null, null, compilationUnits);

        task.call();

        Set kinds = new HashSet();
        kinds.add(Kind.CLASS);

        Iterable javaFileObjectIterable = null;

        try {
            javaFileObjectIterable= fileManager.list(StandardLocation.CLASS_PATH, "", kinds , false);

        } catch (IOException e1) {
            e1.printStackTrace();
        }

        if(javaFileObjectIterable != null) {
            for (JavaFileObject javaFileObject : javaFileObjectIterable) {
                String name = null;

                try {
                    InputStream in = javaFileObject.openInputStream();
                    name = getClassName(in);

                } catch (IOException e) {
                    e.printStackTrace();
                }

                System.out.println("Nome da classe em formato .class: " + name);
            }
        }

        try {
            fileManager.close();

        } catch (IOException e) {

        }

        for (Diagnostic diagnostic : diagnostics
                .getDiagnostics()) {
            System.out.println("Tipo de Arquivo: " + diagnostic.getKind());
            System.out.println("Código: " + diagnostic.getCode());
            System.out.println("Linha: " + diagnostic.getLineNumber());
            System.out.println("Coluna: " + diagnostic.getColumnNumber());
            System.out.println("Posição inicial: " + diagnostic.getStartPosition());
            System.out.println("Posição: " + diagnostic.getPosition());
            System.out.println("Posição final: " + diagnostic.getEndPosition());
        }
    }

    public static String getClassName(final InputStream inputStream) throws IOException {
        Map mapIndexAndObject = new HashMap();

        byte b[] = new byte[10];

        inputStream.read(b, 0, b.length);

        int aux = (MASK & b[0])<<24;
        aux = aux | (MASK & b[1])<<16;
        aux = aux | (MASK & b[2])<<8;
        aux = aux | MASK & b[3];

        if(aux != MAGIC_CLASS) {
            throw new IllegalArgumentException("O arquivo class inválido! ");
        }

        int constant_pool_count = (MASK & b[8])<<8 | MASK & b[9];

        for (int i = 1; i < constant_pool_count; i++) {
            int tag = inputStream.read();

            switch(tag) {
            case CONSTANT_UTF8_INFO: {
                b = new byte[2];
                inputStream.read(b, 0, b.length);

                int length = (MASK & b[0])<<8 | MASK & b[1];

                b = new byte[length];
                inputStream.read(b, 0, b.length);

                Reader reader = new InputStreamReader(new ByteArrayInputStream(b), "UTF-8");

                char cbuf [] = new char[b.length];
                reader.read(cbuf);

                mapIndexAndObject.put(i, new String(cbuf));

                reader.close();

            }; break;
            case CONSTANT_INTEGER_INFO: {
                advanceInputStream(inputStream, 4);

            };break;
            case CONSTANT_FLOAT_INFO: {
                advanceInputStream(inputStream, 4);

            }; break;
            case CONSTANT_LONG_INFO: {
                advanceInputStream(inputStream, 8);

                i++;

            }; break;
            case CONSTANT_DOUBLE_INFO: {
                advanceInputStream(inputStream, 8);

                i++;

            }; break;
            case CONSTANT_CLASS_INFO: {
                b = new byte[2];
                inputStream.read(b, 0, b.length);

                aux = (MASK & b[0])<<8 | MASK & b[1];

                mapIndexAndObject.put(i, aux);

            }; break;
            case CONSTANT_STRING_INFO: {
                advanceInputStream(inputStream, 2);

            }; break;
            case CONSTANT_FIELDREF_INFO: {
                advanceInputStream(inputStream, 4);

            }; break;
            case CONSTANT_METHODREF_INFO: {
                advanceInputStream(inputStream, 4);

            };break;
            case CONSTANT_INTERFACE_METHODREF_INFO: {
                advanceInputStream(inputStream, 4);

            }; break;
            case CONSTANT_NAMEANDTYPE_INFO: {
                advanceInputStream(inputStream, 4);

            }; break;
            }
        }

        advanceInputStream(inputStream, 2);

        b = new byte[2];
        inputStream.read(b, 0, b.length);

        aux = (MASK & b[0])<<8 | MASK & b[1];

        int index = (Integer) mapIndexAndObject.get(aux);

        return (String) mapIndexAndObject.get(index);
    }

    /**
     * Avança InputStream sem obter os bytes.
     *
     * @param in InputStream
     * @param length Quantidade a ser avançada.
     * @throws IOException Erros de I/O.
     */
    private static void advanceInputStream(final InputStream in, final int length) throws IOException {
        for (int i = 0; i < length; i++) {
            in.read();
        }
    }
}
 

-------------------------------------------------------------------------------------------------------------------

De posse do arquivo java anterior, vamos explicar algumas linhas principais.

DiagnosticCollector diagnostics = new DiagnosticCollector();

Aqui criamos um objeto que será usado para armazenar erros e avisos durante o processo de compilação do arquivo java. Os erros / avisos são obtidos através da chamada:

List diagnostic = diagnostics              .getDiagnostics();

Cada objeto Diagnostic armazena um erro / aviso, se não existir nenhum erro / aviso, a lista estará vazia.

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

Nessa linha anterior, criamos o compilador Java que será usado mais adiante no processo de compilação dinâmica.

StandardJavaFileManager fileManager = compiler.getStandardFileManager(
                diagnostics, null, null);


Essa instrução obtêm o gerenciador de arquivos java padrão (objeto que implementa a interface javax.tools.StandardJavaFileManager). Esse objeto gerencia o classpath para o compilador, sem esse objeto, o compilador não tem meios de resolver as referências externas, assim como num compilador usado de modo convencional (em linha de comando ou através de um "front end", como uma IDE - Ambiente de Desenvolvimento de Aplicações). 
Nesse exemplo, usaremos o padrão que basicamente usa todos os recursos no sistema de arquivos convencional do sistema operacional, assim, para obter o código fonte Java, abre-se uma conexão no caminho especificado (usando por exemplo java.io.File) e depois fecha-se essa conexão. E depois da compilação, os arquivos ".class" são automaticamentes salvos no sistema de arquivos do usuário.
Se implementassemos um outro gerenciador de arquivos, poderiamos estender a classe javax.tools.ForwardingJavaFileManager que segue o padrão de projetos Chain of Responsibility cuja idéia central é resolver o problema, não conseguindo, repassa a responsabilidade para o outro e assim sucessivamente até encontrar alguém que resolva. Assim, se o arquivo ".java" ou ".class" não foi localizado, passariamos a responsabilidade para o gerenciador de arquivos padrão (que implementa a interface javax.tools.StandardJavaFileManager).
Consequentemente, poderiamos definir uma politica para uma implementação customizada, como por exemplo, trabalhar com arquivos .java em tempo de execução, sem acessar disco para abrir ou salvar, trabalhando com arquivos virtuais, no sentido, que podemos construir um código fonte Java dentro do próprio programa, compilá-lo e depois faz a sua execução usando a API Java de Reflexão, sem intervir no sistema de arquivos do usuário.


Iterable compilationUnits = fileManager
        .getJavaFileObjectsFromStrings(Arrays.asList(args));


Nessa linha obtemos os objetos de implementam a interface javax.tools.JavaFileObject apartir de uma lista de nomes contendo o caminho para cada arquivo ".java". Com base nesses argumentos, o gerenciador de arquivos java padrão cria uma sequência (Interface java.lang.Iterable) de objetos que representam cada código fonte Java. Essa sequência será usada mais adiante no processo de compilação fornecendo os arquivos que deverão ser compilados.

JavaCompiler.CompilationTask task = compiler.getTask(null, 
                         fileManager, diagnostics, null, null, compilationUnits);

Nessa linha, construimos uma task que será invocada posteriormente, para a criação do ambiente de compilação. Muitos parâmetros criados anteriormente, serão usados aqui, como o objeto para armazenar erros e avisos de compilação, gerenciador de arquivos Java, código fonte Java e outros objetos que a principio, serão deixados nulos, pois são opcionais.

task.call();

Nessa instrução, damos início ao processo de compilação de código fonte Java de modo programático. E como estivessemos usandos as ferramentas de compilação Java, como o famoso comando "javac" ou através de uso de IDE, como Eclipse, Netbeans, etc.

Iterable javaFileObjectIterable = null;


try {
            javaFileObjectIterable= fileManager.list(StandardLocation.CLASS_PATH, "", kinds , false);

} catch (IOException e1) {
       e1.printStackTrace();
}



Depois da compilação dos códigos fonte Java, antes de fecharmos o gerenciador de arquivos Java (através da chamada ao método "close()"), vamos obter os arquivos compilador do gerenciador, através dessa chamada!
De posse da sequência de arquivos ".class", iteramos essa sequência como mostra um trecho do código a seguir:

if(javaFileObjectIterable != null) {
            for (JavaFileObject javaFileObject : javaFileObjectIterable) {
                String name = null;
                try {
                    InputStream in = javaFileObject.openInputStream();
                    name = getClassName(in);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("Nome da classe em formato .class: " + name);
            }
        }


Note que aqui pegamos todos os arquivos ".class" do pacote padrão. E por último, fechamos o gerenciador de arquivos Java usado e se ocorreram erros, fazemos a análise dos mesmos através do objeto que instância a classe javax.tools.DiagnosticCollector





EXEMPLO

Para executar o exemplo dado, vamos compilar o arquivo CompilationTest.java através do comando:

 javac CompilationTest.java
Com isso, será gerado o arquivo CompilationTest.class. Agora, vamos criar uma classe para testar a compilação dinâmica, conforme mostra abaixo:

Arquivo - Test.java
-----------------------------------------------------------------------------------------------------------------
public class Test {

    /**
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("Arquivo compilado e executado corretamente!!");
    }
}

------------------------------------------------------------------------------------------------------------------

Execute o comando abaixo no qual rodará o programa Java CompilationTest passando como argumento o arquivo Test.java que será compilado dentro do programa Java!

java CompilationTest Test.java



Se a compilação foi bem sucedida, no sistema de arquivos do usuário, estará o arquivo Test.class.

Observações:

Os arquivos CompilationTest.java e Test.java devem estar no mesmo diretório para execução desses testes!

Portanto, foi apresentando uma maneira de compilar código fonte java de maneira programática e dinâmica, pois as aplicações Java podem compilar os códigos fontes Java.

REFERÊNCIAS

http://download.oracle.com/javase/6/docs/api/javax/tools/package-summary.html

http://www.ibm.com/developerworks/java/library/j-jcomp/index.html

http://www.docjar.com/docs/api/javax/tools/JavaFileManager.html

Nenhum comentário:

Postar um comentário