Padrões de Código Java
Padrões de codificação da Basis para qualquer código Java. Pareada com basis-spring-app (aquela cobre stack, configuração e arquitetura; esta cobre como o código é escrito dentro dela).
1. Escopo e enforcement
- Vale para todo código Java da Basis — apps Spring, libs, ferramentas.
- Regra que dá pra automatizar não vive só em documento: o que dá pra checar por ferramenta falha o
pipeline, não o revisor. A divisão na Basis é:
SonarQube é o lint. Roda no check-quality do Dagger e cobre as regras (chaves ausentes,
catch genérico, complexidade, code smell, vulnerabilidade). Não adicionar Checkstyle,
SpotBugs ou PMD — é a mesma análise duas vezes, com dois conjuntos de regras pra manter.
Spotless é o formatter, com palantir-java-format como engine. Sonar aponta
formatação, não reescreve arquivo; spotless:apply resolve indentação, largura de linha,
ordem de import e import não usado de uma vez. A configuração completa, os erros que o
rascunho óbvio comete e como implantar num projeto que ainda não tem estão em §2.1 e
§2.2 — não improvise um <plugin> novo.
Antes de empurrar, rode a análise no que está no índice: git add e depois
sonar analyze --staged (CLI sonar, autenticada com sonar auth login). São segundos, e
pega de graça o que seria uma MR reprovada.
Mas "No issues found" ali não é o gate. A análise local roda sem compilar e sem
classpath — é a diferença entre 200 ms e o check-quality inteiro —, e sem bytecode as
regras que dependem de fluxo de dados não têm como rodar. Medido neste projeto: um arquivo
que o CLI declarou limpo chegou à MR com dois java:S2259 ("A NullPointerException could be
thrown") e um java:S7467. Use-a como filtro, nunca como aval.
E ela não mede cobertura, que é a outra metade do gate e a que reprova em silêncio:
código novo bem escrito e sem teste passa no analyze e derruba a MR.
Para ter o que o check-quality teria, rode a análise de verdade na sua branch. É o
mesmo scanner da pipeline, com bytecode, classpath e o jacoco.xml que o verify acabou de
escrever — e por isso pega as regras de fluxo de dados que a análise local não alcança:
./mvnw clean verify
./mvnw org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \
-Dsonar.host.url=https://codequality.basis.com.br -Dsonar.token="$SONAR_TOKEN" \
-Dsonar.projectKey=<chave> -Dsonar.branch.name=$(git branch --show-current)
Duas coisas a não errar. O plugin não está declarado no pom (nem precisa: quem invoca na
pipeline é o orchestrator), então vai pelo nome completo — sonar:sonar por prefixo falha com
No plugin found for prefix 'sonar'. E -Dsonar.branch.name não é opcional: sem ele a
análise entra como a branch principal do projeto e passa a valer como o retrato oficial dele.
Isso escreve no servidor compartilhado — cria (ou atualiza) a branch no Sonar. Numa branch
de feature é barato e some com ela; combine antes de fazer em develop ou main.
Depois da análise, a conta sai por API:
curl -sS -u "$SONAR_TOKEN:" "https://codequality.basis.com.br/api/measures/component_tree\
?component=&pullRequest=&qualifiers=FIL&metricKeys=new_uncovered_lines,new_uncovered_conditions"
```
Ela aponta **arquivo e contagem**; `api/sources/lines?key=<chave>:<caminho>&pullRequest=<n>`
desce à linha, com `isNew`, `lineHits` e `coveredConditions` — que é o que diz qual `if`
ficou sem teste, em vez de mandar procurar.
-Xlint:all no maven-compiler-plugin, sem warning novo. Pega o que é do compilador
(deprecation, unchecked, this-escape) e que não é papel do Sonar.
- Nulidade é checada por JSpecify + IDE/Sonar (ver §6); NullAway/Error Prone é opcional e
depende do suporte da versão ao JDK em uso.
- Cobertura do Sonar vale para todos os módulos do reactor — módulo fora da lista de análise é
código sem lint nenhum, e é sempre o
-domain/-commons que fica de fora por esquecimento.
- Revisão humana/agente foca no que ferramenta não pega: design, nomes, tratamento de erro, teste.
2. Formatação
- Chaves sempre presentes em
if, if-else, if-else-if-else, for, while, do-while, switch, try-catch e try-catch-finally, mesmo com bloco vazio ou de uma linha.
- Posição das chaves no padrão One True Brace.
- Limite de 120 colunas por linha.
- Uma instrução por linha.
- Sem
import * (nem estático).
- Indentação e espaçamento são responsabilidade do formatter — não gastar review com isso.
2.1 Spotless: a configuração
O formatter é palantir-java-format, não google-java-format. O motivo está na
regra acima: o google-java-format fixa 100 colunas e não permite alterar, então
ele é incapaz de implementar o limite de 120 exigido aqui. O Palantir se descreve como
"a modern, lambda-friendly, 120 character Java formatter" — 120 fixo, que é
exatamente o número exigido aqui.
Surpresa do Palantir que ninguém antecipa: além das 120 colunas, ele aplica
80 colunas para cadeias de método — o último ponto da cadeia tem que vir antes
da coluna 80, senão a cadeia não é inlinada e quebra em várias linhas. É
deliberado (legibilidade em review) e não é configurável. Cadeia que hoje cabe em
120 vai quebrar.
<properties>
<spotless.version>3.10.1</spotless.version>
<palantir.version>2.97.0</palantir.version>
</properties>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>${spotless.version}</version>
<configuration>
<formats>
<!-- Só o que nenhum bloco de linguagem cobre. NÃO incluir .java aqui. -->
<format>
<includes>
<include>src/main/resources/templates/**/*.html</include>
<include>*.md</include>
</includes>
<trimTrailingWhitespace/>
<endWithNewline/>
</format>
</formats>
<java>
<!-- <includes> omitido de propósito: o default do bloco <java> já é
src/main/java/**/*.java + src/test/java/**/*.java -->
<importOrder/>
<removeUnusedImports/>
<expandWildcardImports/>
<!-- trocar por <forbidWildcardImports/> quando a base estiver estável:
aí o wildcard passa a reprovar o build em vez de ser expandido -->
<shortenFullyQualifiedTypes/>
<!-- <style> importa: GOOGLE ou AOSP aqui voltariam para 100 colunas -->
<palantirJavaFormat>
<version>${palantir.version}</version>
<style>PALANTIR</style>
</palantirJavaFormat>
<!-- DEPOIS do formatter, sempre: ele conserta quebras de linha que o
formatter introduziu em anotações. Antes dele não há o que consertar. -->
<formatAnnotations/>
</java>
</configuration>
<executions>
<execution>
<goals><goal>check</goal></goals>
<phase>verify</phase>
</execution>
</executions>
</plugin>
Três erros que o rascunho óbvio comete:
| Erro |
Por quê |
<include>.java</include> |
Casa um arquivo literalmente chamado .java. Precisa de glob — ou, melhor, omitir <includes> e usar o default do bloco <java> |
<format> genérico cobrindo .java |
O bloco <java> já trata esses arquivos; dois blocos sobre o mesmo alvo uma hora conflitam |
<formatAnnotations/> antes do formatter |
O README é explícito: "add the formatAnnotations rule after a Java formatter" |
2.2 Implantar Spotless num projeto que ainda não tem
Palantir e google-java-format são formatters totais: reescrevem o arquivo
inteiro, não apenas as linhas longas. Escolher 120 em vez de 100 não diminui o
primeiro spotless:apply — ele reformata tudo de qualquer jeito.
A decisão da equipe é commit isolado por projeto, com todos atualizando o
repositório local em seguida. Nessa ordem:
- Fechar ou mergear as MRs abertas antes. Reformatação global conflita com
toda MR viva. Rebasear depois é pior do que esperar.
- Aplicar e commitar só a formatação, sem nenhuma mudança de lógica junto —
é o que torna o commit revisável sem lê-lo linha a linha.
- Registrar o hash em
.git-blame-ignore-revs na raiz do repositório. Sem isso,
git blame de todo o projeto passa a apontar para esse commit.
- Cada pessoa habilita o arquivo localmente — não é automático:
git config blame.ignoreRevsFile .git-blame-ignore-revs
- Avisar a equipe para atualizar antes de começar qualquer trabalho novo.
O beco sem saída: o commit de reformatação move a linha de corte do Sonar.
O SCM Publisher usa git blame para decidir o que é código novo (ver
basis-ci-gitlab/references/sonar-analise-e-quality-gate.md). Depois da
reformatação em massa, toda linha reformatada passa a ter a data do commit de
formatação — e cai dentro do período de new code. O gate, que cobra cobertura
e zero issues em código novo, passa a julgar a base legada inteira. A primeira MR
depois disso reprova por código que ninguém tocou.
.git-blame-ignore-revs resolve o git blame humano; não há garantia de que o
SCM Publisher do Sonar o respeite — confirme na instância antes de contar com
isso. O caminho seguro é redefinir a baseline de new code no Sonar logo após
mergear a reformatação, para que o commit fique fora da janela.
A alternativa que foi considerada e não escolhida — registrada para não ser
re-litigada às cegas: <ratchetFrom>origin/develop</ratchetFrom> faz o Spotless
formatar e cobrar apenas os arquivos tocados desde a develop. A base
converge branch a branch, sem commit global, sem conflito em MR aberta e sem
mexer na baseline do Sonar — é o mesmo modelo Clean as You Code que o Sonar já
aplica. O custo é conviver com formatação heterogênea durante a transição. Se um
projeto novo entrar depois, ou se o big-bang doer num repo grande, é esta a saída.
3. Nomes
- Classes/records/enums em
PascalCase; métodos e variáveis em camelCase; static final em CONSTANT_CASE; pacotes em minúsculo sem underscore.
- Nome descreve intenção, não tipo nem implementação:
usuariosAtivos, não listaUsr; PagamentoService, não PagamentoManagerImpl.
- Sem prefixo
I em interfaces e sem sufixo Impl quando existe uma única implementação — nesse caso normalmente a interface é desnecessária.
- Abreviação só quando é o vocabulário do domínio (
cnpj, cpf, nfe).
- Boolean lê como afirmação:
ativo, temSaldo, podeAprovar.
@Override sempre que sobrescreve.
4. Java moderno
Usar os recursos da LTS em uso (ver basis-spring-app para a versão vigente) em vez de reproduzir padrões pré-Java 17:
record para DTOs, value objects e retornos multi-valor — em vez de classe com getters/equals/hashCode manuais.
sealed interface + switch com pattern matching para hierarquias fechadas (estados, resultados, comandos) — o compilador garante exaustividade.
switch como expressão (->) em vez de statement com break.
- Text block (
""") para SQL, JSON e mensagens multi-linha.
instanceof com pattern (if (o instanceof Pedido p)) em vez de instanceof + cast.
var só quando o tipo é óbvio no lado direito (var pedido = new Pedido()); nunca com retorno de método genérico ou literal ambíguo.
Stream.toList(), List.of(), Map.of() para coleções imutáveis.
5. Tratamento de exceções
- Toda exceção capturada é tratada — nenhuma ignorada ou escondida. Dois tratamentos válidos: logar ou relançar. Escolher um: logar e relançar duplica o erro no log.
- No caso raro em que uma exceção não deve ser tratada, o bloco
catch leva um comentário explicando o porquê.
- Exceção relançada usa classe de exceção do projeto (
<Application>Exception) e aninha a original (throw new PedidoException("...", e)) — nunca perder a causa nem só a mensagem (e.getMessage()).
- Não lançar exceções genéricas (
RuntimeException, IllegalArgumentException, IllegalStateException) para erro de negócio — usar exceções da aplicação, com nome que descreve a falha (SaldoInsuficienteException).
- Guardas internas de programação (
Objects.requireNonNull, pré-condições de método privado) podem manter NullPointerException/IllegalArgumentException — são bug, não fluxo de negócio.
- Capturar exceções específicas (
IOException, NoSuchAlgorithmException), nunca Exception, Throwable ou RuntimeException. Única exceção: handler de fronteira (@ControllerAdvice, loop de consumidor de mensageria), onde capturar Exception é intencional e existe para não derrubar o processo — ali documente.
- Nunca engolir
InterruptedException: restaurar o flag (Thread.currentThread().interrupt()) ou propagar.
- Exceção não é fluxo de controle — não usar para sinalizar caso esperado (ex: "não encontrado" em busca opcional).
- Mensagem de exceção diz o que falhou e com quais dados de contexto — sem dado sensível.
6. Nulidade e imutabilidade
JSpecify é o padrão
Spring Framework 7 / Spring Boot 4 migraram todo o codebase para JSpecify, e é o que a Basis usa.
As anotações são org.jspecify.annotations.* — não javax.annotation, não
org.springframework.lang.Nullable (removida no Spring 7), não jakarta.annotation.Nullable.
@NullMarked no package-info.java de cada pacote. Dentro de um pacote marcado, todo tipo
não anotado é não-nulo; só a exceção leva @Nullable. É o inverso do default do Java, e é o
que faz a anotação valer a pena: quem lê a assinatura sabe a resposta sem abrir a implementação.
- Declarar
org.jspecify:jspecify explicitamente no pom de cada módulo que usa as anotações.
Ela chega transitivamente pelo Spring, mas depender de transitiva pra algo que se import quebra
no dia em que a cadeia muda. A versão vem do BOM do Spring Boot.
@Nullable vai no tipo, não no membro (@Nullable String buscar(), List<@Nullable String>)
— JSpecify é anotação de tipo, e a posição importa em genérico e array.
- Módulo sem Spring (
-domain, libs) também é @NullMarked; ali a dependência é obrigatória.
- Enforcement: IntelliJ e SonarQube entendem JSpecify direto. NullAway (via Error Prone) leva a
checagem pro compilador quando a versão suporta o JDK em uso — desejável, não obrigatório.
Regras que continuam valendo
- Não retornar
null em métodos que retornam coleções — retornar coleção vazia. Isso vale mesmo com
@Nullable disponível: coleção vazia é o contrato, não uma ausência a ser tratada.
Optional só como tipo de retorno de método; nunca campo, parâmetro ou dentro de coleção. Com
JSpecify, campo/parâmetro opcional é @Nullable, que não aloca e não vaza Optional na API.
- Não usar
Optional.get() — usar orElseThrow(), orElse(), map().
Objects.requireNonNull fica para as fronteiras que o compilador não vê: entrada
desserializada (JSON, mensagem, banco), reflexão, e API pública consumida por código não anotado.
Dentro de código @NullMarked, repetir requireNonNull em todo parâmetro é ruído — a anotação já
é o contrato.
- Preferir campos
final e imutabilidade sempre que possível (records, builders, sem setter desnecessário).
- Coleção exposta por API é imutável (
List.copyOf, Collections.unmodifiableList) ou cópia defensiva — nunca a referência interna mutável.
- Sobrescrever
equals() e hashCode() sempre juntos; se a classe entra em coleção ordenada, compareTo consistente com equals.
toString() útil em entidades e value objects — e sem dado sensível.
7. Coleções e streams
- Escolher a estrutura pelo acesso:
List para ordem, Set para unicidade, Map para lookup — não List com contains em laço.
- Stream quando deixa mais legível; laço quando não. Stream de mais de 3-4 operações encadeadas normalmente pede extração de método.
- Sem efeito colateral dentro de lambda (
forEach que muta estado externo, peek para logar) — coletar e agir sobre o resultado.
- Nunca modificar coleção durante iteração — usar
removeIf ou Iterator.remove.
- Não usar stream paralelo sem medição; na prática quase nunca é a resposta.
8. Data/hora e valores monetários
java.time sempre. Proibido Date, Calendar, SimpleDateFormat.
- Timestamp de evento/auditoria:
Instant em UTC. Data de negócio sem hora: LocalDate. Data/hora com fuso relevante: ZonedDateTime com ZoneId explícito.
- Nunca depender do fuso default da JVM (
ZoneId.systemDefault() implícito) — o pod tem o fuso que o cluster deu.
- Dinheiro e quantidade fiscal em
BigDecimal com escala e RoundingMode explícitos — nunca double/float.
new BigDecimal("0.1") (string), nunca new BigDecimal(0.1); comparar com compareTo, não equals.
9. Logging
- SLF4J com logger
private static final por classe. Sem System.out/System.err e sem e.printStackTrace().
- Placeholder
{}, nunca concatenação: log.debug("Pedido {} processado", id).
- Exceção vai como último argumento (
log.error("Falha ao processar {}", id, e)) para preservar o stack trace.
- Níveis:
error = precisa de ação humana; warn = degradação recuperável; info = evento de negócio relevante; debug = diagnóstico.
- Sem dado sensível (senha, token, PII) em nenhum nível.
- Identificador de correlação via MDC quando há requisição/mensagem em contexto.
10. Concorrência
- Evitar estado mutável compartilhado sem sincronização explícita — o padrão é não compartilhar.
- Preferir
java.util.concurrent (ConcurrentHashMap, AtomicInteger, ExecutorService) a synchronized manual.
- Documentar explicitamente se uma classe é thread-safe ou não.
- Nunca criar
Thread na mão — usar executor (virtual threads para carga IO-bound na LTS atual).
ExecutorService com try-with-resources ou shutdown() garantido; sempre tratar Future/exceção da task, senão a falha some.
ThreadLocal sempre limpo em finally — pool reaproveita a thread.
11. Recursos e IO
- try-with-resources para todo
Closeable/AutoCloseable (conexão, stream, arquivo).
- Nunca depender de
finalize() (removido) nem de Cleaner como estratégia principal.
java.nio.file.Path/Files em vez de java.io.File.
- Charset sempre explícito (
StandardCharsets.UTF_8) em leitura, escrita e conversão String/byte[].
- Não carregar arquivo inteiro em memória quando dá para streamar.
12. Segurança
- Nunca concatenar string para montar query SQL — prepared statement / query parametrizada.
- Validar toda entrada externa (parâmetro de API, arquivo, dado de usuário) antes de processar; Bean Validation na fronteira quando disponível.
- Nunca logar dado sensível (senha, token, PII).
- Nunca colocar segredo (senha, chave de API) no código-fonte — ver
basis-k8s-deploy para origem de secrets.
- Não implementar algoritmo criptográfico próprio — bibliotecas consolidadas e atualizadas.
SecureRandom para qualquer valor com significado de segurança (token, salt, id de sessão); Random/Math.random() nunca.
- Senha só com hash lento e salgado (bcrypt/argon2/scrypt), nunca MD5/SHA-1/SHA-256 puro.
- Não desserializar dado não confiável com serialização nativa Java; parsers configurados sem resolução de entidade externa (XXE).
- Caminho de arquivo derivado de entrada do usuário: normalizar e validar contra o diretório base (path traversal).
13. API e design
- Javadoc em classes e métodos públicos de API — o que faz, o que assume, o que lança. Sem Javadoc que repete a assinatura.
- Menor visibilidade que funciona:
private > package-private > public. public é contrato.
- Composição em vez de herança quando não há "é um" claro; classe não projetada para extensão é
final.
- Método com responsabilidade única e curto o bastante para ser entendido de uma vez.
- Getter sem efeito colateral.
- Injeção de dependência por construtor (campos
final) — nunca new de dependência dentro de lógica de negócio nem injeção em campo.
- Interface criada quando há necessidade real (mais de uma implementação, fronteira de teste), não por padrão.
- Evitar parâmetro booleano em API pública — dois métodos nomeados são mais legíveis que
processar(pedido, true).
- Comentário explica por quê; o o quê é o código. Sem código comentado no repositório — o git guarda.
14. Testes
- Lógica de negócio tem cobertura de teste unitário. Cobertura é sinal, não meta.
- Stack padrão: JUnit 5 + AssertJ (
assertThat) + Mockito; Testcontainers quando o teste precisa de banco/broker real.
- Nome descreve comportamento esperado, não implementação:
deveLancarExcecaoQuandoXNulo, não testMetodo1.
- Estrutura arrange/act/assert visível; um comportamento por teste.
- Sem lógica (
if, laço) no teste — se o teste precisa de lógica, ele precisa de teste.
- Teste determinístico: sem
Thread.sleep, sem dependência de rede externa, sem ordem entre testes, relógio injetado (Clock) quando há tempo envolvido.
- Mockar o que é nosso e o que é lento/externo; não mockar tipo de terceiro que dá pra usar de verdade (
List, Optional, entidade).
- Teste de exceção verifica tipo e causa/mensagem relevante (
assertThatThrownBy(...).isInstanceOf(...).hasCauseInstanceOf(...)).
Asserções do mesmo actual vão numa cadeia só
Dois assertThat seguidos sobre o mesmo valor reprovam o quality gate. É a
java:S5853 — Consecutive AssertJ "assertThat" statements should be chained, code smell de
severidade MINOR —, e a mensagem que aparece na MR é "Join these multiple assertions subject to
one assertion chain." Já derrubou MR mais de uma vez pelo mesmo caminho: a segunda verificação
nasce depois da primeira, na revisão ou num ajuste, e entra como statement novo em vez de entrar
na cadeia que já estava lá.
A regra, no Sonar da Basis:
https://codequality.basis.com.br/coding_rules?open=java%3AS5853&rule_key=java%3AS5853
// Reprova
assertThat(pagina).contains("Versão " + versao);
assertThat(pagina).doesNotContain("href=\"/retomar\"");
// Passa
assertThat(pagina)
.contains("Versão " + versao)
.doesNotContain("href=\"/retomar\"");
O que dispara é adjacência com o mesmo valor testado — está no nome da regra:
Consecutive. Dois assertThat em sequência, sem statement no meio, sobre a mesma expressão.
actual diferente não dispara, e statement entre as duas quebra a sequência.
.as(...) descreve o que vem depois dele na cadeia, então dar nome a uma asserção
específica não obriga a quebrá-la em duas:
assertThat(pagina)
.contains("Versão " + versao)
.as("prova que a versão está fora do `th:if` do link de retomada")
.doesNotContain("href=\"/retomar\"");
Não saia varrendo a suíte inteira atrás disso. Numa base com suíte grande há dezenas de
ocorrências antigas que o Sonar nunca reportou, e o motivo é estrutural: o check-quality só
roda em MR, então o que existe é análise de pull request — que olha as linhas da MR — e não
análise de branch. Num projeto assim a branch principal pode nunca ter sido analisada, e aí
api/issues/search com branch= devolve zero enquanto a pullRequest=<n> devolve o achado
(ver basis-ci-gitlab §6).
Mas a linha que você tocar entra na análise da MR. Editar um teste antigo faz as asserções
vizinhas serem avaliadas, e o gate reprova por código que "já estava lá". Ao mexer num arquivo de
teste, encadeie as ocorrências daquele arquivo na mesma passada; é mais barato que descobrir
na pipeline.
Para conferir sem esperar a pipeline, com um token de API:
curl -sS -u "$SONAR_TOKEN:" \
"https://codequality.basis.com.br/api/issues/search?componentKeys=<chave>&rules=java:S5853&pullRequest=<n>"
15. Checklist de revisão
Ao revisar ou gerar código Java, verificar em ordem:
- Erro: exceção tratada uma vez, causa aninhada, tipo do projeto, nada engolido.
- Nulidade: pacote
@NullMarked, @Nullable só onde o nulo é real, sem null retornado em
coleção, Optional só em retorno, requireNonNull nas fronteiras não anotadas.
- Recurso: try-with-resources em tudo que fecha, charset explícito.
- Segurança: query parametrizada, entrada validada, nada sensível em log, sem segredo no código.
- Design: dependências por construtor, visibilidade mínima, método curto, sem efeito colateral escondido.
- Teste: comportamento coberto, nome descritivo, determinístico, asserções do mesmo
actual
numa cadeia só.
- Estilo: Spotless e SonarQube passaram — se não passaram, é build quebrado, não comentário de review.
1---2name: basis-java-code-standards3description: Use when writing, reviewing, or refactoring Java code in a Basis project — formatting and naming, modern Java (records, sealed types, pattern matching, text blocks), exception handling, nullability and immutability, collections and streams, date/time and money, logging, concurrency, resources/IO, security, API design, and tests (JUnit 5 + AssertJ + Mockito). Activate on any change to `.java` files, on code review, or when the user asks to check conformance with the code standards.4---56# Padrões de Código Java78Padrões de codificação da Basis para qualquer código Java. Pareada com `basis-spring-app` (aquela cobre stack, configuração e arquitetura; esta cobre como o código é escrito dentro dela).910## 1. Escopo e enforcement1112- Vale para todo código Java da Basis — apps Spring, libs, ferramentas.13- Regra que dá pra automatizar não vive só em documento: o que dá pra checar por ferramenta falha o14 pipeline, não o revisor. A divisão na Basis é:15 - **SonarQube** é o lint. Roda no `check-quality` do Dagger e cobre as regras (chaves ausentes,16 `catch` genérico, complexidade, code smell, vulnerabilidade). **Não** adicionar Checkstyle,17 SpotBugs ou PMD — é a mesma análise duas vezes, com dois conjuntos de regras pra manter.18 - **Spotless** é o formatter, com **palantir-java-format** como engine. Sonar aponta19 formatação, não reescreve arquivo; `spotless:apply` resolve indentação, largura de linha,20 ordem de import e import não usado de uma vez. A configuração completa, os erros que o21 rascunho óbvio comete e como implantar num projeto que ainda não tem estão em **§2.1 e22 §2.2** — não improvise um `<plugin>` novo.23 - **Antes de empurrar, rode a análise no que está no índice:** `git add` e depois24 `sonar analyze --staged` (CLI `sonar`, autenticada com `sonar auth login`). São segundos, e25 pega de graça o que seria uma MR reprovada.2627 **Mas "No issues found" ali não é o gate.** A análise local roda sem compilar e sem28 classpath — é a diferença entre 200 ms e o `check-quality` inteiro —, e sem bytecode as29 regras que dependem de fluxo de dados não têm como rodar. Medido neste projeto: um arquivo30 que o CLI declarou limpo chegou à MR com dois `java:S2259` ("A NullPointerException could be31 thrown") e um `java:S7467`. Use-a como filtro, nunca como aval.3233 **E ela não mede cobertura**, que é a outra metade do gate e a que reprova em silêncio:34 código novo bem escrito e sem teste passa no `analyze` e derruba a MR.3536 - **Para ter o que o `check-quality` teria, rode a análise de verdade na sua branch.** É o37 mesmo scanner da pipeline, com bytecode, classpath e o `jacoco.xml` que o `verify` acabou de38 escrever — e por isso pega as regras de fluxo de dados que a análise local não alcança:3940 ```bash41 ./mvnw clean verify42 ./mvnw org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \43 -Dsonar.host.url=https://codequality.basis.com.br -Dsonar.token="$SONAR_TOKEN" \44 -Dsonar.projectKey=<chave> -Dsonar.branch.name=$(git branch --show-current)45 ```4647 Duas coisas a não errar. O plugin **não** está declarado no pom (nem precisa: quem invoca na48 pipeline é o orchestrator), então vai pelo nome completo — `sonar:sonar` por prefixo falha com49 `No plugin found for prefix 'sonar'`. E **`-Dsonar.branch.name` não é opcional**: sem ele a50 análise entra como a branch principal do projeto e passa a valer como o retrato oficial dele.5152 Isso **escreve no servidor compartilhado** — cria (ou atualiza) a branch no Sonar. Numa branch53 de feature é barato e some com ela; combine antes de fazer em `develop` ou `main`.5455 Depois da análise, a conta sai por API:5657 ```bash58 curl -sS -u "$SONAR_TOKEN:" "https://codequality.basis.com.br/api/measures/component_tree\59?component=<chave>&pullRequest=<n>&qualifiers=FIL&metricKeys=new_uncovered_lines,new_uncovered_conditions"60 ```6162 Ela aponta **arquivo e contagem**; `api/sources/lines?key=<chave>:<caminho>&pullRequest=<n>`63 desce à linha, com `isNew`, `lineHits` e `coveredConditions` — que é o que diz qual `if`64 ficou sem teste, em vez de mandar procurar.65 - **`-Xlint:all`** no `maven-compiler-plugin`, sem warning novo. Pega o que é do compilador66 (deprecation, unchecked, this-escape) e que não é papel do Sonar.67 - **Nulidade** é checada por JSpecify + IDE/Sonar (ver §6); NullAway/Error Prone é opcional e68 depende do suporte da versão ao JDK em uso.69- Cobertura do Sonar vale para **todos** os módulos do reactor — módulo fora da lista de análise é70 código sem lint nenhum, e é sempre o `-domain`/`-commons` que fica de fora por esquecimento.71- Revisão humana/agente foca no que ferramenta não pega: design, nomes, tratamento de erro, teste.7273## 2. Formatação7475- Chaves sempre presentes em `if`, `if-else`, `if-else-if-else`, `for`, `while`, `do-while`, `switch`, `try-catch` e `try-catch-finally`, mesmo com bloco vazio ou de uma linha.76- Posição das chaves no padrão One True Brace.77- Limite de 120 colunas por linha.78- Uma instrução por linha.79- Sem `import *` (nem estático).80- Indentação e espaçamento são responsabilidade do formatter — não gastar review com isso.8182### 2.1 Spotless: a configuração8384O formatter é **palantir-java-format**, não google-java-format. O motivo está na85regra acima: o google-java-format fixa **100 colunas e não permite alterar**, então86ele é incapaz de implementar o limite de 120 exigido aqui. O Palantir se descreve como87*"a modern, lambda-friendly, 120 character Java formatter"* — 120 fixo, que é88exatamente o número exigido aqui.8990**Surpresa do Palantir que ninguém antecipa:** além das 120 colunas, ele aplica91**80 colunas para cadeias de método** — o último ponto da cadeia tem que vir antes92da coluna 80, senão a cadeia não é inlinada e quebra em várias linhas. É93deliberado (legibilidade em review) e não é configurável. Cadeia que hoje cabe em94120 vai quebrar.9596```xml97<properties>98 <spotless.version>3.10.1</spotless.version>99 <palantir.version>2.97.0</palantir.version>100</properties>101102<plugin>103 <groupId>com.diffplug.spotless</groupId>104 <artifactId>spotless-maven-plugin</artifactId>105 <version>${spotless.version}</version>106 <configuration>107 <formats>108 <!-- Só o que nenhum bloco de linguagem cobre. NÃO incluir .java aqui. -->109 <format>110 <includes>111 <include>src/main/resources/templates/**/*.html</include>112 <include>*.md</include>113 </includes>114 <trimTrailingWhitespace/>115 <endWithNewline/>116 </format>117 </formats>118119 <java>120 <!-- <includes> omitido de propósito: o default do bloco <java> já é121 src/main/java/**/*.java + src/test/java/**/*.java -->122 <importOrder/>123 <removeUnusedImports/>124 <expandWildcardImports/>125 <!-- trocar por <forbidWildcardImports/> quando a base estiver estável:126 aí o wildcard passa a reprovar o build em vez de ser expandido -->127 <shortenFullyQualifiedTypes/>128129 <!-- <style> importa: GOOGLE ou AOSP aqui voltariam para 100 colunas -->130 <palantirJavaFormat>131 <version>${palantir.version}</version>132 <style>PALANTIR</style>133 </palantirJavaFormat>134135 <!-- DEPOIS do formatter, sempre: ele conserta quebras de linha que o136 formatter introduziu em anotações. Antes dele não há o que consertar. -->137 <formatAnnotations/>138 </java>139 </configuration>140141 <executions>142 <execution>143 <goals><goal>check</goal></goals>144 <phase>verify</phase>145 </execution>146 </executions>147</plugin>148```149150**Três erros que o rascunho óbvio comete:**151152| Erro | Por quê |153|---|---|154| `<include>.java</include>` | Casa um arquivo *literalmente chamado* `.java`. Precisa de glob — ou, melhor, omitir `<includes>` e usar o default do bloco `<java>` |155| `<format>` genérico cobrindo `.java` | O bloco `<java>` já trata esses arquivos; dois blocos sobre o mesmo alvo uma hora conflitam |156| `<formatAnnotations/>` antes do formatter | O README é explícito: *"add the `formatAnnotations` rule **after** a Java formatter"* |157158### 2.2 Implantar Spotless num projeto que ainda não tem159160**Palantir e google-java-format são formatters totais**: reescrevem o arquivo161inteiro, não apenas as linhas longas. Escolher 120 em vez de 100 **não diminui o162primeiro `spotless:apply`** — ele reformata tudo de qualquer jeito.163164A decisão da equipe é **commit isolado por projeto**, com todos atualizando o165repositório local em seguida. Nessa ordem:1661671. **Fechar ou mergear as MRs abertas antes.** Reformatação global conflita com168 toda MR viva. Rebasear depois é pior do que esperar.1692. Aplicar e commitar **só a formatação**, sem nenhuma mudança de lógica junto —170 é o que torna o commit revisável sem lê-lo linha a linha.1713. Registrar o hash em `.git-blame-ignore-revs` na raiz do repositório. Sem isso,172 `git blame` de todo o projeto passa a apontar para esse commit.1734. Cada pessoa habilita o arquivo localmente — **não é automático**:174 `git config blame.ignoreRevsFile .git-blame-ignore-revs`1755. Avisar a equipe para atualizar antes de começar qualquer trabalho novo.176177**O beco sem saída: o commit de reformatação move a linha de corte do Sonar.**178O SCM Publisher usa `git blame` para decidir o que é código novo (ver179`basis-ci-gitlab/references/sonar-analise-e-quality-gate.md`). Depois da180reformatação em massa, **toda linha reformatada passa a ter a data do commit de181formatação** — e cai dentro do período de *new code*. O gate, que cobra cobertura182e zero issues em código novo, passa a julgar a base legada inteira. A primeira MR183depois disso reprova por código que ninguém tocou.184185`.git-blame-ignore-revs` resolve o `git blame` humano; **não há garantia de que o186SCM Publisher do Sonar o respeite** — confirme na instância antes de contar com187isso. O caminho seguro é **redefinir a baseline de new code no Sonar logo após188mergear a reformatação**, para que o commit fique fora da janela.189190**A alternativa que foi considerada e não escolhida** — registrada para não ser191re-litigada às cegas: `<ratchetFrom>origin/develop</ratchetFrom>` faz o Spotless192formatar e cobrar **apenas os arquivos tocados desde a `develop`**. A base193converge branch a branch, sem commit global, sem conflito em MR aberta e sem194mexer na baseline do Sonar — é o mesmo modelo *Clean as You Code* que o Sonar já195aplica. O custo é conviver com formatação heterogênea durante a transição. Se um196projeto novo entrar depois, ou se o big-bang doer num repo grande, é esta a saída.197198## 3. Nomes199200- Classes/records/enums em `PascalCase`; métodos e variáveis em `camelCase`; `static final` em `CONSTANT_CASE`; pacotes em minúsculo sem underscore.201- Nome descreve intenção, não tipo nem implementação: `usuariosAtivos`, não `listaUsr`; `PagamentoService`, não `PagamentoManagerImpl`.202- Sem prefixo `I` em interfaces e sem sufixo `Impl` quando existe uma única implementação — nesse caso normalmente a interface é desnecessária.203- Abreviação só quando é o vocabulário do domínio (`cnpj`, `cpf`, `nfe`).204- Boolean lê como afirmação: `ativo`, `temSaldo`, `podeAprovar`.205- `@Override` sempre que sobrescreve.206207## 4. Java moderno208209Usar os recursos da LTS em uso (ver `basis-spring-app` para a versão vigente) em vez de reproduzir padrões pré-Java 17:210211- `record` para DTOs, value objects e retornos multi-valor — em vez de classe com getters/`equals`/`hashCode` manuais.212- `sealed` interface + `switch` com pattern matching para hierarquias fechadas (estados, resultados, comandos) — o compilador garante exaustividade.213- `switch` como expressão (`->`) em vez de statement com `break`.214- Text block (`"""`) para SQL, JSON e mensagens multi-linha.215- `instanceof` com pattern (`if (o instanceof Pedido p)`) em vez de `instanceof` + cast.216- `var` só quando o tipo é óbvio no lado direito (`var pedido = new Pedido()`); nunca com retorno de método genérico ou literal ambíguo.217- `Stream.toList()`, `List.of()`, `Map.of()` para coleções imutáveis.218219## 5. Tratamento de exceções220221- Toda exceção capturada é tratada — nenhuma ignorada ou escondida. Dois tratamentos válidos: logar ou relançar. Escolher **um**: logar e relançar duplica o erro no log.222- No caso raro em que uma exceção não deve ser tratada, o bloco `catch` leva um comentário explicando o porquê.223- Exceção relançada usa classe de exceção do projeto (`<Application>Exception`) e **aninha a original** (`throw new PedidoException("...", e)`) — nunca perder a causa nem só a mensagem (`e.getMessage()`).224- Não lançar exceções genéricas (`RuntimeException`, `IllegalArgumentException`, `IllegalStateException`) para erro de negócio — usar exceções da aplicação, com nome que descreve a falha (`SaldoInsuficienteException`).225- Guardas internas de programação (`Objects.requireNonNull`, pré-condições de método privado) podem manter `NullPointerException`/`IllegalArgumentException` — são bug, não fluxo de negócio.226- Capturar exceções específicas (`IOException`, `NoSuchAlgorithmException`), nunca `Exception`, `Throwable` ou `RuntimeException`. Única exceção: handler de fronteira (`@ControllerAdvice`, loop de consumidor de mensageria), onde capturar `Exception` é intencional e existe para não derrubar o processo — ali documente.227- Nunca engolir `InterruptedException`: restaurar o flag (`Thread.currentThread().interrupt()`) ou propagar.228- Exceção não é fluxo de controle — não usar para sinalizar caso esperado (ex: "não encontrado" em busca opcional).229- Mensagem de exceção diz o que falhou e com quais dados de contexto — sem dado sensível.230231## 6. Nulidade e imutabilidade232233### JSpecify é o padrão234235Spring Framework 7 / Spring Boot 4 migraram todo o codebase para **JSpecify**, e é o que a Basis usa.236As anotações são `org.jspecify.annotations.*` — não `javax.annotation`, não237`org.springframework.lang.Nullable` (removida no Spring 7), não `jakarta.annotation.Nullable`.238239- **`@NullMarked` no `package-info.java` de cada pacote.** Dentro de um pacote marcado, todo tipo240 não anotado é **não-nulo**; só a exceção leva `@Nullable`. É o inverso do default do Java, e é o241 que faz a anotação valer a pena: quem lê a assinatura sabe a resposta sem abrir a implementação.242- Declarar `org.jspecify:jspecify` **explicitamente** no pom de cada módulo que usa as anotações.243 Ela chega transitivamente pelo Spring, mas depender de transitiva pra algo que se `import` quebra244 no dia em que a cadeia muda. A versão vem do BOM do Spring Boot.245- `@Nullable` vai **no tipo**, não no membro (`@Nullable String buscar()`, `List<@Nullable String>`)246 — JSpecify é anotação de tipo, e a posição importa em genérico e array.247- Módulo sem Spring (`-domain`, libs) também é `@NullMarked`; ali a dependência é obrigatória.248- Enforcement: IntelliJ e SonarQube entendem JSpecify direto. NullAway (via Error Prone) leva a249 checagem pro compilador quando a versão suporta o JDK em uso — desejável, não obrigatório.250251### Regras que continuam valendo252253- Não retornar `null` em métodos que retornam coleções — retornar coleção vazia. Isso vale mesmo com254 `@Nullable` disponível: coleção vazia é o contrato, não uma ausência a ser tratada.255- `Optional` só como tipo de retorno de método; nunca campo, parâmetro ou dentro de coleção. Com256 JSpecify, campo/parâmetro opcional é `@Nullable`, que não aloca e não vaza `Optional` na API.257- Não usar `Optional.get()` — usar `orElseThrow()`, `orElse()`, `map()`.258- `Objects.requireNonNull` fica para as fronteiras que o compilador **não** vê: entrada259 desserializada (JSON, mensagem, banco), reflexão, e API pública consumida por código não anotado.260 Dentro de código `@NullMarked`, repetir `requireNonNull` em todo parâmetro é ruído — a anotação já261 é o contrato.262- Preferir campos `final` e imutabilidade sempre que possível (records, builders, sem setter desnecessário).263- Coleção exposta por API é imutável (`List.copyOf`, `Collections.unmodifiableList`) ou cópia defensiva — nunca a referência interna mutável.264- Sobrescrever `equals()` e `hashCode()` sempre juntos; se a classe entra em coleção ordenada, `compareTo` consistente com `equals`.265- `toString()` útil em entidades e value objects — e sem dado sensível.266267## 7. Coleções e streams268269- Escolher a estrutura pelo acesso: `List` para ordem, `Set` para unicidade, `Map` para lookup — não `List` com `contains` em laço.270- Stream quando deixa mais legível; laço quando não. Stream de mais de 3-4 operações encadeadas normalmente pede extração de método.271- Sem efeito colateral dentro de lambda (`forEach` que muta estado externo, `peek` para logar) — coletar e agir sobre o resultado.272- Nunca modificar coleção durante iteração — usar `removeIf` ou `Iterator.remove`.273- Não usar stream paralelo sem medição; na prática quase nunca é a resposta.274275## 8. Data/hora e valores monetários276277- `java.time` sempre. Proibido `Date`, `Calendar`, `SimpleDateFormat`.278- Timestamp de evento/auditoria: `Instant` em UTC. Data de negócio sem hora: `LocalDate`. Data/hora com fuso relevante: `ZonedDateTime` com `ZoneId` explícito.279- Nunca depender do fuso default da JVM (`ZoneId.systemDefault()` implícito) — o pod tem o fuso que o cluster deu.280- Dinheiro e quantidade fiscal em `BigDecimal` com escala e `RoundingMode` explícitos — nunca `double`/`float`.281- `new BigDecimal("0.1")` (string), nunca `new BigDecimal(0.1)`; comparar com `compareTo`, não `equals`.282283## 9. Logging284285- SLF4J com logger `private static final` por classe. Sem `System.out`/`System.err` e sem `e.printStackTrace()`.286- Placeholder `{}`, nunca concatenação: `log.debug("Pedido {} processado", id)`.287- Exceção vai como último argumento (`log.error("Falha ao processar {}", id, e)`) para preservar o stack trace.288- Níveis: `error` = precisa de ação humana; `warn` = degradação recuperável; `info` = evento de negócio relevante; `debug` = diagnóstico.289- Sem dado sensível (senha, token, PII) em nenhum nível.290- Identificador de correlação via MDC quando há requisição/mensagem em contexto.291292## 10. Concorrência293294- Evitar estado mutável compartilhado sem sincronização explícita — o padrão é não compartilhar.295- Preferir `java.util.concurrent` (`ConcurrentHashMap`, `AtomicInteger`, `ExecutorService`) a `synchronized` manual.296- Documentar explicitamente se uma classe é thread-safe ou não.297- Nunca criar `Thread` na mão — usar executor (virtual threads para carga IO-bound na LTS atual).298- `ExecutorService` com try-with-resources ou `shutdown()` garantido; sempre tratar `Future`/exceção da task, senão a falha some.299- `ThreadLocal` sempre limpo em `finally` — pool reaproveita a thread.300301## 11. Recursos e IO302303- try-with-resources para todo `Closeable`/`AutoCloseable` (conexão, stream, arquivo).304- Nunca depender de `finalize()` (removido) nem de `Cleaner` como estratégia principal.305- `java.nio.file.Path`/`Files` em vez de `java.io.File`.306- Charset **sempre** explícito (`StandardCharsets.UTF_8`) em leitura, escrita e conversão `String`/`byte[]`.307- Não carregar arquivo inteiro em memória quando dá para streamar.308309## 12. Segurança310311- Nunca concatenar string para montar query SQL — prepared statement / query parametrizada.312- Validar toda entrada externa (parâmetro de API, arquivo, dado de usuário) antes de processar; Bean Validation na fronteira quando disponível.313- Nunca logar dado sensível (senha, token, PII).314- Nunca colocar segredo (senha, chave de API) no código-fonte — ver `basis-k8s-deploy` para origem de secrets.315- Não implementar algoritmo criptográfico próprio — bibliotecas consolidadas e atualizadas.316- `SecureRandom` para qualquer valor com significado de segurança (token, salt, id de sessão); `Random`/`Math.random()` nunca.317- Senha só com hash lento e salgado (bcrypt/argon2/scrypt), nunca MD5/SHA-1/SHA-256 puro.318- Não desserializar dado não confiável com serialização nativa Java; parsers configurados sem resolução de entidade externa (XXE).319- Caminho de arquivo derivado de entrada do usuário: normalizar e validar contra o diretório base (path traversal).320321## 13. API e design322323- Javadoc em classes e métodos públicos de API — o que faz, o que assume, o que lança. Sem Javadoc que repete a assinatura.324- Menor visibilidade que funciona: `private` > package-private > `public`. `public` é contrato.325- Composição em vez de herança quando não há "é um" claro; classe não projetada para extensão é `final`.326- Método com responsabilidade única e curto o bastante para ser entendido de uma vez.327- Getter sem efeito colateral.328- Injeção de dependência por **construtor** (campos `final`) — nunca `new` de dependência dentro de lógica de negócio nem injeção em campo.329- Interface criada quando há necessidade real (mais de uma implementação, fronteira de teste), não por padrão.330- Evitar parâmetro booleano em API pública — dois métodos nomeados são mais legíveis que `processar(pedido, true)`.331- Comentário explica **por quê**; o **o quê** é o código. Sem código comentado no repositório — o git guarda.332333## 14. Testes334335- Lógica de negócio tem cobertura de teste unitário. Cobertura é sinal, não meta.336- Stack padrão: JUnit 5 + AssertJ (`assertThat`) + Mockito; Testcontainers quando o teste precisa de banco/broker real.337- Nome descreve comportamento esperado, não implementação: `deveLancarExcecaoQuandoXNulo`, não `testMetodo1`.338- Estrutura arrange/act/assert visível; um comportamento por teste.339- Sem lógica (`if`, laço) no teste — se o teste precisa de lógica, ele precisa de teste.340- Teste determinístico: sem `Thread.sleep`, sem dependência de rede externa, sem ordem entre testes, relógio injetado (`Clock`) quando há tempo envolvido.341- Mockar o que é nosso e o que é lento/externo; não mockar tipo de terceiro que dá pra usar de verdade (`List`, `Optional`, entidade).342- Teste de exceção verifica tipo **e** causa/mensagem relevante (`assertThatThrownBy(...).isInstanceOf(...).hasCauseInstanceOf(...)`).343344### Asserções do mesmo `actual` vão numa cadeia só345346**Dois `assertThat` seguidos sobre o mesmo valor reprovam o quality gate.** É a347`java:S5853` — *Consecutive AssertJ "assertThat" statements should be chained*, code smell de348severidade MINOR —, e a mensagem que aparece na MR é *"Join these multiple assertions subject to349one assertion chain."* Já derrubou MR mais de uma vez pelo mesmo caminho: a segunda verificação350nasce depois da primeira, na revisão ou num ajuste, e entra como statement novo em vez de entrar351na cadeia que já estava lá.352353A regra, no Sonar da Basis:354<https://codequality.basis.com.br/coding_rules?open=java%3AS5853&rule_key=java%3AS5853>355356```java357// Reprova358assertThat(pagina).contains("Versão " + versao);359assertThat(pagina).doesNotContain("href=\"/retomar\"");360361// Passa362assertThat(pagina)363 .contains("Versão " + versao)364 .doesNotContain("href=\"/retomar\"");365```366367O que dispara é **adjacência com o mesmo valor testado** — está no nome da regra:368*Consecutive*. Dois `assertThat` em sequência, sem statement no meio, sobre a mesma expressão.369`actual` diferente não dispara, e statement entre as duas quebra a sequência.370371**`.as(...)` descreve o que vem depois dele na cadeia**, então dar nome a uma asserção372específica não obriga a quebrá-la em duas:373374```java375assertThat(pagina)376 .contains("Versão " + versao)377 .as("prova que a versão está fora do `th:if` do link de retomada")378 .doesNotContain("href=\"/retomar\"");379```380381**Não saia varrendo a suíte inteira atrás disso.** Numa base com suíte grande há dezenas de382ocorrências antigas que o Sonar nunca reportou, e o motivo é estrutural: o `check-quality` só383roda em MR, então o que existe é análise de *pull request* — que olha as linhas da MR — e não384análise de branch. Num projeto assim a branch principal pode nunca ter sido analisada, e aí385`api/issues/search` com `branch=` devolve zero enquanto a `pullRequest=<n>` devolve o achado386(ver `basis-ci-gitlab` §6).387388**Mas a linha que você tocar entra na análise da MR.** Editar um teste antigo faz as asserções389vizinhas serem avaliadas, e o gate reprova por código que "já estava lá". Ao mexer num arquivo de390teste, encadeie as ocorrências **daquele arquivo** na mesma passada; é mais barato que descobrir391na pipeline.392393Para conferir sem esperar a pipeline, com um token de API:394395```bash396curl -sS -u "$SONAR_TOKEN:" \397 "https://codequality.basis.com.br/api/issues/search?componentKeys=<chave>&rules=java:S5853&pullRequest=<n>"398```399400## 15. Checklist de revisão401402Ao revisar ou gerar código Java, verificar em ordem:4034041. Erro: exceção tratada uma vez, causa aninhada, tipo do projeto, nada engolido.4052. Nulidade: pacote `@NullMarked`, `@Nullable` só onde o nulo é real, sem `null` retornado em406 coleção, `Optional` só em retorno, `requireNonNull` nas fronteiras não anotadas.4073. Recurso: try-with-resources em tudo que fecha, charset explícito.4084. Segurança: query parametrizada, entrada validada, nada sensível em log, sem segredo no código.4095. Design: dependências por construtor, visibilidade mínima, método curto, sem efeito colateral escondido.4106. Teste: comportamento coberto, nome descritivo, determinístico, asserções do mesmo `actual`411 numa cadeia só.4127. Estilo: Spotless e SonarQube passaram — se não passaram, é build quebrado, não comentário de review.