Parte Um. Prompt Preciso ler este artigo?
O carregador de classes Java é crucial para a operação do sistema Java, mas muitas vezes é ignorado por nós. O carregador de classes Java carrega classes em tempo de execução, localizando-as e carregando-as. Carregadores de classes personalizados podem mudar completamente a forma como as classes são carregadas, personalizando sua máquina virtual Java da maneira que você desejar. Este artigo apresenta brevemente o carregador de classes Java e o ilustra por meio de um exemplo de construção de um carregador de classes personalizado. Esse carregador de classes compilará automaticamente o código antes de carregar a classe. Você aprenderá o que um carregador de classes realmente faz e como criar o seu próprio. Contanto que você tenha algum conhecimento básico de Java, saiba como criar, compilar e executar um programa Java de linha de comando e alguns conceitos básicos de arquivos de classe Java, você poderá entender o conteúdo deste artigo. Depois de ler este artigo, você será capaz de:
* Expanda as funções da máquina virtual Java
* Crie um carregador de classes personalizado
* Como integrar um carregador de classes personalizado em sua aplicação
* Modifique seu carregador de classes para ser compatível com Java 2
Parte 2. Introdução O que é um carregador de classes?
A diferença entre Java e outras linguagens é que Java roda na Java Virtual Machine (JVM). Isso significa que o código compilado é salvo em um formato independente de plataforma, em vez de um formato executado em uma máquina específica. Este formato tem muitas diferenças importantes em relação ao formato de código executável tradicional. Especificamente, diferentemente de um programa C ou C++, um programa Java não é um arquivo executável independente, mas consiste em muitos arquivos de classe separados, cada arquivo de classe correspondendo a uma classe Java. Além disso, esses arquivos de classe não são carregados na memória imediatamente, mas quando o programa precisa deles. Um carregador de classes é uma ferramenta usada na máquina virtual Java para carregar classes na memória. Além disso, o carregador de classes Java também é implementado em Java. Dessa forma, você pode criar facilmente seu próprio carregador de classes sem ter um conhecimento profundo da máquina virtual Java.
Por que criar um carregador de classes?
Agora que o Java Virtual Gold já possui um carregador de classes, precisamos criar outros nós mesmos. Boa pergunta. O carregador de classes padrão só sabe como carregar classes do sistema local. Quando seu programa é compilado de forma totalmente nativa, o carregador de classes padrão geralmente funciona bem. Mas uma das coisas mais interessantes sobre Java é como é fácil carregar classes da rede em vez de apenas localmente.
Por exemplo, um navegador pode carregar classes por meio de um carregador de classes customizado. Existem também muitas maneiras de carregar classes. Uma das coisas mais interessantes sobre Java é que você pode personalizá-lo simplesmente no local ou na rede:
* Verifique automaticamente as assinaturas digitais antes de executar código não confiável
* Descriptografe o código com base na senha fornecida pelo usuário
* Crie classes dinamicamente de acordo com as necessidades do usuário. Qualquer coisa de seu interesse pode ser facilmente integrada ao seu aplicativo na forma de bytecode. Exemplos de carregadores de classes personalizados se você tiver usado o appletviewer JDK (Java Software Development Kit) (navegador de aplicativos pequenos) ou outro.
Para navegadores Java integrados, você já usa um carregador de classes customizado. Quando a Sun lançou pela primeira vez a linguagem Java, uma das coisas mais interessantes foi observar como o Java executava código baixado de um site remoto. Executar a partir de site remoto via HTTP
O bytecode transmitido pela conexão P parece um pouco estranho. Isso funciona porque Java tem a capacidade de instalar carregadores de classes customizados. O navegador de miniaplicativos contém um carregador de classes. Este carregador de classes não localiza classes Java localmente. Em vez disso, ele acessa o servidor remoto, carrega o arquivo de bytecode original por meio de HTTP e o converte em uma classe Java na máquina virtual Java. É claro que os carregadores de classes fazem muitas outras coisas: eles bloqueiam classes Java inseguras e evitam que diferentes miniaplicativos em páginas diferentes interfiram uns nos outros. Echidna, um pacote escrito por Luke Gorrie, é um pacote de software Java aberto que permite que vários aplicativos Java sejam executados com segurança em uma máquina virtual Java. Ele evita interferência entre aplicativos usando um carregador de classes personalizado para fornecer a cada aplicativo uma cópia do arquivo de classe.
Nosso exemplo de carregador de classes Agora que sabemos como funciona um carregador de classes e como definir nosso próprio carregador de classes, criamos um carregador de classes customizado chamado CompilingClassLoader (CCL). O CCL faz o trabalho de compilação para nós, então não precisamos compilá-lo manualmente. Isso é basicamente equivalente a ter um programa "make" integrado em nosso ambiente de execução.
Nota: Antes de prosseguirmos para a próxima etapa, é necessário compreender alguns conceitos relacionados.
O sistema foi bastante aprimorado no JDK versão 1.2 (que é o que chamamos de plataforma Java 2). Este artigo foi escrito no JDK 1.0 e 1.1, mas tudo funcionará em versões posteriores. ClassLoader também foi melhorado em Java2.
A introdução detalhada é fornecida na quinta parte.
Parte 3. Visão Geral da Estrutura do ClassLoader O propósito básico de um carregador de classes é atender solicitações de classes Java. Quando a máquina virtual Java precisa de uma classe, ela fornece um nome de classe ao carregador de classes e, em seguida, o carregador de classes tenta retornar uma instância de classe correspondente. Carregadores de classes customizados podem ser criados substituindo os métodos correspondentes em diferentes estágios. A seguir aprenderemos sobre alguns dos principais métodos do carregador de classes. Você entenderá o que esses métodos fazem e como funcionam ao carregar arquivos de classe. Você também saberá qual código precisa escrever ao criar um carregador de classes personalizado. Na próxima parte, você aproveitará esse conhecimento e nosso CompilingCl personalizado
assLoader trabalha em conjunto.
Método loadClass
ClassLoader.loadClass() é o ponto de entrada do ClassLoader. A assinatura do método é a seguinte:
Classe loadClass(Nome da string, resolução booleana);
O nome do parâmetro especifica o nome completo da classe (incluindo o nome do pacote) exigida pela máquina virtual Java, como Foo ou java.lang.Object.
O parâmetro resolve especifica se a classe precisa ser resolvida. Você pode entender a resolução da classe como estando completamente pronta para execução. A análise geralmente não é necessária. Se a máquina virtual Java quiser apenas saber se esta classe existe ou quiser saber sua classe pai, a análise é completamente desnecessária. No Java 1.1 e em suas versões anteriores, se você desejar customizar o carregador de classes, o método loadClass é o único método que precisa ser substituído na subclasse.
(ClassLoader mudou em Java1.2 e forneceu o método findClass()).
métododefineClass
defineClass é um método muito misterioso no ClassLoader. Este método constrói uma instância de classe a partir de uma matriz de bytes. Essa matriz de bytes brutos contendo dados pode vir do sistema de arquivos ou da rede. defineClass ilustra a complexidade, o mistério e a dependência da plataforma da Java Virtual Machine - ele interpreta o bytecode para transformá-lo em estruturas de dados de tempo de execução, verifica a validade e muito mais. Mas não se preocupe, você não precisa fazer nada disso. Na verdade, você não pode substituí-lo,
Porque o método é modificado pela palavra-chave final.
Método encontrarSystemClass
O método findSystemClass carrega arquivos do sistema local. Ele procura por arquivos de classe no sistema local e, se encontrado, chama
defineClass converte a matriz de bytes original em um objeto de classe. Este é o mecanismo padrão para a Java Virtual Machine carregar classes ao executar aplicativos Java. Para carregadores de classes personalizados, só precisamos usar findSystemClass após falha no carregamento. A razão é simples: nosso carregador de classes é responsável por executar certas etapas no carregamento de classes, mas não todas as classes. por exemplo,
Mesmo que nosso carregador de classes carregue algumas classes do site remoto, ainda existem muitas classes básicas que precisam ser carregadas do sistema local.
Essas classes não nos preocupam, então deixamos a máquina virtual Java carregá-las da maneira padrão: a partir do sistema local. Isto é o que findSystemClass faz. Todo o processo é aproximadamente o seguinte:
* A máquina virtual Java solicita que nosso carregador de classes customizado carregue a classe.
* Verificamos se o site remoto possui a classe que precisa ser carregada.
*Se houver, ficamos com essa aula.
* Caso contrário, pensamos que esta classe está na biblioteca de classes básica e chamamos findSystemClass para carregá-la do sistema de arquivos.
Na maioria dos carregadores de classe personalizados, você deve chamar findSystemClass primeiro para economizar tempo procurando remotamente.
Na verdade, como veremos na próxima seção, a máquina virtual Java só tem permissão para carregar classes do sistema de arquivos local quando temos certeza de que compilamos nosso código automaticamente.
Método resolveClass
Conforme mencionado acima, os registros de classe podem ser divididos em carregamento parcial (sem análise) e carregamento completo (incluindo análise). Quando criamos um carregador de classes personalizado, podemos precisar chamar resolveClass.
Método findLoadedClass
findLoadedClass implementa um cache: quando loadClass é necessário para carregar uma classe, você pode primeiro chamar esse método para ver se a classe foi carregada para evitar o recarregamento de uma classe já carregada. Este método deve ser chamado primeiro. Vamos dar uma olhada em como esses métodos são organizados juntos.
Nosso exemplo de implementação de loadClass executa as etapas a seguir. (Não especificamos uma tecnologia específica para obter o arquivo de classe - pode ser da rede, de um pacote compactado ou compilado dinamicamente. Em qualquer caso, o que obtemos é o arquivo de bytecode original)
* Chame findLoadedClass para verificar se esta classe foi carregada.
* Se não for carregado, obteremos o array de bytes original de alguma forma.
* Se o array foi obtido, chame defineClass para convertê-lo em um objeto de classe.
* Se a matriz de bytes original não puder ser obtida, chame findSystemClass para verificar se ela pode ser gravada no sistema de arquivos local.
* Se o parâmetro resolve for verdadeiro, chame resolveClass para resolver o objeto de classe.
* Se a classe não foi encontrada, lance uma ClassNotFoundException.
* Caso contrário, retorne esta classe.
Agora que temos uma compreensão mais abrangente do conhecimento prático dos carregadores de classes, podemos criar um carregador de classes customizado. Na próxima seção, discutiremos o CCL.
Parte 4. CompilandoClassLoader
CCL nos mostra a função do carregador de classes. O objetivo do CCL é permitir que nosso código seja compilado e atualizado automaticamente. Veja como funciona:
* Quando houver uma solicitação de classe, primeiro verifique se o arquivo de classe existe no diretório e subdiretórios atuais do disco.
* Se não houver arquivo de classe, mas houver um arquivo de código-fonte, chame o compilador Java para compilar e gerar o arquivo de classe.
* Se o arquivo de classe já existir, verifique se o arquivo de classe é mais antigo que o arquivo de código-fonte. Se o arquivo de classe for mais antigo que o arquivo de código-fonte, chame o compilador Java para gerar novamente o arquivo de classe.
* Se a compilação falhar ou o arquivo de classe não puder ser gerado a partir do arquivo de origem devido a outros motivos, lance a exceção ClassNotFou
ndExceção.
* Se você ainda não obteve esta classe, ela pode existir em outras bibliotecas de classes. Chame findSystemClass para ver se ela pode ser encontrada.
* Se não for encontrado, lance ClassNotFoundException.
* Caso contrário, retorne esta classe.
Como a compilação Java é implementada?
Antes de prosseguirmos, precisamos entender o processo de compilação Java. Normalmente, o compilador Java compila apenas as classes especificadas. Ele também compilará outras classes relacionadas, se exigido pelas classes especificadas. O CCL compilará as classes que precisamos compilar no aplicativo, uma por uma. No entanto, de modo geral, depois que o compilador compila a primeira classe,
O CCL descobrirá que outras classes relacionadas necessárias foram realmente compiladas. Por que? O compilador Java usa regras semelhantes às nossas: se uma classe não existir ou o arquivo fonte tiver sido atualizado, a classe será compilada. O compilador Java está basicamente um passo à frente do CCL, e a maior parte do trabalho é feita pelo compilador Java. Parece que o CCL está compilando essas classes.
Na maioria dos casos, você descobrirá que ele está chamando o compilador na classe de função principal e pronto - uma simples chamada é suficiente. Entretanto, há um caso especial em que essas classes não são compiladas na primeira vez que aparecem. Se você carregar uma classe com base em seu nome, usando o método Class.forName, o compilador Java não saberá se a classe é necessária. nesse caso,
Você descobre que o CCL chama o compilador novamente para compilar a classe. O código na Seção 6 ilustra esse processo.
Usando CompilationClassLoader
Para usar o CCL, não podemos executar nosso programa diretamente, ele deve ser executado de uma forma especial, como esta:
% java Foo arg1 arg2
Nós executamos assim:
% java CCLRun Foo arg1 arg2
CCLRun é um programa stub especial que cria CompilingClassLoader e o utiliza para carregar nossa classe de função principal. Isso garante que todo o programa seja carregado pelo CompilingClassLoader. CCLRun utiliza Ja
A API de reflexão va chama a função principal da classe de função principal e passa parâmetros para esta função. Para saber mais, consulte o código-fonte na Parte 6.
Vamos executar o exemplo para demonstrar como funciona todo o processo.
O programa principal é uma classe chamada Foo, que cria uma instância da classe Bar. Esta instância Bar, por sua vez, cria uma instância da classe Baz, que existe no pacote baz. Isso demonstra como o CCL carrega classes de subpacotes. A barra também carrega a classe Boo com base no nome da classe
, isso também é feito pelo CCL. Todas as classes estão carregadas e prontas para serem executadas. Use o código-fonte do Capítulo 6 para executar este programa. Compile CCLRun e CompilingClassLoader. Certifique-se de não compilar outras classes (Foo, Bar, Baz, a
nd Boo), caso contrário o CCL não funcionará.
% java CCLRun Foo arg1 arg2
CCL: Compilando Foo.java...
cara! arg1 arg2
barra!
baz! arg1 arg2
CCL: Compilando Boo.java...
Vaia!
Observe que o compilador é chamado pela primeira vez para Foo.java, e Bar e baz.Baz também são compilados juntos. E como Boo
Quando o canal precisa ser carregado, o CCL chama o compilador novamente para compilá-lo.
Parte 5. Visão Geral das Melhorias no Carregador de Classes em Java 2 No Java 1.2 e versões posteriores, o carregador de classes foi bastante aprimorado. O código antigo ainda funciona, mas o novo sistema facilita nossa implementação. Este novo modelo é o modelo de delegação de proxy, o que significa que se o carregador de classes não conseguir encontrar uma classe, ele solicitará que seu carregador de classes pai a encontre. O carregador de classes do sistema é o ancestral de todos os carregadores de classes. O carregador de classes do sistema carrega classes por padrão, ou seja, a partir do sistema de arquivos local. Substituir o método loadClass geralmente tenta várias maneiras de carregar a classe. Se você escrever muitos carregadores de classe, descobrirá que apenas faz algumas modificações neste método complicado repetidas vezes. A implementação padrão de loadClass em Java 1.2 inclui a maneira mais comum de localizar uma classe, permitindo substituir o método findClass e loadClass para chamar o método findClass apropriadamente. A vantagem disso é que você não precisa substituir loadClass, você só precisa substituir findClass, o que reduz a carga de trabalho.
Novo método: findClass
Este método é chamado pela implementação padrão de loadClass. O objetivo de findClass é incluir todo o código específico do carregador de classes,
Não há necessidade de repetir o código (como chamar o carregador de classes do sistema quando o método especificado falhar).
Novo método: getSystemClassLoader
Independentemente de você substituir os métodos findClass e loadClass, o método getSystemClassLoader pode acessar diretamente o carregador de classes do sistema (em vez de acesso indireto por meio de findSystemClass).
Novo método: getParent
Para delegar a solicitação ao carregador de classes pai, o carregador de classes pai deste carregador de classes pode ser obtido por meio deste método. Você pode delegar a solicitação ao carregador de classes pai quando um método específico em um carregador de classes customizado não puder localizar a classe. O carregador de classes pai de um carregador de classes contém o código que cria o carregador de classes.
Parte 6. Código fonte
CompilandoClassLoader.java
A seguir está o conteúdo do arquivo CompilingClassLoader.java
importar java.io.*;
/*
CompilingClassLoader compila dinamicamente arquivos de origem Java. Ele verifica se o arquivo .class existe e se o arquivo .class é mais antigo que o arquivo de origem.
*/
classe pública CompilingClassLoader estende ClassLoader
{
//Especifique um nome de arquivo, leia todo o conteúdo do arquivo no disco e retorne uma matriz de bytes.
byte privado[] getBytes(String nome do arquivo) lança IOException {
// Obtenha o tamanho do arquivo.
Arquivo arquivo = novo arquivo (nome do arquivo);
comprimento longo = arquivo.comprimento();
//Cria um array apenas o suficiente para armazenar o conteúdo do arquivo.
byte bruto[] = novo byte[(int)len];
//Abre arquivo
FileInputStream fin = new FileInputStream(arquivo);
// Ler todo o conteúdo. Se não puder ser lido, ocorreu um erro.
int r = fin.read(bruto);
se (r!=len)
throw new IOException( "Não é possível ler tudo, "+r+" != "+len );
// Não esqueça de fechar o arquivo.
fin.close();
// Retorna esta matriz.
retornar bruto;
}
// Gere um processo para compilar o arquivo de origem Java especificado e especifique os parâmetros do arquivo. Se a compilação for bem-sucedida, retorne verdadeiro, caso contrário,
// Retorna falso.
compilação booleana privada (String javaFile) lança IOException {
//Mostra o progresso atual
System.out.println( "CCL: Compilando "+javaFile+"..." );
//Inicia o compilador
Processo p = Runtime.getRuntime().exec( "javac "+javaFile );
//Aguarda o término da compilação
tentar {
p.waitFor();
} catch(InterruptedException, ou seja) { System.out.println(ou seja,});
// Verifique o código de retorno para ver se há erros de compilação.
int ret = p.exitValue();
//Retorna se a compilação foi bem sucedida.
retornar ret==0;
}
// O código principal do carregador de classes - o carregamento de classes compila automaticamente os arquivos de origem quando necessário.
classe pública loadClass (nome da string, resolução booleana)
lança ClassNotFoundException {
//Nosso objetivo é obter um objeto de classe.
Classe classe = nulo;
// Primeiro, verifique se esta classe foi processada.
classe = findLoadedClass(nome);
//System.out.println( "findLoadedClass: "+clas);
// Obtém o nome do caminho através do nome da classe, por exemplo: java.lang.Object => java/lang/Object
String fileStub = name.replace( ''''.'''', ''''/'''' );
//Constrói objetos apontando para arquivos de origem e arquivos de classe.
String javaFilename = fileStub+".java";
String classFilename = fileStub+".class";
Arquivo javaArquivo = novo arquivo(javaFilename);
Arquivo classFile = novo arquivo(classFilename);
//System.out.println( "j "+javaFile.lastModified()+"c"
//+classFile.lastModified() );
// Primeiro, determine se a compilação é necessária. Se o arquivo de origem existir, mas o arquivo de classe não existir, ou ambos existirem, mas o arquivo de origem
// Mais recente, indicando que precisa ser compilado.
if (javaFile.exists() &&(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
tentar {
// Compile, se a compilação falhar, devemos declarar o motivo da falha (apenas usar classes obsoletas não é suficiente).
if (!compile(javaFilename) || !classFile.exists()) {
throw new ClassNotFoundException( "Falha na compilação: "+javaFilename);
}
} catch(IOException, ou seja) {
// Um erro de IO pode ocorrer durante a compilação.
lançar new ClassNotFoundException(ou seja.toString());
}
}
// Certifique-se de que foi compilado corretamente ou não requer compilação, começamos a carregar bytes brutos.
tentar {
//Lê bytes.
byte raw[] = getBytes(classFilename);
//Converte para objeto de classe
clas = defineClass(nome, raw, 0, raw.length);
} catch(IOException, ou seja) {
// Isso não significa falha, talvez a classe com a qual estamos lidando esteja na biblioteca de classes local, como java.lang.Object.
}
//System.out.println( "defineClass: "+classes);
//Talvez na biblioteca de classes, carregada da forma padrão.
if (classe==nulo) {
classe = findSystemClass(nome);
}
//System.out.println( "findSystemClass: "+clas );
// Se o parâmetro resolve for verdadeiro, interprete a classe conforme necessário.
if (resolver && classe! = nulo)
resolverClass(class);
// Se a classe não foi obtida, algo deu errado.
if (classe == nulo)
lançar nova ClassNotFoundException(nome);
// Caso contrário, retorne este objeto de classe.
aula de retorno;
}
}
CCRun.java
Aqui está o arquivo CCRun.java
importar java.lang.reflect.*;
/*
CCLRun carrega classes por meio de CompilingClassLoader para executar o programa.
*/
classe pública CCLRun
{
static public void main(String args[]) lança exceção {
//O primeiro parâmetro especifica a classe de função principal que o usuário deseja executar.
String progClass = args[0];
//Os próximos parâmetros são os parâmetros passados para esta classe de função principal.
String progArgs[] = new String[args.length-1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
//Cria CompilingClassLoader
CompilingClassLoader ccl = new CompilingClassLoader();
//Carrega a classe de função principal através do CCL.
Classe classe = ccl.loadClass(progClass);
// Use reflexão para chamar sua função principal e passar parâmetros.
// Gera um objeto de classe representando o tipo de parâmetro da função principal.
Classe mainArgType[] = { (new String[0]).getClass() };
// Encontre a função principal padrão na classe.
Método main = clas.getMethod( "main", mainArgType );
// Cria uma lista de parâmetros - neste caso, um array de strings.
Objeto argsArray[] = { progArgs };
//Chama a função principal.
main.invoke(nulo,argsArray);
}
}
Foo.java
A seguir está o conteúdo do arquivo Foo.java
classe pública Foo
{
static public void main(String args[]) lança exceção {
System.out.println( "foo! "+args[0]+" "+args[1] );
nova Barra( args[0], args[1] );
}
}
Barra.java
A seguir está o conteúdo do arquivo Bar.java
importar baz.*;
classe pública Bar
{
barra pública (String a, String b) {
System.out.println( "barra! "+a+" "+b );
novo Baz(a,b);
tentar {
Classe booClass = Class.forName("Boo");
Objeto boo = booClass.newInstance();
} catch(Exceção e) {
e.printStackTrace();
}
}
}
baz/Baz.java
A seguir está o conteúdo do arquivo baz/Baz.java
pacote baz;
aula pública Baz
{
public Baz(String a, String b) {
System.out.println( "baz! "+a+" "+b );
}
}
Boo.java
A seguir está o conteúdo do arquivo Boo.java
classe pública Boo
{
publicBoo() {
System.out.println("Boo!");
}
}
Parte 7. Resumo Resumo Depois de ler este artigo, você percebeu que a criação de um carregador de classes customizado permite que você se aprofunde nos detalhes internos da Java Virtual Machine. Você pode carregar um arquivo de classe de qualquer recurso ou gerá-lo dinamicamente, para que possa fazer muitas coisas de seu interesse estendendo essas funções, e também pode completar algumas funções poderosas.
Outros tópicos sobre ClassLoader Conforme mencionado no início deste artigo, os carregadores de classes customizados desempenham um papel importante em navegadores Java integrados e navegadores de miniaplicativos.