domingo, 19 de setembro de 2010

Execução de Classes Java via API de Reflexão.

INTRODUÇÃO

Nesse artigo, mostraremos como executar uma instância de uma classe Java usando a API de Reflexão do Java. Partiremos de um arquivo Java compilador (".class") passado como parâmetro. Esse arquivo ".class" será carregado por um carregador de classes personalizado, chamado de ClassLoader. Nesse caso, extenderemos a classe java.lang.ClassLoader para criarmos o nosso carregador de classes especifico com o nome de MyClassLoader. Durante a execução da aplicação, depois de carregado o arquivo ".class" passado como parâmetro, usaremos a API de Reflexão para executar um método estático dessa classe.


CARREGADOR DE CLASSES

Primeiramente, vamos criar um ClassLoader personalizado, chamado "MyClassLoader.java", veja a seguir:

Arquivo "MyClassLoader.java"
--------------------------------------------------------------------------------------------------------

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

public class MyClassLoader extends ClassLoader {

    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;

    private Map> mapClasses = new HashMap>();

    public MyClassLoader() {

    }

    /**
     * @param name
     *            Nome da Classe ou Nome do Recurso.
     */
    protected synchronized Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // Verifica se a classe já foi carregada
        Class c1 = mapClasses.get(name);

        if (c1 == null) {
            // Nova classe.

            // Retorna se for uma classe de sistema.
            try {
                return findSystemClass(name);

            } catch (ClassNotFoundException e) {
            }

            // Carrega os bytes através do nome do recurso.
            byte[] classBytes = loadClassBytes(name);

            if (classBytes == null) {
                throw new ClassNotFoundException(name);
            }

            // Através do bytecodes, descobrir o nome da classe!
            String className = getClassName(classBytes);

            c1 = defineClass(className, classBytes, 0, classBytes.length);

            if (c1 == null) {
                throw new ClassNotFoundException(name);
            }

            // Nome do recurso - Classe.
            mapClasses.put(name, c1);

            // Nome da classe - Classe.
            mapClasses.put(className, c1);

            if (resolve) {
                resolveClass(c1);
            }
        }

