Fundamentos Arquitectura JVM
Fundamentos

O Conceito de Abstracção

Abstracção é o mecanismo pelo qual a engenharia de software torna o complexo manejável — escondendo detalhes de implementação atrás de interfaces simples. Mas abstracção nunca elimina a complexidade subjacente: apenas a desloca. Quando uma abstracção falha — quando os detalhes que deveria esconder "vazam" para a camada acima — o programador que não percebe o que está por baixo fica completamente desorientado. Perceber as camadas é o que separa quem programa de quem engenheira.

A Pilha Completa

Quando escreves accountRepository.findByIdAndOwnerId(42L, 99L) em Spring Boot, estás no topo de uma pilha com mais de dez camadas de abstracção. Cada camada esconde a anterior e expõe uma interface mais simples.

// A pilha de abstracção — de baixo para cima:

// ── Nível 0: Física ──────────────────────────────────────────────────────────
// Transístores — interruptores electrónicos (aberto/fechado = 0/1)
// Gravados em silício a 3-5 nanómetros (Intel/TSMC, 2026)
// Um chip moderno: ~50 mil milhões de transístores

// ── Nível 1: Lógica Digital ──────────────────────────────────────────────────
// Portas lógicas — AND, OR, NOT, XOR — construídas com transístores
// Um flip-flop (1 bit de memória) = ~6 transístores
// Um somador de 64 bits = ~1000 portas lógicas

// ── Nível 2: Arquitectura do Processador ─────────────────────────────────────
// ALU (Arithmetic Logic Unit), registos, unidade de controlo
// O conjunto de instruções: x86-64, ARM, RISC-V
// MOV, ADD, CMP, JMP, CALL, RET — cada instrução = 1-15 bytes de código máquina

// ── Nível 3: Assembly ────────────────────────────────────────────────────────
// Representação textual das instruções máquina
// Uma correspondência (quase) directa com o código máquina
// mov rax, [rbp-8]   →   48 8B 45 F8 (bytes no executável)

// ── Nível 4: C / Linguagem de Sistema ────────────────────────────────────────
// Abstracção sobre Assembly — o compilador faz a tradução
// Tipos, funções, estruturas — mas ainda acesso directo a memória (ponteiros)
// int x = a + b;   → o compilador decide quais registos usar

// ── Nível 5: Bytecode JVM ────────────────────────────────────────────────────
// Java compila para bytecode (.class) — instruções para uma máquina virtual
// Portável: o mesmo .class executa em qualquer plataforma com JVM
// iload_1, iload_2, iadd, ireturn — instruções da JVM

// ── Nível 6: Java / Linguagem de Alto Nível ──────────────────────────────────
// O JIT compiler traduz bytecode para código máquina nativo em runtime
// Gestão automática de memória (GC), verificação de limites de arrays
// int soma = a + b;   → bytecode → JIT → código máquina

// ── Nível 7: JDBC / Driver de Base de Dados ──────────────────────────────────
// Abstracção sobre a comunicação de rede com a base de dados
// Transforma chamadas Java em protocolo de rede proprietário (PostgreSQL wire protocol)
// connection.prepareStatement("SELECT * FROM accounts WHERE id = ?")

// ── Nível 8: JPA / Hibernate (ORM) ───────────────────────────────────────────
// Abstracção sobre SQL — mapeia objectos Java para tabelas relacionais
// @Entity, @Column, relações — o Hibernate gera o SQL automaticamente
// entityManager.find(Account.class, 42L)  →  SELECT * FROM accounts WHERE id = 42

// ── Nível 9: Spring Data JPA ─────────────────────────────────────────────────
// Abstracção sobre JPA — repositórios gerados automaticamente por interfaces
// interface AccountRepository extends JpaRepository<Account, Long> {
//     Optional<Account> findByIdAndOwnerId(Long id, Long ownerId);
// }
// Spring gera a implementação — nenhum SQL ou JPQL escrito manualmente

