Nível 4 · Produção · Módulo 12
Rumo à produção
Duração estimada: 3h a 4h
Você vai sair sabendo
- →Compilar o plugin como jar, fora do Eclipse, com Maven e Tycho
- →Instalar e atualizar o plugin num servidor iDempiere
- →Proteger as regras do plugin com testes automatizados
- →Evitar os problemas de desempenho mais comuns em plugins
- →Contribuir de volta com o projeto iDempiere
Checkpoint: 0 de 6
Antes de começar
- Módulos 00 a 11 concluídos. O repositório
guia-idempiere-projetotem o plugin, os 2Packs de 1.0.0 a 1.0.7 e os scripts de migração. - Para a parte 2, um segundo iDempiere 14 instalado a partir do instalador ou do Docker, fora do Eclipse. Serve de "produção" de teste.
O que você vai construir
Até aqui, o plugin só existe dentro do Eclipse. Neste módulo ele vira software de verdade:
- um jar gerado por linha de comando, reproduzível em qualquer máquina e num servidor de integração contínua;
- instalado num iDempiere que nunca viu o seu Eclipse;
- com testes que rodam sem ninguém clicar em nada;
- e você fecha o guia do outro lado: contribuindo com o projeto que usou até aqui.
Parte 1: o jar
1. O pom do plugin
Plugins do iDempiere compilam com Tycho, a extensão do Maven que entende OSGi: ele lê o MANIFEST.MF e resolve as dependências a partir de um repositório p2, e não do Maven Central. O repositório p2 do core é gerado pelo ./mvnw verify que você rodou no módulo 00, em org.idempiere.p2/target/repository. É o mesmo mecanismo que o plugin REST usa.
Crie com.gardenworld.visitas/pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gardenworld</groupId>
<artifactId>com.gardenworld.visitas</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
<properties>
<tycho.version>4.0.8</tycho.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<idempiere.core.repository.url>file://${user.home}/sources/idempiere/org.idempiere.p2/target/repository</idempiere.core.repository.url>
</properties>
<repositories>
<repository>
<id>idempiere-core</id>
<url>${idempiere.core.repository.url}</url>
<layout>p2</layout>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.eclipse.tycho</groupId>
<artifactId>tycho-maven-plugin</artifactId>
<version>${tycho.version}</version>
<extensions>true</extensions>
</plugin>
<plugin>
<groupId>org.eclipse.tycho</groupId>
<artifactId>target-platform-configuration</artifactId>
<version>${tycho.version}</version>
<configuration>
<executionEnvironment>JavaSE-17</executionEnvironment>
</configuration>
</plugin>
</plugins>
</build>
</project>
Três detalhes que quebram o build se esquecidos:
- A versão do pom acompanha a do manifesto.
1.0.0-SNAPSHOTno pom corresponde a1.0.0.qualifiernoBundle-Version. O Tycho recusa se forem diferentes. tycho.versionigual à do core (a doorg.idempiere.parent/pom.xml). Misturar versões gera erros difíceis de entender.- Tudo o que o jar precisa levar está no
build.properties. Confira que obin.includestemMETA-INF/,OSGI-INF/ereports/(módulo 07).
2. Compile
Use o mesmo Maven Wrapper do core, para não depender de um Maven instalado. Copie para a raiz do plugin:
cd ~/sources/guia-idempiere-projeto/com.gardenworld.visitas
cp ~/sources/idempiere/mvnw .
cp -r ~/sources/idempiere/.mvn .
./mvnw verify
O jar sai em target/com.gardenworld.visitas-1.0.0-SNAPSHOT.jar. Antes de instalar em qualquer lugar, confira o conteúdo:
unzip -l target/com.gardenworld.visitas-1.0.0-SNAPSHOT.jar | grep -E "OSGI-INF|2Pack|reports|MANIFEST"
Tem de aparecer: os arquivos de OSGI-INF/ (as factories e o event manager), os META-INF/2Pack_1.0.x.zip e o reports/VisitasPorTecnico.jrxml. Faltou algo, volte ao build.properties. Este é o erro mais comum de quem sai do Eclipse: lá tudo funciona porque o Eclipse lê a pasta do projeto; o jar só leva o que foi declarado.
Dentro do jar, abra o META-INF/MANIFEST.MF: o qualifier virou uma data e hora (1.0.0.202610151430). Cada build gera uma versão maior que a anterior, e é assim que o OSGi sabe qual é a mais nova.
Parte 2: instalar num servidor
3. Instale pelo Felix Web Console
Todo iDempiere tem um console web de administração do OSGi, o Felix Web Console, em:
https://seu-servidor:8443/osgi/system/console/bundles
Entre com um usuário que tenha o perfil System Administrator (no banco de demonstração, SuperUser). Na lista de bundles:
- Clique em Install/Update....
- Escolha o jar, marque Start Bundle e confirme.
- Procure
com.gardenworld.visitasna lista. O status deve ser Active.
Não precisa reiniciar o servidor. Ao iniciar, o plugin aplica os 2Packs de 1.0.0 a 1.0.7, em ordem (módulo 04). Confira como System na aba Package Installation da janela Pack In: uma linha por versão, todas com status de concluído. Depois, como GardenAdmin, rode Role Access Update e abra a Visita Técnica.
Para atualizar: gere um jar novo e repita Install/Update.... O OSGi troca a versão, e o 2Pack novo (se houver) é aplicado; os já aplicados não rodam de novo.
4. Distribuindo para outras pessoas
Se o plugin for útil para outras empresas, há dois caminhos oficiais:
- Repositório de extensões do iDempiere (módulo 11): um
metadata.jsoncom o link do jar, enviado por pull request para o idempiere-extension-repository. O plugin passa a aparecer no Extension Management de qualquer iDempiere. - Repositório p2 próprio, publicado num servidor web, para instalação por linha de comando. É o que o plugin REST faz, com o script
update-rest-extensions.shdo repositório dele.
Parte 3: testes
5. Um projeto de testes
O core tem um framework de testes pronto no projeto org.idempiere.test. A classe AbstractTestCase faz o que nenhum teste do iDempiere pode esquecer: entra como GardenAdmin na GardenWorld antes de cada teste e desfaz tudo (rollback) depois. O banco fica como estava.
Crie um segundo plugin, com.gardenworld.visitas.test (mesmo assistente do módulo 04, sem ativador), com estas dependências no manifesto:
Require-Bundle: org.adempiere.base;bundle-version="14.0.0",
org.idempiere.test;bundle-version="14.0.0",
com.gardenworld.visitas
Import-Package: org.junit.jupiter.api;version="5.9.0"
O plugin de testes é separado para que o jar de produção não leve teste nem dependa do JUnit.
O plugin de testes só enxerga as classes que o plugin principal exporta. No MANIFEST.MF de com.gardenworld.visitas, acrescente:
Export-Package: com.gardenworld.visitas.model
Exporte só o que outro plugin precisa usar. O resto (factories, event handlers, formulário) continua interno.
E o teste:
package com.gardenworld.visitas.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.compiere.process.DocAction;
import org.compiere.util.Env;
import org.compiere.util.TimeUtil;
import org.idempiere.test.AbstractTestCase;
import org.idempiere.test.DictionaryIDs;
import org.junit.jupiter.api.Test;
import com.gardenworld.visitas.model.MVisita;
import com.gardenworld.visitas.model.X_EDU_VisitaLinha;
public class MVisitaTest extends AbstractTestCase {
private MVisita novaVisita(boolean urgente) {
MVisita visita = new MVisita(Env.getCtx(), 0, getTrxName());
visita.setC_BPartner_ID(DictionaryIDs.C_BPartner.JOE_BLOCK.id);
visita.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id);
visita.setEDU_DataVisita(TimeUtil.getDay(System.currentTimeMillis()));
visita.setEDU_TipoVisita("MA");
visita.setEDU_IsUrgente(urgente);
visita.saveEx();
return visita;
}
private void incluirLinha(MVisita visita) {
X_EDU_VisitaLinha linha = new X_EDU_VisitaLinha(Env.getCtx(), 0, getTrxName());
linha.set_ValueOfColumn("EDU_Visita_ID", visita.getEDU_Visita_ID());
linha.set_ValueOfColumn("Line", 10);
linha.set_ValueOfColumn("M_Product_ID", DictionaryIDs.M_Product.OAK.id);
linha.set_ValueOfColumn("Qty", Env.ONE);
linha.saveEx();
}
@Test
public void naoCompletaSemLinhas() throws Exception {
MVisita visita = novaVisita(false);
boolean ok = visita.processIt(DocAction.ACTION_Complete);
assertFalse(ok, "Visita sem linhas não pode completar");
assertNotNull(visita.getProcessMsg(), "Deve explicar o motivo");
}
@Test
public void completaComLinha() throws Exception {
MVisita visita = novaVisita(false);
incluirLinha(visita);
boolean ok = visita.processIt(DocAction.ACTION_Complete);
visita.saveEx();
assertTrue(ok, visita.getProcessMsg());
assertEquals(DocAction.STATUS_Completed, visita.getDocStatus(), "Status");
assertTrue(visita.isEDU_IsConcluida(), "Concluída");
}
@Test
public void urgenteAguardaAprovacao() throws Exception {
MVisita visita = novaVisita(true);
incluirLinha(visita);
visita.processIt(DocAction.ACTION_Complete);
assertEquals(DocAction.STATUS_InProgress, visita.getDocStatus(), "Urgente sem aprovação fica em processo");
}
}
O que cada teste protege:
naoCompletaSemLinhas: a regra que já mudou de lugar duas vezes (módulos 04, 08 e 09). Se alguém mexer de novo, o teste acusa.completaComLinha: o caminho feliz. Parece óbvio, até que uma mudança num event handler quebre o complete sem ninguém perceber.urgenteAguardaAprovacao: a regra de negócio mais cara de errar. Visita urgente completa sem aprovação é um problema de controle interno, não um bug visual.
Repare que nenhum teste abre tela: eles chamam a MVisita direto. É mais um motivo para as regras morarem nela.
Para rodar: em Run > Run Configurations > JUnit Plug-in Test, duplique a configuração idempiere.unit.test do core. Na aba Test, aponte para o projeto com.gardenworld.visitas.test. Na aba Plug-ins, marque os seus dois plugins. Rode: os três devem passar, contra o seu banco de desenvolvimento.
6. O que testar
Testar tudo não é o objetivo. Teste o que dói quando quebra:
| Teste | Exemplo no guia |
|---|---|
| Regra que protege dinheiro, estoque ou controle | Aprovação de urgentes |
| Regra que já mudou de lugar ou de forma | Visita precisa de linha |
| Integração com efeito externo | Notificação do módulo 11 (teste que monta o JSON certo) |
| Correção de bug | Todo bug corrigido ganha um teste que o reproduz |
Parte 4: desempenho e diagnóstico
7. Os erros que mais pesam em produção
| Erro | Sintoma | Como evitar |
|---|---|---|
| Consulta dentro de laço (N+1) | Processo lento que piora a cada mês | Uma consulta com IN ou JOIN; ou carregue a lista uma vez antes do laço |
| Buscar o registro inteiro para ler um número | Lentidão em telas e callouts | DB.getSQLValue para um valor só; Query.count() ou .match() para existência |
| Consulta sem índice | Tudo lento com poucos usuários | Índice pelo Dicionário (módulo 08) nas colunas dos filtros frequentes |
| Transação longa | Usuários travados esperando | Processos longos em lotes; nunca espere resposta de outro sistema dentro da transação (módulo 11) |
Ignorar get_TrxName() | Dados inconsistentes, travamentos | Toda consulta e todo new MAlgo(...) recebem a transação corrente |
| Ler configuração ou cadastro sem cache | Milhares de consultas iguais | MSysConfig e os get() estáticos do core já usam cache; para tabelas suas, CCache |
System.out.println | Log perdido, sem nível | CLogger, com log.fine() para depuração e log.warning() para o que alguém precisa ver |
8. Encontrando uma consulta travada
Quando "o sistema travou", quase sempre é uma transação esperando outra. No PostgreSQL:
SELECT pid, state, wait_event_type, now() - query_start AS tempo, left(query, 120) AS consulta
FROM pg_stat_activity
WHERE datname = 'idempiere' AND state <> 'idle'
ORDER BY query_start;
As linhas com wait_event_type = 'Lock' estão esperando. A mais antiga sem espera é, em geral, a que segura o lock. Com o pid dela, a consulta mostra o que ela está fazendo e há quanto tempo.
9. Medindo antes de otimizar
Não otimize por intuição. O JDK traz um profiler de graça, o Java Flight Recorder:
jcmd <pid-do-idempiere> JFR.start duration=120s filename=/tmp/idempiere.jfr
Reproduza a lentidão durante os dois minutos e abra o arquivo no JDK Mission Control. Ele mostra onde o tempo foi gasto: CPU, espera por banco ou por lock. VisualVM e JProfiler fazem o mesmo, com outras interfaces.
Parte 5: contribuindo com o iDempiere
10. Do bug ao pull request
Você passou doze módulos lendo e usando o código do iDempiere. Quando encontrar um bug ou uma melhoria no core, o caminho é este (detalhes no CONTRIBUTING.md do repositório):
- Confira se já existe. Procure no JIRA do projeto e reproduza num dos servidores de teste, de preferência com a GardenWorld.
- Para melhoria, converse antes. Proponha no fórum ou no Mattermost. Ticket de melhoria sem conversa prévia costuma ficar parado.
- Abra o ticket no JIRA: título claro, passos para reproduzir, o esperado e o obtido. Você recebe um número, como
IDEMPIERE-1234. - Corrija num fork, numa branch a partir da
master. Se mexer no Dicionário, o script de migração gerado com o Log Migration Script (módulo 01) vai junto: troquePlaceholderForTicketno nome do arquivo pelo número do ticket e gere a versão Oracle também. - Abra o pull request com o ticket no título (
IDEMPIERE-1234 Descrição curta) e preencha o checklist do template: testes, documentação, revisão própria. - Responda à revisão. Todo código do core é revisado por outros contribuidores. É a parte mais valiosa: você aprende com quem mantém o projeto há anos.
11. Commit final
cd ~/sources/guia-idempiere-projeto
git add .
git commit -m "Build com Tycho e testes da MVisita"
git tag m12
Checkpoint
Erros comuns
| Sintoma | Causa provável | Correção |
|---|---|---|
mvnw verify não resolve org.adempiere.base | Repositório p2 do core não existe ou caminho errado | Rode ./mvnw verify no core e confira idempiere.core.repository.url |
| Erro de versão entre pom e manifesto | 1.0.0-SNAPSHOT × 1.0.0.qualifier diferentes | Alinhe os dois |
Jar sem OSGI-INF ou sem os 2Packs | Pasta fora do bin.includes | Passo 1 |
| Bundle fica Installed no servidor | Dependência que existe no Eclipse mas não no servidor (por exemplo, o plugin REST) | Clique no bundle no console: a página mostra a dependência que falta |
| 2Pack não aplicado no servidor | Plugin não iniciou, ou falhou no meio | Log do servidor e aba Package Installation |
| Teste passa sozinho e falha em conjunto | Teste dependendo de dado criado por outro | Cada teste cria o que precisa; o rollback limpa |
| Teste grava de verdade no banco | Objeto criado sem getTrxName() | Sempre a transação do AbstractTestCase |
Desafio
Configure um workflow do GitHub Actions que, a cada push, baixa o core, roda ./mvnw verify nele e depois no seu plugin, e publica o jar como artefato. Um pipeline de build que não depende da máquina de ninguém é o que separa um plugin de uma customização.
Referência
Antes de cada versão em produção
- Build limpo pelo
mvnw verify, e não pelo Eclipse - Testes passando
- 2Pack novo com versão maior que a anterior, e nenhum 2Pack antigo alterado
- Instalado e testado em homologação com cópia do banco de produção
- Backup do banco de produção antes de instalar
- Mudanças em objetos do core listadas no README do plugin (módulo 10)
- Tag no Git com a versão
Para continuar
- Documentação oficial do iDempiere e o wiki
- O próprio código do core: quando a documentação não responder, a classe
Mda tabela e os testes emorg.idempiere.testrespondem - A comunidade no Mattermost e no fórum
Fim da trilha
Você começou sem nada instalado e terminou com um plugin que tem tabelas, janelas, regras de negócio, documento com aprovação, relatórios, integração, testes e build reproduzível. É o mesmo caminho de qualquer customização séria no iDempiere, do tamanho que for.
O próximo passo é seu: aplicar isso num problema real, e devolver ao projeto um pouco do que ele te deu.