Executando verificação de segurança...
1

Criei um framework em java para mapeamento relacional de arquivos de propriedades em classes

Olá a todos, boa noite!

Estou aqui fazendo este post pra anunciar meu novo framework de mapeamento relacional de arquivos de propriedades em classes java.

A inspiração para começar a desenvolver este framework veio do início do meu aprendizado no desenvolvimento android. Percebi que o uso de uma classe estática para acessar recursos compilados como strings, layouts e afins facilitava e muito o desenvolvimento.

Pensei então: seria interessante usar a API de processamento XML do java pra criar esse projeto.
Mas a manipulação de XML tende a se tornar complexa quando escalamos o projeto para algo maior do que uma simples manipulação rotineira de arquivos simples.

Então resolvi usar arquivos de propriedades pra isso. Por quê? Porque as propriedades são estruturas mais simples, maleáveis, idiomáticas e altamente escaláveis.

Dito isso, pra manter a similaridade do projeto à sua inspiração, tudo é estruturado em uma classe externa chamada P (que faz alusão a properties), seguindo de classes internas estáticas e finais, cujo nomes são a formatação dos nomes dos arquivos de propriedades mapeados (de acordo com a convenção de nomenclatura java) e com campos denotando os pares chave-valor que compõem as propriedades de um arquivo de propriedades. Esses campos têm os modificadores public static final, o tipo do campo é determinado a partir de um padrão que o dev insere como cabeçalho do arquivo (mais a frente explicarei sobre), o nome do campo, que é a chave da propriedade mapeada, é formatado também de acordo com a convenção para campos constantes e o valor do campo, que é o valor da propriedade mapeada.

Como funciona?

Antes de qualquer coisa, o dev precisa colocar um comentário como placeholder em seu arquivo de propriedades, indicando o tipo de dados de todas as propriedades ali existentes.
O padrão lido pela ferramenta é o seguinte:
# $javatype:@<tipo_de_dados_java>

Por exemplo, se os dados contidos nas propriedades devem ser do tipo String, então deve ser definido # $javatype:@String. Após a geração dos dados, todas as variáveis na classe interna daquele arquivo mapeado serão do tipo String. (Recomendo definir o tipo dos dados no topo do arquivo para uma leitura mais rápida)

Você pode usar tipos primitivos e seus wrappers como dados.

Um adendo ao uso de double, float e long: a ferramenta não determina (por enquanto) o caractere de identificação de tipo de dados pra esses tipos (10.0d, 2.3f, 98L, etc). Então, caso alguns desses tipos de dados sejam utilizados, deve ser inserido o caractere de identificação junto aos dados correspondentes. Não é necessário inserir, por exemplo, 98L como 98'L' ou algo assim. Defina como você definiria normalmente em uma variável do tipo correspondente.

Bem, feitas as explicações sobre como os tipos de dados são definidos para as variáveis mapeadas, vamos começar pela inicialização dos dados. inicialmente precisamos chamar o método estático init(). Este método recebe 1 parâmetro Path, 1 parâmetro String e um parâmetro boolean. O primeiro parâmetro Path indica o caminho do arquivo de propriedades a ser mapeado ou do diretório que contém tais arquivos. O segundo parâmetro String indica o nome do pacote onde o código fonte será gerado (o framework tem prefixado por padrão o diretório src/main/java e, na etapa da resolução dos caminhos, o prefixo padrão é resolvido com o nome do pacote fornecido pelo dev). O parâmetro pode apontar pra um diretório de pacote não existente, a ferramenta se encarrega de criar o(s) diretório(s), caso ele(s) não exista(m). Após essa resolução, ao final do nome do pacote fornecido pelo dev, é resolvido o nome "generated", para indicar que aquele pacote contém classes geradas. O último parâmetro que representa um booleano indica se a geração de dados deve ou não ser recursiva. Se essa opção for 'true', além do diretório fornecido, todos os subdiretórios contidos no diretório fornecido serão mapeados em busca de arquivos de propriedades. Caso contrário, apenas o diretório fornecido será analisado.
Essa opção não surte efeito quando um arquivo é analisado ao invés de um diretório.

Após tudo ser configurado, devemos chamar o método generate(). É um método sem parâmetros.

A geração e modificação do código fonte é feita usando uma implementaçãoda Abstract Syntax Tree (JavaParser) e a compilação inicial é feita com uma chamada para a ferramenta JavaCompiler.