// ── Nível 10: Spring Boot ────────────────────────────────────────────────────
// Auto-configuração, injecção de dependências, servidor embebido
// @SpringBootApplication — configura ~300 beans automaticamente
// @RestController, @Service, @Repository — papéis declarativos

// ── O teu código ─────────────────────────────────────────────────────────────
accountRepository.findByIdAndOwnerId(42L, 99L);
// Esta linha simples atravessa, em milissegundos:
// Java → JIT → bytecode → JVM → Spring Data → Hibernate → JDBC
// → rede TCP/IP → PostgreSQL → B-tree index → disco (ou cache)
// → resposta → rede → JDBC → Hibernate → JPA → Spring Data → Java

O que a Abstracção Ganha e o que Custa

Cada camada de abstracção tem um custo e um benefício. O erro mais comum é assumir que uma abstracção é gratuita — que esconder complexidade não tem custo. Sempre tem.

CamadaO que escondeBenefícioCusto
JVM sobre C Gestão de memória, aritmética de ponteiros, diferenças de plataforma Elimina buffer overflows, use-after-free, portabilidade imediata Overhead do GC, pausas de garbage collection, maior uso de memória
Hibernate sobre JDBC SQL gerado automaticamente, mapeamento objeto-relacional, cache de primeiro nível Produtividade — sem SQL repetitivo, mudança de base de dados mais simples N+1 queries, SQL ineficiente gerado, comportamento lazy inesperado, curva de aprendizagem
Spring Data sobre JPA Implementações de repositório, queries derivadas de nomes de métodos Zero código boilerplate para CRUD standard Magic implícita difícil de debugar, queries geradas opacas
Spring Boot sobre Spring Configuração XML/Java, wiring manual de beans, setup do servidor Produtividade extrema — aplicação funcional em minutos Startup lento, consumo de memória elevado, comportamento auto-configurado difícil de sobrepor
HTTPS sobre TCP Encriptação, autenticação do servidor, integridade dos dados Segurança transparente — o código da aplicação não precisa de saber Latência do TLS handshake, overhead de encriptação/desencriptação por pacote

Leaky Abstractions — Quando a Camada de Baixo Vaza

Joel Spolsky formulou a Lei das Abstrações com Fugas em 2002: "All non-trivial abstractions, to some degree, are leaky." Uma abstracção com fuga é aquela onde os detalhes da camada inferior "vazam" e afectam o comportamento visível na camada superior — obrigando o programador a perceber o que a abstracção deveria esconder.

// ── Exemplo 1: Hibernate N+1 Query Problem ───────────────────────────────────
// A abstracção: "trata Account e as suas Transactions como objectos Java normais"
// A fuga: cada acesso a uma relação lazy dispara uma query SQL separada

@Entity
public class Account {
    @OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
    private List<Transaction> transactions;  // carregadas "automaticamente"
}

// Código aparentemente inocente:
List<Account> accounts = accountRepository.findAll();  // 1 query: SELECT * FROM accounts
for (Account acc : accounts) {
    System.out.println(acc.getTransactions().size());   // 1 query POR conta!
}
// 100 contas → 101 queries ao invés de 1
// A abstracção "objectos relacionados carregam automaticamente" vazou o custo SQL

// Solução: perceber o SQL que está por baixo e usar JOIN FETCH
@Query("SELECT a FROM Account a JOIN FETCH a.transactions WHERE a.ownerId = :ownerId")
List<Account> findWithTransactions(@Param("ownerId") Long ownerId);
// → 1 query com JOIN — o programador precisou de saber SQL para resolver
// ── Exemplo 2: String em Java e UTF-16 ───────────────────────────────────────
// A abstracção: "String é uma sequência de caracteres"
// A fuga: internamente é UTF-16 — alguns caracteres ocupam 2 code units (surrogate pairs)

String emoji = "😀";
emoji.length();          // → 2  (não 1! — emoji é um surrogate pair em UTF-16)
emoji.charAt(0);         // → '\uD83D' (metade do surrogate pair — não é o emoji)
emoji.codePointCount(0, emoji.length());  // → 1 (correcto — conta caracteres reais)

