Tutorial avançado de classe Java: um guia para recarregar classe
Publicados: 2022-03-11Em projetos de desenvolvimento Java, um fluxo de trabalho típico envolve reiniciar o servidor a cada mudança de classe, e ninguém reclama disso. Isso é um fato sobre o desenvolvimento Java. Trabalhamos assim desde o nosso primeiro dia com Java. Mas o recarregamento da classe Java é tão difícil de alcançar? E esse problema poderia ser desafiador e empolgante para resolver para desenvolvedores Java qualificados? Neste tutorial de classe Java, tentarei resolver o problema, ajudá-lo a obter todos os benefícios do recarregamento de classe em tempo real e aumentar imensamente sua produtividade.
O recarregamento de classe Java não é discutido com frequência e há muito pouca documentação explorando esse processo. Estou aqui para mudar isso. Este tutorial de aulas de Java fornecerá uma explicação passo a passo desse processo e ajudará você a dominar essa técnica incrível. Tenha em mente que implementar o recarregamento de classe Java requer muito cuidado, mas aprender como fazê-lo o colocará nas grandes ligas, tanto como desenvolvedor Java quanto como arquiteto de software. Também não fará mal entender como evitar os 10 erros mais comuns de Java.
Configuração do espaço de trabalho
Todo o código-fonte deste tutorial é carregado no GitHub aqui.
Para executar o código enquanto você segue este tutorial, você precisará de Maven, Git e Eclipse ou IntelliJ IDEA.
Se você estiver usando o Eclipse:
- Execute o comando
mvn eclipse:eclipse
para gerar os arquivos de projeto do Eclipse. - Carregue o projeto gerado.
- Defina o caminho de saída para
target/classes
.
Se você estiver usando o IntelliJ:
- Importe o arquivo
pom
do projeto. - O IntelliJ não compilará automaticamente quando você estiver executando qualquer exemplo, então você deve:
- Execute os exemplos dentro do IntelliJ, então toda vez que você quiser compilar, você terá que pressionar
Alt+BE
- Execute os exemplos fora do IntelliJ com o
run_example*.bat
. Defina a compilação automática do compilador do IntelliJ como true. Então, toda vez que você alterar qualquer arquivo java, o IntelliJ o compilará automaticamente.
Exemplo 1: recarregando uma classe com o Java Class Loader
O primeiro exemplo lhe dará uma compreensão geral do carregador de classes Java. Aqui está o código fonte.
Dada a seguinte definição de classe de User
:
public static class User { public static int age = 10; }
Podemos fazer o seguinte:
public static void main(String[] args) { Class<?> userClass1 = User.class; Class<?> userClass2 = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example1.StaticInt$User"); ...
Neste exemplo de tutorial, haverá duas classes User
carregadas na memória. userClass1
será carregado pelo carregador de classes padrão da JVM e userClass2
usando o DynamicClassLoader
, um carregador de classes personalizado cujo código fonte também é fornecido no projeto GitHub e que descreverei em detalhes abaixo.
Aqui está o resto do método main
:
out.println("Seems to be the same class:"); out.println(userClass1.getName()); out.println(userClass2.getName()); out.println(); out.println("But why there are 2 different class loaders:"); out.println(userClass1.getClassLoader()); out.println(userClass2.getClassLoader()); out.println(); User.age = 11; out.println("And different age values:"); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass1)); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass2)); }
E a saída:
Seems to be the same class: qj.blog.classreloading.example1.StaticInt$User qj.blog.classreloading.example1.StaticInt$User But why there are 2 different class loaders: qj.util.lang.DynamicClassLoader@3941a79c sun.misc.Launcher$AppClassLoader@1f32e575 And different age values: 11 10
Como você pode ver aqui, embora as classes User
tenham o mesmo nome, elas são na verdade duas classes diferentes e podem ser gerenciadas e manipuladas independentemente. O valor age, embora declarado como estático, existe em duas versões, anexando separadamente a cada classe, e também pode ser alterado independentemente.
Em um programa Java normal, ClassLoader
é o portal que traz classes para a JVM. Quando uma classe requer que outra classe seja carregada, é tarefa do ClassLoader
fazer o carregamento.
No entanto, neste exemplo de classe Java, o ClassLoader
customizado denominado DynamicClassLoader
é usado para carregar a segunda versão da classe User
. Se em vez de DynamicClassLoader
, usarmos o carregador de classes padrão novamente (com o comando StaticInt.class.getClassLoader()
), a mesma classe User
será usada, pois todas as classes carregadas são armazenadas em cache.
O DynamicClassLoader
Pode haver vários carregadores de classe em um programa Java normal. O que carrega sua classe principal, ClassLoader
, é o padrão e, a partir do seu código, você pode criar e usar quantos carregadores de classe desejar. Esta, então, é a chave para o recarregamento de classes em Java. O DynamicClassLoader
é possivelmente a parte mais importante de todo este tutorial, portanto, devemos entender como o carregamento dinâmico de classes funciona antes de podermos atingir nosso objetivo.
Ao contrário do comportamento padrão de ClassLoader
, nosso DynamicClassLoader
herda uma estratégia mais agressiva. Um carregador de classe normal daria ao seu pai ClassLoader
a prioridade e carregaria apenas classes que seu pai não pudesse carregar. Isso é adequado para circunstâncias normais, mas não no nosso caso. Em vez disso, o DynamicClassLoader
tentará examinar todos os caminhos de classe e resolver a classe de destino antes de desistir do direito ao pai.
Em nosso exemplo acima, o DynamicClassLoader
é criado com apenas um caminho de classe: "target/classes"
(em nosso diretório atual), portanto, é capaz de carregar todas as classes que residem nesse local. Para todas as classes que não estão lá, ele terá que se referir ao carregador de classes pai. Por exemplo, precisamos carregar a classe String
em nossa classe StaticInt
, e nosso carregador de classes não tem acesso ao rt.jar
em nossa pasta JRE, então a classe String
do carregador de classes pai será usada.
O código a seguir é de AggressiveClassLoader
, a classe pai de DynamicClassLoader
, e mostra onde esse comportamento é definido.
byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }
Observe as seguintes propriedades de DynamicClassLoader
:
- As classes carregadas têm o mesmo desempenho e outros atributos que outras classes carregadas pelo carregador de classes padrão.
- O
DynamicClassLoader
pode ser coletado como lixo junto com todas as suas classes e objetos carregados.
Com a capacidade de carregar e usar duas versões da mesma classe, agora estamos pensando em despejar a versão antiga e carregar a nova para substituí-la. No próximo exemplo, faremos exatamente isso... continuamente.
Exemplo 2: recarregando uma classe continuamente
Este próximo exemplo Java mostrará a você que o JRE pode carregar e recarregar classes para sempre, com classes antigas despejadas e coletadas como lixo, e novas classes carregadas do disco rígido e colocadas em uso. Aqui está o código fonte.
Aqui está o loop principal:
public static void main(String[] args) { for (;;) { Class<?> userClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example2.ReloadingContinuously$User"); ReflectUtil.invokeStatic("hobby", userClass); ThreadUtil.sleep(2000); } }
A cada dois segundos, a classe User
antiga será despejada, uma nova será carregada e seu método hobby
invocado.
Aqui está a definição da classe User
:
@SuppressWarnings("UnusedDeclaration") public static class User { public static void hobby() { playFootball(); // will comment during runtime // playBasketball(); // will uncomment during runtime } // will comment during runtime public static void playFootball() { System.out.println("Play Football"); } // will uncomment during runtime // public static void playBasketball() { // System.out.println("Play Basketball"); // } }
Ao executar esta aplicação, você deve tentar comentar e descomentar o código indicado na classe User
. Você verá que a definição mais recente sempre será usada.
Aqui está uma saída de exemplo:
... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball
Toda vez que uma nova instância de DynamicClassLoader
é criada, ela carrega a classe User
da pasta target/classes
, onde configuramos o Eclipse ou o IntelliJ para gerar o arquivo de classe mais recente. Todas as classes antigas de DynamicClassLoader
e User
serão desvinculadas e submetidas ao coletor de lixo.
Se você estiver familiarizado com o JVM HotSpot, vale ressaltar aqui que a estrutura de classes também pode ser alterada e recarregada: o método playFootball
deve ser removido e o método playBasketball
adicionado. Isso é diferente do HotSpot, que permite que apenas o conteúdo do método seja alterado, ou a classe não pode ser recarregada.
Agora que somos capazes de recarregar uma classe, é hora de tentar recarregar várias classes de uma só vez. Vamos experimentá-lo no próximo exemplo.
Exemplo 3: recarregando várias classes
A saída deste exemplo será a mesma do Exemplo 2, mas mostrará como implementar esse comportamento em uma estrutura mais semelhante a um aplicativo com objetos de contexto, serviço e modelo. O código-fonte deste exemplo é bastante grande, então mostrei apenas partes dele aqui. O código fonte completo está aqui.
Aqui está o método main
:
public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }
E o método createContext
:
private static Object createContext() { Class<?> contextClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example3.ContextReloading$Context"); Object context = newInstance(contextClass); invoke("init", context); return context; }
O método invokeHobbyService
:
private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue("hobbyService", context); invoke("hobby", hobbyService); }
E aqui está a classe Context
:

public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }
E a classe HobbyService
:
public static class HobbyService { public User user; public void hobby() { user.hobby(); } }
A classe Context
neste exemplo é muito mais complicada do que a classe User
nos exemplos anteriores: ela possui links para outras classes e possui o método init
a ser chamado a cada instanciação. Basicamente, é muito semelhante às classes de contexto do aplicativo do mundo real (que mantém o controle dos módulos do aplicativo e faz a injeção de dependência). Portanto, poder recarregar essa classe Context
junto com todas as classes vinculadas é um grande passo para aplicar essa técnica à vida real.
À medida que o número de classes e objetos cresce, nossa etapa de “descartar versões antigas” também se tornará mais complicada. Esta também é a maior razão pela qual o recarregamento de classe é tão difícil. Para possivelmente descartar versões antigas, teremos que garantir que, uma vez que o novo contexto seja criado, todas as referências às classes e objetos antigos sejam descartadas. Como lidamos com isso com elegância?
O método main
aqui terá o objeto de contexto, e esse é o único link para todas as coisas que precisam ser descartadas. Se quebrarmos esse link, o objeto de contexto e a classe de contexto e o objeto de serviço… estarão todos sujeitos ao coletor de lixo.
Uma pequena explicação sobre por que normalmente as classes são tão persistentes e não recebem coleta de lixo:
- Normalmente, carregamos todas as nossas classes no carregador de classes Java padrão.
- O relacionamento class-classloader é um relacionamento bidirecional, com o class loader também armazenando em cache todas as classes que ele carregou.
- Portanto, enquanto o classloader ainda estiver conectado a qualquer thread ativo, tudo (todas as classes carregadas) ficará imune ao coletor de lixo.
- Dito isso, a menos que possamos separar o código que queremos recarregar do código já carregado pelo carregador de classes padrão, nossas novas alterações de código nunca serão aplicadas durante o tempo de execução.
Com este exemplo, vemos que recarregar todas as classes do aplicativo é realmente muito fácil. O objetivo é apenas manter uma conexão fina e passível de ser solta do encadeamento ativo para o carregador de classes dinâmico em uso. Mas e se quisermos que alguns objetos (e suas classes) não sejam recarregados e sejam reutilizados entre os ciclos de recarga? Vejamos o próximo exemplo.
Exemplo 4: Separando Espaços de Classe Persistentes e Recarregados
Segue o código fonte..
O método main
:
public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }
Então você pode ver que o truque aqui é carregar a classe ConnectionPool
e instanciá-la fora do ciclo de recarregamento, mantendo-a no espaço persistente, e passar a referência para os objetos Context
O método createContext
também é um pouco diferente:
private static Object createContext(ConnectionPool pool) { ExceptingClassLoader classLoader = new ExceptingClassLoader( (className) -> className.contains(".crossing."), "target/classes"); Class<?> contextClass = classLoader.load("qj.blog.classreloading.example4.reloadable.Context"); Object context = newInstance(contextClass); setFieldValue(pool, "pool", context); invoke("init", context); return context; }
A partir de agora, chamaremos os objetos e classes que são recarregados a cada ciclo de “espaço recarregável” e outros - os objetos e classes não reciclados e não renovados durante os ciclos de recarga - de “espaço persistente”. Teremos que ser muito claros sobre quais objetos ou classes ficam em qual espaço, traçando assim uma linha de separação entre esses dois espaços.
Como visto na figura, não apenas o objeto Context
e o objeto UserService
estão se referindo ao objeto ConnectionPool
, mas as classes Context
e UserService
também estão se referindo à classe ConnectionPool
. Esta é uma situação muito perigosa que muitas vezes leva à confusão e ao fracasso. A classe ConnectionPool
não deve ser carregada pelo nosso DynamicClassLoader
, deve haver apenas uma classe ConnectionPool
na memória, que é aquela carregada pelo ClassLoader
padrão. Este é um exemplo de por que é tão importante ter cuidado ao projetar uma arquitetura de recarga de classe em Java.
E se nosso DynamicClassLoader
carregar acidentalmente a classe ConnectionPool
? Então o objeto ConnectionPool
do espaço persistido não pode ser passado para o objeto Context
, porque o objeto Context
está esperando um objeto de uma classe diferente, que também é chamada de ConnectionPool
, mas na verdade é uma classe diferente!
Então, como impedimos que nosso DynamicClassLoader
carregue a classe ConnectionPool
? Em vez de usar DynamicClassLoader
, este exemplo usa uma subclasse dele chamada: ExceptingClassLoader
, que passará o carregamento para o super classloader com base em uma função de condição:
(className) -> className.contains("$Connection")
Se não usarmos ExceptingClassLoader
aqui, o DynamicClassLoader
carregaria a classe ConnectionPool
porque essa classe reside na pasta “ target/classes
”. Outra maneira de evitar que a classe ConnectionPool
seja selecionada pelo nosso DynamicClassLoader
é compilar a classe ConnectionPool
para uma pasta diferente, talvez em um módulo diferente, e ela será compilada separadamente.
Regras para Escolher o Espaço
Agora, o trabalho de carregamento de classe Java fica realmente confuso. Como determinamos quais classes devem estar no espaço persistente e quais classes no espaço recarregável? Aqui estão as regras:
- Uma classe no espaço recarregável pode fazer referência a uma classe no espaço persistente, mas uma classe no espaço persistente pode nunca fazer referência a uma classe no espaço recarregável. No exemplo anterior, a classe
Context
recarregável faz referência à classeConnectionPool
persistente, masConnectionPool
não tem referência aContext
- Uma classe pode existir em qualquer espaço se não referenciar nenhuma classe no outro espaço. Por exemplo, uma classe de utilitário com todos os métodos estáticos como
StringUtils
pode ser carregada uma vez no espaço persistente e carregada separadamente no espaço recarregável.
Então você pode ver que as regras não são muito restritivas. Exceto para as classes de cruzamento que têm objetos referenciados nos dois espaços, todas as outras classes podem ser usadas livremente no espaço persistente ou no espaço recarregável ou em ambos. Obviamente, apenas as classes no espaço recarregável gostarão de ser recarregadas com ciclos de recarga.
Assim, o problema mais desafiador com o recarregamento de classe é resolvido. No próximo exemplo, tentaremos aplicar essa técnica a um aplicativo da Web simples e aproveitaremos o recarregamento de classes Java como qualquer linguagem de script.
Exemplo 5: Pequena lista telefônica
Segue o código fonte..
Este exemplo será muito semelhante à aparência de um aplicativo da Web normal. É um aplicativo de página única com AngularJS, SQLite, Maven e Jetty Embedded Web Server.
Aqui está o espaço recarregável na estrutura do servidor web:
O servidor web não conterá referências aos servlets reais, que devem ficar no espaço recarregável, para serem recarregados. O que ele contém são servlets de stub, que, com cada chamada para seu método de serviço, resolverão o servlet real no contexto real a ser executado.
Este exemplo também apresenta um novo objeto ReloadingWebContext
, que fornece ao servidor Web todos os valores como um Context normal, mas mantém internamente referências a um objeto de contexto real que pode ser recarregado por um DynamicClassLoader
. É este ReloadingWebContext
que fornece servlets stub para o servidor web.
O ReloadingWebContext
será o wrapper do contexto real e:
- Recarregará o contexto real quando um HTTP GET para “/” for chamado.
- Fornecerá servlets stub para o servidor web.
- Definirá valores e invocará métodos toda vez que o contexto real for inicializado ou destruído.
- Pode ser configurado para recarregar o contexto ou não, e qual carregador de classe é usado para recarregar. Isso ajudará ao executar o aplicativo em produção.
Como é muito importante entender como isolamos o espaço persistente e o espaço recarregável, aqui estão as duas classes que estão cruzando os dois espaços:
Classe qj.util.funct.F0
para objeto public F0<Connection> connF
in Context
- Objeto de função, retornará uma conexão toda vez que a função for invocada. Essa classe reside no pacote qj.util, que é excluído do
DynamicClassLoader
.
Classe java.sql.Connection
para objeto public F0<Connection> connF
in Context
- Objeto de conexão SQL normal. Essa classe não reside no caminho de classe do nosso
DynamicClassLoader
, portanto, não será selecionada.
Resumo
Neste tutorial de classes Java, vimos como recarregar uma única classe, recarregar uma única classe continuamente, recarregar um espaço inteiro de várias classes e recarregar várias classes separadamente das classes que devem ser persistidas. Com essas ferramentas, o fator chave para obter uma recarga de classe confiável é ter um design super limpo. Então você pode manipular livremente suas classes e toda a JVM.
Implementar o recarregamento de classes Java não é a coisa mais fácil do mundo. Mas se você tentar e, em algum momento, descobrir que suas aulas estão sendo carregadas rapidamente, você já está quase lá. Haverá muito pouco a fazer antes que você possa obter um design limpo e totalmente soberbo para o seu sistema.
Boa sorte meus amigos e aproveitem seu novo superpoder!