Após a geração, duas threads são executadas em segundo plano

  • Watch Service Thread;
  • File Events Processor Thread;

A primeira thread executa uma implementação que fiz do WatchService para monitorar o diretório onde o arquivo (se for somente um) ou os arquivos (se for somente um diretório completo, sendo ou não recursivo) residem, em busca de mudanças.
Esses eventos de mudanças são mapeados de acordo com o tipo de evento capturado junto ao caminho do arquivo que disparou o evento. Esse mapeamento é registrado em uma pilha síncrona de eventos pendentes para processamento, onde a thread File Events Processor atua, esperando que a Watch Service Thread poste novos elementos para processamento (esquema produtor-consumidor)

Essas duas threads ficam em execução até que um erro seja lançado ou ate que o dev pare a execução.

Mais um adendo: as chamadas a estes métodos devem ser feitas na aplicação principal do desenvolvedor pelo menos 1 vez (quando o framework estiver em execução, você pode comentar os métodos de chamada para que eles não sejam chamados novamente em uma nova run, mantendo duas execuções simultâneas do mesmo projeto). O dev pode fechar seu projeto em execução, mas nunca parar a execução das threads restantes na IDE, ou então o framework não vai processar os arquivos reativamente.

Quaisquer alterações em quaisquer arquivos de propriedades mapeados relacionalmente durante a execução do framework resultará em eventos de modificação de arquivos (ENTRY_CREATE, ENTRY_DELETE e ENTRY_MODIFY).
Pra não matar a performance, eu fiz a implementação da nova API ClassFile do Java para alterações de alta precisão no código compilado, evitando a recompilação de toda a classe P.java. Isso evita engasgos na performance caso sejam gerados milhares de campos em dezenas ou centenas de modelos de propriedades.

Reiniciar o framework com uma geração pré existente não culmina em regeneração total, mas sim na resincronização de elementos alterados durante a não execução da ferramenta.

O sistema conta com modelos de cache de arquivos em JSON usando a API Gson e uma estrutura de modelos de dados de propriedades carregados em um esquema de chunk, onde cada chunk representa um modelo record de um arquivo de propriedade in-memory e uma lista interna com todos os modelos de campos referente àquela propriedade mapeada.

Também implementei um processador de anotações pra pesquisar pelo hash dos campos e das classes mapeadas e um ClassLoader customizado, para "driblar" a limitação de recarregamento de arquivo de classe compilada em tempo de execução. Antes dessa implementação do CustomClassLoader, após a primeira geração ou após algumas modificações nos arquivos, você precisava parar a execução do framework e reiniciar ele, para que a classe compilada fosse atualizada. Agora, isso não é mais necessário.

O cache fica no diretório base do projeto em um diretório oculto chamado .json-propertiesCache.

Bom, acho que não tem mais o que falar sobre o projeto 😅. Tentei ser o mais completo o possível aqui nas palavras, mas ainda sinto que faltam algumas coisas.

Só mais um aviso: o projeto pode contar com alguns erros que, com o tempo, vou resolvendo com alguns patchs. Acho importante falar disso aqui e não somente das coisas boas que o projeto tem. Então se vocês encontrarem erros no uso, podem deixar issues no GitHub caso queiram e eu vou resolvê-las o mais rapido o possivel ❤️

Meus planos futuros:

Ainda pretendo implementar um padrão builder (além desse padrão factory), um identificador de instâncias (pra evitar reexecutar o framework se já tiver uma instância dele ativa. Isso evita ter que comentar o código de chamada de método), mais refinamento nos dados fornecidos pelo dev, mais controle sobre o processo de execução da ferramenta, melhorias no tratamento de exceções, verificadores de integridade (reduz erros de fonte e de compilação)... Enfim, são diversas coisas que pretendo fazer. Mas meu maior objetivo é transformar este Framework em um plugin que pode ser utilizado no eclipse. Isso melhoraria absurdamente a experiência de uso pelo dev, sendo uma ferramenta integrada a IDE. Por enquanto seu uso está limitado em adicionar o jar como dependência do projeto.
Também estou tentando adicionar o jar ao repositório central do maven, mas, por enquanto, sem sucesso.

O projeto roda no java 24 por conta de algumas melhorias que a equipe do JDK fez na API ClassFile.

Vou estar deixando o link do repositório aqui pra quem quiser testar:

https://github.com/GrimReaper3223/PropsClassGenerator

Espero que gostem 🥹...

Carregando publicação patrocinada...