// Validação de tamanho máximo de username — a abstracção vaza:
String username = "😀😀😀😀😀";   // 5 emojis visíveis
username.length();        // → 10 (não 5!)
// Um limite de 10 chars parece razoável mas deixa passar apenas 5 emojis
// A abstracção "length = número de caracteres" vazou a representação interna UTF-16
// ── Exemplo 3: TCP e a ilusão de stream contínuo ─────────────────────────────
// A abstracção: "TCP é um stream de bytes — envias bytes, recebes bytes"
// A fuga: TCP divide dados em segmentos — uma escrita pode chegar em múltiplas leituras

// Servidor Java a ler de um socket:
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead = in.read(buffer);
// ERRO: bytesRead pode ser MENOS que 1024 mesmo que o cliente tenha enviado 1024 bytes
// TCP pode ter entregue apenas parte dos dados neste momento
// A abstracção "stream contínuo" vaza a natureza segmentada do TCP

// Solução correcta: ler em loop até ter todos os bytes esperados
int totalRead = 0;
int expected = 1024;
while (totalRead < expected) {
    int n = in.read(buffer, totalRead, expected - totalRead);
    if (n == -1) throw new EOFException("Ligação fechada prematuramente");
    totalRead += n;
}
// O programador precisou de perceber a semântica real do TCP
// ── Exemplo 4: Spring Security e o SecurityContext ───────────────────────────
// A abstracção: "@AuthenticationPrincipal injiecta o utilizador autenticado"
// A fuga: o SecurityContext é armazenado num ThreadLocal — não sobrevive a threads diferentes

@GetMapping("/accounts")
public List<AccountResponse> list(@AuthenticationPrincipal UserDetails user) {
    // funciona — estamos na thread do pedido HTTP, onde o SecurityContext foi populado
    return service.listAccounts(((AppUserDetails) user).getId());
}

// Código que quebra a abstracção:
@GetMapping("/accounts/async")
public CompletableFuture<List<AccountResponse>> listAsync(
        @AuthenticationPrincipal UserDetails user) {
    return CompletableFuture.supplyAsync(() -> {
        // PERIGO: esta lambda executa numa thread diferente do pool
        // SecurityContextHolder.getContext().getAuthentication() → null
        // A abstracção "utilizador autenticado está sempre disponível" vazou o ThreadLocal
        return service.listAccounts(((AppUserDetails) user).getId());
        // Solução: capturar o userId ANTES de entrar no supplyAsync
    });
}

A Pilha de Segurança Web como Abstracção

A arquitectura deste projecto — JWT, HttpOnly cookie, isolamento SQL — é ela própria uma pilha de abstracções sobre os protocolos de rede. Cada camada resolve um problema específico que a camada abaixo não resolve.

// A pilha de segurança deste projecto — cada camada resolve um problema diferente:

// ── Camada 1: TLS (HTTPS) ────────────────────────────────────────────────────
// Problema resolvido: confidencialidade e integridade em trânsito
// O que esconde: encriptação, certificados, handshake
// O que NÃO resolve: quem é o utilizador, o que pode fazer, IDOR

// ── Camada 2: HttpOnly Cookie ────────────────────────────────────────────────
// Problema resolvido: transportar o token de identidade sem expô-lo ao JavaScript
// O que esconde: o mecanismo de envio automático do browser
// O que NÃO resolve: provar que o token é legítimo, o que o utilizador pode aceder

// ── Camada 3: JWT (assinado com HMAC-SHA256) ─────────────────────────────────
// Problema resolvido: identidade verificável sem estado no servidor
// O que esconde: a assinatura criptográfica, a serialização Base64
// O que NÃO resolve: o que o utilizador pode fazer com os seus recursos