        return c1;
    }

    /**
     * Implementação padrão.
     * 
     * Procurar dentro da pasta scripts. Usar caminho relativo.
     * 
     * 
     */
    private byte[] loadClassBytes(String name) {
        try {
            FileInputStream in = new FileInputStream(name);

            if (in != null) {
                ByteArrayOutputStream buffer = new ByteArrayOutputStream();

                int ch = in.read();

                while (ch != -1) {
                    buffer.write((byte) ch);

                    ch = in.read();
                }

                in.close();

                return buffer.toByteArray();
            }

        } catch (IOException e) {
        }

        return null;
    }

    /**
     * Localizar nome de arquivo através dos bytecodes.
     * 
     */
    private String getClassName(byte b[]) {
        // return "Test";
        int j = 0;

        Map mapIndexAndObject = new HashMap();

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

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

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

        j = j + 10;

        for (int i = 1; i < constant_pool_count; i++) {

            switch (MASK & b[j]) {
            case CONSTANT_UTF8_INFO: {
                j++;

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

                j = j + 2;

                Reader reader;

                try {
                    reader = new InputStreamReader(new ByteArrayInputStream(b,
                            j, length), "UTF-8");
                } catch (UnsupportedEncodingException e1) {
                    e1.printStackTrace();

                    // Usa o padrão (UTF-16) se não existir.
                    reader = new InputStreamReader(new ByteArrayInputStream(b,
                            j, length));
                }

                j = j + length;

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

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

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

                try {
                    reader.close();

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
                ;
                break;
            case CONSTANT_INTEGER_INFO: {
                j = j + 5;
            }
                ;
                break;
            case CONSTANT_FLOAT_INFO: {
                j = j + 5;

            }
                ;
                break;
            case CONSTANT_LONG_INFO: {
                j = j + 9; 

                i++;
            }
                ;
                break;
            case CONSTANT_DOUBLE_INFO: {
                j = j + 9; 

                i++;
            }
                ;
                break;
            case CONSTANT_CLASS_INFO: {
                j++;

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

                mapIndexAndObject.put(i, aux);

                j = j + 2;
            }
                ;
                break;
            case CONSTANT_STRING_INFO: {
                j = j + 3;
            }
                ;
                break;
            case CONSTANT_FIELDREF_INFO: {
                j = j + 5;

            }
                ;
                break;
            case CONSTANT_METHODREF_INFO: {
                j = j + 5;

            }
                ;
                break;
            case CONSTANT_INTERFACE_METHODREF_INFO: {
                j = j + 5;

            }
                ;
                break;
            case CONSTANT_NAMEANDTYPE_INFO: {
                j = j + 5;

            }
                ;
                break;
            }
        }

        j = j + 2;

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

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

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

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

Nessa implementação, vamos sobrescrever o seguinte método da classe ClassLoader: 

protected synchronized Class loadClass(String name, boolean resolve)  throws ClassNotFoundException

Esse método recebe como parâmetro o nome qualificado (Pacote + Nome da Classe) da classe a ser carregada. O objetivo desse método é apresentar essa classe a JVM com a chamada ao seguinte método:

protected final Class defineClass(String name, byte[] b, int off, int len) throws ClassFormatError

E também, o ClassLoader personalizado deve administrar as classes que foram carregadas, através do uso de um mapa (Objeto cuja classe implementa a interface java.util.Map) relacionando o nome da classe com a classe carregada, por exemplo. 
Se caso a classe carregada for classe do sistema ou da API do Java, é chamado o seguinte método:

protected final Class findSystemClass(String name) throws ClassNotFoundException

Esse método deve ser executado no começo do processo de carregamento de uma classe, se está for de sistema, o ClassLoader personalizado não deve carregá-la e esse método retornará a classe do sistema já carregada. Se a classe não for do sistema, será lançada a exceção java.lang.ClassNotFoundException com o intuito de indicar que a classe não foi localizada. Nesse caso, o nosso ClassLoader não deve tratar a exceção e partir para o processo de carregamento da classe em questão.

Se a flag "resolve" do método "loadClass(String name, boolean resolve)" estiver configurada com Verdadeiro, esse método deve chamar o seguinte método da superclasse:

protected final void resolveClass(Class c) 

Esse método carregará as outras classes que a classe carregada depende. Mas a chamadas ao método "loadClass(String name, boolean resolve)" são feitas pela JVM e não pela aplicação. 


API DE REFLEXÃO JAVA

Nesse momento já temos o nosso carregador de classes personalizado. A seguir temos um programa que lê da linha de comandos o arquivo ".class" a ser carregado pelo nosso carregador e depois, de posse com o objeto da classe java.lang.Class localizamos o método estático "main" da classe carregada, chamando o seguinte método:

Method m = c.getMethod("main", new Class[] { args.getClass() });

A variável "c" é a classe carregada contendo o método estático "main" cujo argumentos é um array de java.lang.String!
Nesse chamada, temos o método a ser invocado pela chamada a seguir:

m.invoke(null, new Object[] { args });

Essa chamada invocará o método localizado na classe carregada!
A seguir, temos o programa que fará essa execução:

Arquivo "ExecutionTest.java"
--------------------------------------------------------------------------------------------------------
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ExecutionTest {

    /**
     * @param args
     */
    public static void main(String[] args) {
        if (args.length < 1) {
            throw new IllegalArgumentException(
            "Deve especificar o nome do arquivo");
        }

        ClassLoader classLoader = new MyClassLoader();

        Class c = null;

        try {
            c = classLoader.loadClass(args[0]);

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

        // Faz invocação do método estático "main"
        Method m = null;

        // Localiza método estático main.

        try {
            m = c.getMethod("main", new Class[] { args.getClass() });

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

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

        // Executa método estático main.
        try {
            m.invoke(null, new Object[] { args });

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

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

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

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

Além desse arquivo, abaixo temos um arquivo java de teste que deve ser compilado para ser usado no programa ExecutionTest.

Arquivo "Test.java"
--------------------------------------------------------------------------------------------------------

public class Test {

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

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

Para executar esse exemplo nesse artigo, basta compilar a classe "Test.java" e o resultado que é o arquivo "Test.class" deve ser usado como argumento na execução do programa "ExecutionTest" lembrando que o "MyClassLoader.class" deve estar no classpath desse programa.


REFERÊNCIAS


http://en.wikipedia.org/wiki/Java_Classloader

http://www.javaworld.com/javaworld/jw-10-1996/jw-10-indepth.html

http://download-llnw.oracle.com/javase/6/docs/api/java/lang/ClassLoader.html

http://download.oracle.com/javase/tutorial/reflect/

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

terça-feira, 7 de setembro de 2010

Formato de Arquivo Class - Java 6

INTRODUÇÃO

Nesse artigo vamos conhecer um pouco da estrutura do arquivo ".class" gerados pelos compiladores Java. Nesse arquivo contém informações a respeito da classe compilada, métodos, campos e outras referências necessárias que serão usadas pela Máquina Virtual do Java no momento do carregamento da classe.
O conhecimento a respeito da estrutura desse arquivo ".class" nos ajuda a entender melhor como os dados estão organizados de maneira que possamos fazer otimizações nesse arquivo, buscar informações, etc.

ESTRUTURA BÁSICA

Nas figuras a seguir, veremos a estrutura básica do formato de arquivo class, versão Java 6.



A estrutura do formato de arquivo class segue uma estrutura análoga as estruturas criadas na linguagem C. Nos primeiros itens às esquerdas, indicam o tipo do elemento, por exemplo, "u1", "u2", "u4" e nos itens às direitas, indicam as variáveis. Basicamente, "u1", "u2" e "u4" armazenam 1, 2 e 4 bytes sem sinal. A seguir, temos a descrição de cada item.

u4 magic: Indica o formato de arquivo class cujo valor em hexadecimal é 0xCAFEBABE.

u2 minor_version: Indica a menor versão suportada.

u2 major_version: Indica a maior versão suportada.

u2 constant_pool_count: Número de constantes armazenadas na tabela de constantes.

cp_info - Constantes: Armazena constant_pool_count - 1 constantes que representam valores constantes como cadeia de caracteres, números, nomes de classes, métodos, etc. Para cada constante, é utilizada uma estrutura especifica que será explicada mais adiante.


u2 acess_flags: Representa propriedades da classe ou interface que está sendo representada  nesse arquivo ".class". Abaixo temos alguns valores usados nesse item.


Nome - Valor - Definição

ACC_PUBLIC                - 0x0001 - Declara classe publica, pode ser acessado fora do pacote.
ACC_FINAL                   - 0X0010 - Declara classe final, subclasses não são permitidas.
ACC_SUPER                 - 0X0020 - Trata métodos da super classe quando invocado pela instrução invokespecial
ACC_INTERFACE        - 0x0200 - É uma interface, não é uma classe.
ACC_ABSTRACT         - 0X0400 - Declara que a classe é abstrata, não pode ser instânciada.
ACC_SYNTHETIC        - 0X1000 - Declara sintético, não presente no código fonte.
ACC_ANNOTATION    - 0X2000 - Declara com um tipo de anotação.
ACC_ENUM                  - 0X4000 - Declara como tipo enum


Observações:

A flag ACC_SYNTHETIC indica que a classe foi gerada pelo compilador e não tem código fonte.
A flag ACC_ENUM indica que a classe ou superclasse é declarada como tipo enum.
A flag ACC_INTERFACE configurada indica uma interface e não classe, e o contrário, indica classe.

Cada arquivo de classe não pode ter as flags ACC_FINAL, ACC_SUPER ou ACC_ENUM configurados.
A flag ACC_INTERFACE indica que é um tipo de anotação e a flag ACC_INTERFACE nesse caso, deve ser configurada.
O arquivo de classe não pode ter as flags ACC_FINAL e ACC_ABSTRACT configuradas.

u2 this_class: Indice para a tabela de constantes indicando a classe / interface em questão.

u2 super_class: Indice para a tabela de constantes indicando a super classe da classe desse arquivo em questão. Se esse valor for 0 (Zero), a super classe é java.lang.Object.

u2 interface_count: Número de interfaces implementadas (ou estendida) pela classe (ou interface).

u2 interfaces [interface_count]: Array de indices para a tabela de constantes com todas as interfaces implementadas (ou estendida) pela classe (ou interface).

u2 fields_count: Número de campos que foi definido nesse arquivo ".class".

field_info - Descrição dos Campos: Estrutura que define cada campo usado no arquivo. No total, temos um número de fields_count para essas estruturas. E a estrutura field_info, será explicada mais adiante nesse artigo.

u2 methods_count: Número de métodos que foi definido nesse arquivo ".class".

method_info - Descrição dos Métodos: Estrutura que define cada método usado no arquivo. No total, temos um número de methods_count para essas estruturas. E a estrutura method_info, será explicada mais adiante nesse artigo.

u2 attribute_count: Número de atributos que foi definido nesse arquivo ".class".

attribute_info - Descrição dos Campos: Estrutura que define cada método usado no arquivo. No total, temos um número de attribute_count para essas estruturas. E a estrutura attribute_info, será explicada mais adiante nesse artigo.


ESTRUTURA - cp_info

Agora vamos explorar a fundo a estrutura geral das constantes que será mostrada na figura a seguir:


u1 tag: Define o tipo da constante.
u1 info[]: Array de bytes armazenados pela constante. Esse array é usado de acordo com o tipo da constante, pois tem constantes que usam apenas 4 bytes e outras usam um número variável de bytes cujo tamanho é geralmente guardado na primeira posição depois da definição do tipo de constante (item tag).

Observações:
Note que não temos como saber o tamanho de bytes a seguir (depois do item tag) sem saber o tipo de constante, o que nos obriga a determinar o tipo de constante (pelo item tag) para depois definirmos com precisão a quantidade de bytes reservada para aquela constante em particular.
Para as constantes do tipo long e double, conta-se dois itens do array de constantes (estrutura cp_info). Ou seja, se a constante long possuir um indice n, o próximo indice n+1 também pertence a constante long.

No total, temos 11 tipos de constantes usadas no formato de arquivo class - Java 6 que estão descritos na figura a seguir:


Nas figuras a seguir, serão mostrados cada um dos 11 tipos de constantes existentes no arquivo class.















ESTRUTURA - field_info



u2 acess_flags: Propriedades do Campo.

Nome - Valor - Descrição

ACC_PUBLIC              - 0x0001 - Declara campo público e pode ser acessado fora do pacote.
ACC_PRIVATE            - 0x0002 - Declara campo privado, uso somente na própria classe.
ACC_PROTECTED     - 0x0004 - Declara campo protegido, uso somente na classe e subclasses.
ACC_STATIC              - 0x0008 - Declara campo estático.
ACC_FINAL                - 0x0010 - Declara campo final, não pode ser alterado depois de inicializado.
ACC_VOLATILE         - 0x0040 - Declara volatil não pode ser cacheada.
ACC_TRANSIENT     - 0x0080 - Declara como temporária, não pode ser escrita ou lida num gerenciador de objetos persistentes.
ACC_SYNTHETIC     - 0x1000 - Declara sintético, não presente no código fonte.
ACC_ENUM               - 0x4000  - Declara com um elemento enum


ESTRUTURA - method_info


u2 acess_flags: Propriedades do Método.

Nome - Valor - Descrição

ACC_PUBLIC                  - 0x0001 - Declara método público.
ACC_PRIVATE                - 0x0002 - Declara método privado.
ACC_PROTECTED         - 0x0004 - Declara método protegido.
ACC_STATIC                  - 0x0008 - Declara método estático.
ACC_FINAL                    - 0x0010 - Declara método final.
ACC_SYNCHRONIZED - 0x0020 - Declara método sincronizado.
ACC_BRIDGE                 - 0x0040 - Declara método "ponte", gerado pelo compilador.
ACC_VARARGS              - 0x0080 - Declarado com variáveis com vários argumentos.
ACC_NATIVE                  - 0x0100 - Declara native, implementando em outra linguagem além de Java.
ACC_ABSTRACT            - 0x0400 - Declara método abstrato, a implementação não é fornecida.
ACC_STRICT                  - 0x0800 - Declara strictfp - Modo de ponto flutuante é FP-strict.
ACC_SYNTHETIC          - 0x1000 - Declara sintético, não presente no código fonte.


ESTRUTURA - attribute_info



Observações:

Na estrutura attribute_info, temos os últimos itens definindo um tipo de atributo em particular. Cada tipo de atributo é usado em uma situação particular (por exemplo, dentro de métodos, campos, etc) e em cada uma dessas, a JVM deve reconhecer os atributos suportados ou ignorá-los. Como exemplo de atributos, temos: ConstantValue, Code, StackMapTable, Exceptions, Synthetic, Signature, LineNumberTable, LocalVariableTable, Deprecated, InnerClasses (Classes internas), EnclosingMethod (Classes anônimas), SourceFile, SourceDebugExtension, RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations, AnnotationDefault, etc. Veja nas referências para conhecer a fundo esses atributos.


EXEMPLO

Depois do estudo do formato de arquivo class, podemos criar uma aplicação de exemplo na qual apartir de uma classe Java compilada em forma de array de bytes, podemos extrair o nome da classe. Veja na classe Java abaixo:

Arquivo - FindClassNameTest.java
-------------------------------------------------------------------------------------------------------------------
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;


public class FindClassNameTest {

    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) {
        if(args.length < 1) {
            throw new IllegalArgumentException("Precisa pelo menos de um nome de arquivo .class");
        }

        InputStream in = null;

        try {
            in = new FileInputStream(args[0]);

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

        InputStream inBuf = new BufferedInputStream(in);

        String name = null;

        try {
            name = getClassName(inBuf);

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

        System.out.println("O nome da classe / interface é: " + name);
    }

    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();
        }
    }
}

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

Na execução do arquivo compilado Java, precisamos passar como argumento o caminho e o nome da classe ou interface na qual queremos extrair o nome partindo do array de bytes do arquivo ".class". Assim, se temos o arquivo ".class" localizado em /home/user/Test.class, então, na execução do programa teremos o seguinte comando:


java FindClassNameTest /home/user/Test.class


Mas, pode ser perguntado, porque extrair o nome da classe / interface apartir do array de byte sendo que no código temos o nome (filename) do arquivo Java ?
Sim, é verdade, podemos extrair o nome diretamente pelo caminho / nome fornecido como parâmetro na execução do interpretador Java. Mas a vantagem maior de extrair o nome da classe / interface apartir do array de bytes é que em muitas situações não temos acesso ao arquivo físico que originou o array de bytes, como o uso de Classloaders, transformação, manipulação de bytecodes, etc. Situações nas quais não temos o arquivo físico e somente o array de bytes. Consequentemente, não precisamos mais depender do arquivo físico originário do array de bytes, pois o próprio array contém todas as informações a respeito da classe compilada!


REFERÊNCIAS

http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#75883

http://jcp.org/aboutJava/communityprocess/final/jsr202/index.html

http://en.wikipedia.org/wiki/Class_%28file_format%29

http://www.murrayc.com/learning/java/java_classfileformat.shtml

http://en.wikipedia.org/wiki/Java_Virtual_Machine

http://asm.ow2.org/

domingo, 15 de agosto de 2010

Distribuindo Aplicação Java usando linguagem de script

Nesse artigo, explicaremos como criar uma aplicação Java, usar uma linguagem de script embutida através do uso da JSR-223 e distribui-la junto com as suas dependências, como arquivos ".jar", scripts e outros recursos para que possa ser uma aplicação standalone.

Primeiramente, vamos usar o IDE Eclipse Helios (versão 3.6) com o suporte a Python instalado. Criamos um Projeto Java com o nome de "JavaScriptEmbedded" como mostram as figuras a seguir:





Agora criaremos o arquivo Test.java para poder acessar os scripts das linguagens de scripts como mostra abaixo:

Arquivo Test.java
----------------------------------------------------------------------------
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;


public class Test {

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

if(args.length < 2) {
throw new IllegalArgumentException("Precisa fornecer a linguagem de script usada e o arquivo do script!");
}

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName(args[0]);

if(scriptEngine == null) {
throw new IllegalArgumentException("Suporte para a linguagem de script " + args[0] + " inexistente");
}

String script = buildScript(args[1]);

List doubleList = new ArrayList(args.length - 2);
for (int i = 2; i < args.length; i++) {
doubleList.add(Double.parseDouble(args[i]));
}

scriptEngine.put("doubleList", doubleList);

try {
scriptEngine.eval(script);

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

public static String buildScript(String file) {
try {
Reader reader = new BufferedReader(new FileReader(file));

return getString(reader);

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

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

return "";
}

public static String getString(Reader reader) throws IOException {
int read = reader.read();
Collection charList = new LinkedList();

while (read != -1) {
charList.add((char) read);
read = reader.read();
}

StringBuffer strBuffer = new StringBuffer(charList.size());

for (Character character : charList) {
strBuffer.append(character);
}

return strBuffer.toString();
}
}

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

A idéia principal desse arquivo ".java" é o usuário fornecer dois parâmetros na linha de comando, um indica o nome da engine da linguagem de script usada e o arquivo da linguagem de script usado.
Esse nome é obtido pelo método "getEngineName()" do objeto da classe ScriptEngineFactory que por sua ver, é uma das fabricas de engines que são obtidas pelo método "getEngineFactories()" do objeto da classe ScriptEngineManager. Mais informações, veja no seguinte artigo desse mesmo blog:
http://josepojr.blogspot.com/2010/01/introducao-java-scripting-api-jsr-223.html

Por exemplo, para a linguagem Python teriamos os seguintes argumentos:
Argumento 1 - "python"
Argumento 2 - "scripts/Script.py"

No projeto Java criado, vamos criar um diretório chamado "scripts" aonde estarão os scripts a serem executados pela JSR-223. Nesse artigo, vamos trabalhar com a linguagem Python, portanto, criamos o arquivo "script.py" para a linguagem Python. Para mais detalhes veja a figura a seguir:




Além dos dois parâmetros fornecidos ao programa Java em linha de comando, os demais parâmetros que devem ser númericos, serão usados para o cálculo da média pelo script.

Arquivo - Script.py
----------------------------------------------------------------------------

auxList = list(doubleList)

sum = 0

for num in auxList:
sum += num

if(len(auxList) > 0):
print u'A média: ' + str(sum/len(auxList))
----------------------------------------------------------------------------

Configuração da Execução de Aplicação Java

O objetivo dessa etapa é construir uma configuração que será usada dentro do Eclipse para executar o programa Java criado e também, para numa outra etapa ser usado na exportação do programa Java em arquivo Jar. Nas figuras a seguir, veremos como construir a configuração de execução de programas Java no Eclipse.














Exportando aplicação Java em arquivo Jar


Nessa etapa, vamos configurar e exportar o projeto Java para um arquivo Jar, como mostram as figuras a seguir:









Agora que temos o arquivo Test.jar gerado e armazenado no diretório /home/user/JavaExport conforme foi descrito nas figuras anteriores, precisamos copiar os seguintes diretórios dentro do diretório raiz da instalação Jython para o diretório /home/user/JavaExport

- Lib
- extlibs


A pasta scripts também deve ser copiada para /home/user/JavaExport, lembrando que dentro dessa pasta de scripts deve estar o arquivo Script.py.

Agora, vamos criar o arquivo run.sh (para Linux) ou run.bat (para Windows). O conteúdo do arquivo run.sh (ou run.bat) será o seguinte comando:

java -jar Test.jar python scripts/Script.py 10 20 30 40 50

Também, o arquivo run.sh deve possuir permissão para ser executado, para isso, execute o seguinte comando:

chmod a+x run.sh

Esse comando diz que todos (proprietário, membro do grupo e outros) terão permissão para executar run.sh.

Para executar o arquivo run.sh digite o seguinte comando:

./run.sh


Se ocorrer algum erro, pode usar a opção -verbose para listar todas as classes carregadas, como mostra abaixo:

java -verbose -jar Test.jar python scripts/Script.py 10 20 30 40 50


Com as pastas Lib, extlibs, scripts e arquivos Test.jar, run.sh e run.bat dentro do diretório /home/user/JavaExport, podemos distribuir a aplicação Java sempre transportando todo o conteúdo da pasta JavaExport.


Referências:

Linguagem Python:
http://www.python.org/

Plugin Python para Eclipse:
http://pydev.org/