Módulos do guia

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

ver .md

Antes de começar

  • Módulos 00 a 11 concluídos. O repositório guia-idempiere-projeto tem 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-SNAPSHOT no pom corresponde a 1.0.0.qualifier no Bundle-Version. O Tycho recusa se forem diferentes.
  • tycho.version igual à do core (a do org.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 o bin.includes tem META-INF/, OSGI-INF/ e reports/ (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:

  1. Clique em Install/Update....
  2. Escolha o jar, marque Start Bundle e confirme.
  3. Procure com.gardenworld.visitas na 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.json com 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.sh do 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:

TesteExemplo no guia
Regra que protege dinheiro, estoque ou controleAprovação de urgentes
Regra que já mudou de lugar ou de formaVisita precisa de linha
Integração com efeito externoNotificação do módulo 11 (teste que monta o JSON certo)
Correção de bugTodo bug corrigido ganha um teste que o reproduz

Parte 4: desempenho e diagnóstico

7. Os erros que mais pesam em produção

ErroSintomaComo evitar
Consulta dentro de laço (N+1)Processo lento que piora a cada mêsUma consulta com IN ou JOIN; ou carregue a lista uma vez antes do laço
Buscar o registro inteiro para ler um númeroLentidão em telas e calloutsDB.getSQLValue para um valor só; Query.count() ou .match() para existência
Consulta sem índiceTudo lento com poucos usuáriosÍndice pelo Dicionário (módulo 08) nas colunas dos filtros frequentes
Transação longaUsuários travados esperandoProcessos longos em lotes; nunca espere resposta de outro sistema dentro da transação (módulo 11)
Ignorar get_TrxName()Dados inconsistentes, travamentosToda consulta e todo new MAlgo(...) recebem a transação corrente
Ler configuração ou cadastro sem cacheMilhares de consultas iguaisMSysConfig e os get() estáticos do core já usam cache; para tabelas suas, CCache
System.out.printlnLog perdido, sem nívelCLogger, 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):

  1. Confira se já existe. Procure no JIRA do projeto e reproduza num dos servidores de teste, de preferência com a GardenWorld.
  2. Para melhoria, converse antes. Proponha no fórum ou no Mattermost. Ticket de melhoria sem conversa prévia costuma ficar parado.
  3. Abra o ticket no JIRA: título claro, passos para reproduzir, o esperado e o obtido. Você recebe um número, como IDEMPIERE-1234.
  4. 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: troque PlaceholderForTicket no nome do arquivo pelo número do ticket e gere a versão Oracle também.
  5. 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.
  6. 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

0 de 6 itens

Erros comuns

SintomaCausa provávelCorreção
mvnw verify não resolve org.adempiere.baseRepositório p2 do core não existe ou caminho erradoRode ./mvnw verify no core e confira idempiere.core.repository.url
Erro de versão entre pom e manifesto1.0.0-SNAPSHOT × 1.0.0.qualifier diferentesAlinhe os dois
Jar sem OSGI-INF ou sem os 2PacksPasta fora do bin.includesPasso 1
Bundle fica Installed no servidorDependê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 servidorPlugin não iniciou, ou falhou no meioLog do servidor e aba Package Installation
Teste passa sozinho e falha em conjuntoTeste dependendo de dado criado por outroCada teste cria o que precisa; o rollback limpa
Teste grava de verdade no bancoObjeto 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

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.