// ── Camada 4: Isolamento Lógico na Query SQL ─────────────────────────────────
// Problema resolvido: garantir que um utilizador só acede aos seus próprios recursos
// WHERE id = ? AND owner_id = ?
// O que esconde: a verificação de propriedade (está no SQL, não no código da aplicação)
// O que NÃO resolve: as camadas acima (se o JWT for roubado, este nível não ajuda)

// A Lei das Abstrações com Fugas aplicada:
// Se o TLS falhar (certificado comprometido) → o cookie é visível em trânsito
// Se o HttpOnly falhar (bug no browser) → o JavaScript acede ao token
// Se o JWT falhar (chave fraca, algoritmo "none") → identidade falsificável
// Se o SQL falhar (query sem owner_id) → IDOR independentemente das camadas acima
//
// Cada camada é necessária porque as camadas acima e abaixo têm falhas distintas.

Abstrações no Dia-a-Dia do Spring Boot

Reconhecer quando estás a usar uma abstracção — e saber o que está por baixo quando ela falha — é uma competência prática, não teórica.

// Quando a abstracção funciona — não precisas de saber o que está por baixo:
@Transactional
public void transferir(Long fromId, Long toId, BigDecimal valor) {
    Account from = repository.findByIdAndOwnerId(fromId, userId).orElseThrow(...);
    Account to   = repository.findById(toId).orElseThrow(...);
    from.debitar(valor);
    to.creditar(valor);
    // @Transactional garante atomicidade — ou ambas as operações acontecem, ou nenhuma
    // não precisas de escrever BEGIN TRANSACTION / COMMIT / ROLLBACK
}

// Quando a abstracção falha — precisas de saber o que está por baixo:

// Cenário 1: @Transactional não funciona numa chamada interna (self-invocation)
@Service
public class AccountService {
    @Transactional
    public void operacaoA() { ... }

    public void operacaoB() {
        operacaoA();  // ❌ @Transactional NÃO funciona — chama directamente o método,
                      // não passa pelo proxy Spring que intercepta a anotação
    }
}
// Para perceber isto, precisas de saber que @Transactional usa AOP proxy —
// a abstracção "anota o método e fica transaccional" vaza a implementação via proxy.

// Cenário 2: Hibernate não actualiza a entidade — flush não acontece quando esperado
@Transactional
public void actualizar(Long id, String novoNome) {
    Account account = repository.findById(id).orElseThrow(...);
    account.setNome(novoNome);
    // parece que falta repository.save(account) — mas com @Transactional + JPA,
    // o Hibernate detecta a alteração (dirty checking) e faz UPDATE automaticamente no commit
    // a abstracção "save para persistir" não é necessária dentro de @Transactional
    // mas só funciona se a entidade foi carregada pelo mesmo EntityManager da transacção
}

Princípio: Conhecer Dois Níveis Abaixo

Uma regra prática para engenheiros: quando usas uma abstracção, conhece pelo menos dois níveis abaixo. Não precisas de dominar cada nível — mas precisas de saber o suficiente para diagnosticar quando algo falha.

// O que "dois níveis abaixo" significa na prática:

// Se escreves Spring Data JPA:
//   → conhece JPA/Hibernate (nível -1): lazy loading, dirty checking, flush
//   → conhece SQL (nível -2): percebe o SQL gerado, sabe escrever queries manuais

// Se escreves Spring Security com JWT:
//   → conhece o mecanismo de filtros (nível -1): como o JwtAuthFilter intercepta pedidos
//   → conhece HTTP (nível -2): sabe o que é um Cookie header, como Set-Cookie funciona

// Se escreves @Transactional:
//   → conhece AOP proxy (nível -1): self-invocation não funciona
//   → conhece transacções SQL (nível -2): ACID, isolamento, locks

// Se usas String em Java:
//   → conhece a representação interna (nível -1): UTF-16, surrogate pairs
//   → conhece encoding (nível -2): UTF-8 vs UTF-16, bytes vs chars

// A regra não exige que sejas especialista em cada nível —
// exige que saibas o suficiente para responder:
// "O que está realmente a acontecer quando isto falha?"

Checklist Abstracção