Um olhar para trás sobre o curso e sobre o Farol: de onde veio cada pedra do servidor, o que ele deliberadamente não faz, como a linguagem mudou durante o ano e para onde seguir depois do núcleo.
Cinquenta e uma aulas atrás, "Uma Linguagem para Torradeiras que Conquistou o Mundo" contou a história de um projeto da Sun Microsystems, em 1991, que queria programar aparelhos domésticos e acabou criando uma das linguagens mais usadas do planeta. A aula seguinte, "Do Código ao Bytecode", instalou o JDK e imprimiu uma linha no terminal. Hoje, o mesmo JDK roda um servidor HTTP escrito do zero, que atende milhares de conexões ao mesmo tempo e serve um site para um navegador de verdade. Esta aula não apresenta nenhum recurso novo da linguagem. Ela olha para o caminho percorrido, porque aprender é também reconhecer o que já se sabe, e fecha a Fase 9 e o núcleo do curso.
O Farol pronto
O servidor terminou com 26 arquivos de código de produção, cerca de 1.600 linhas, das quais uns 1.200 são código de fato, sem comentários e linhas em branco, mais 7 arquivos de teste, com 740 linhas e 67 testes, e um site de três arquivos. Não usa nenhuma biblioteca além do JDK, depende só do módulo java.base e cabe numa imagem de execução de 42 MB. O que ele faz:
- lê requisições HTTP/1.1 direto dos bytes do socket, seguindo a RFC 9112, e recusa entradas malformadas, grandes demais ou perigosas com o código de estado certo, incluindo as defesas contra contrabando de requisições e injeção de cabeçalhos;
- representa requisições e respostas com records, enums e hierarquias seladas, de forma que o compilador impede respostas inválidas e obriga todo código a tratar todos os tipos de conteúdo;
- roteia por método e caminho, com parâmetros, curingas, especificidade, 404, 405 com
Allow,HEADeOPTIONS; - atende cada conexão numa thread virtual, com conexões persistentes, dois prazos diferentes, limite de conexões com 503 e desligamento ordenado;
- serve arquivos estáticos com o tipo de mídia certo, cache com
ETageLast-Modified, e duas camadas de defesa contra a travessia de diretório.
Nas medições, atendeu cinco mil requisições lentas simultâneas em 1,9 segundo, cerca de 17 mil requisições por segundo com keep-alive em dois núcleos divididos com o próprio cliente de carga, e respondeu a um cliente normal em 25 milissegundos com cinco mil conexões hostis abertas.
Uma pedra de cada fase
O Farol foi a prova de que cada fase do curso tinha um propósito. Olhando para o código, dá para apontar de onde veio cada parte.
Da Fase 1, os fundamentos: os tipos primitivos de "Cada Valor em sua Caixa" nos bytes lidos um a um, com o int que vale -1 no fim do fluxo; os laços e as decisões de "O Programa que Escolhe e Repete" na máquina de estados que procura o CRLF; os arrays de "Muitos Valores sob um Nome" nos buffers; e a distinção entre caracteres e bytes de "Operadores, Texto e Conversa com o Usuário", sem a qual o Content-Length estaria errado para qualquer texto com acento.
Da Fase 2, a modelagem: o encapsulamento de "O que Fica Dentro, o que Fica Fora" nas classes Corpo e Cabecalhos, que não deixam ninguém alterar o que guardam; o contrato de equals e hashCode de "Iguais ou o Mesmo?", reescrito para comparar bytes; as interfaces de "Contratos sem Implementação" no Manipulador; os records e enums de "Dados sem Cerimônia" em Requisicao, Resposta, Metodo e Status; e as hierarquias seladas de "Hierarquias Fechadas" em Conteudo, Segmento e Busca, que fizeram o compilador apontar, três vezes ao longo do projeto, cada lugar que precisava mudar.
Da Fase 3, a biblioteca: o TreeMap com CASE_INSENSITIVE_ORDER de "Ordem e Critério" nos cabeçalhos; as exceções verificadas de "Quando o Contrato se Quebra" na ErroDeProtocolo, que obriga todo chamador a tratar uma requisição inválida; o try-with-resources e o NIO.2 de "Recursos que se Fecham Sozinhos" em cada socket, em cada arquivo e no toRealPath que barra ligações simbólicas; e o java.time e as expressões regulares de "Tempo, Dinheiro e Padrões" nas datas RFC 1123 e na gramática dos tokens.
Da Fase 4, o estilo funcional: o Manipulador como lambda, de "Comportamento como Valor"; os streams de "Pare de Escrever Laços" na listagem de tarefas e na escolha da rota mais específica; o Collectors.joining de "Agrupar, Achatar, Reduzir" no JSON; e o Optional de "Um Valor que Pode Não Existir" em cada consulta que pode não ter resposta.
Da Fase 5, a engenharia: o módulo de "Fronteiras Explícitas", que expõe só o que deve ser exposto; o Maven de "Construção Automatizada"; e os testes de "Provando que Funciona", que transformaram cada ataque descoberto num teste permanente.
Da Fase 6, o conhecimento da máquina: o custo de criar exceções, medido para justificar o Supplier do leitor; o CDS e o cache de classes de "A Máquina por Trás da Máquina" e "Medir Antes de Otimizar" nas medições de inicialização; e o despejo de threads do jcmd, que mostrou cinco mil threads virtuais esperando sem ocupar o sistema operacional.
Da Fase 7, a concorrência, a fase que mais pesou no Farol: as threads virtuais de "Um Milhão de Threads" em cada conexão; o Semaphore, o ConcurrentHashMap, o LongAdder e o AtomicInteger de "Ferramentas de Precisão"; os campos volatile de "O Cadeado e a Memória" no sinal de encerramento; o CountDownLatch e o desligamento em etapas de "Não Crie Threads, Entregue Tarefas"; e os CompletableFutures de "Encadeando o que Ainda Não Aconteceu" no teste com mil requisições assíncronas.
Da Fase 8, a responsabilidade: as fábricas estáticas, o Builder dos cabeçalhos e o Decorator de "Lições de Quem Veio Antes"; a imutabilidade, as cópias defensivas e a validação na fronteira de "Contratos à Vista"; e o jdeps, o jlink e o CDS de "Do JAR ao Executável", que transformaram o projeto num Java sob medida.
O que o Farol não é
O Farol é um servidor completo para o que se propôs, e é importante saber exatamente onde ele para, porque é ali que começa o trabalho dos servidores de produção.
Ele não fala HTTPS. Na prática, isso raramente é um problema, porque é comum colocar um proxy reverso, como o nginx, na frente do servidor Java para cuidar da criptografia, e é o que a Trilha A vai fazer. Mas o JDK tem tudo o que é preciso para o Farol falar TLS sozinho, como o exercício 1 mostra.
Ele não entende corpos em partes (chunked) nas requisições, que a RFC exige de todo servidor HTTP/1.1, e responde 411 a eles; o exercício 2 implementa a leitura. Não fala HTTP/2 nem HTTP/3, que usam quadros binários e multiplexam várias requisições numa conexão. Não comprime respostas com gzip, não tem autenticação, não interpreta formulários multipart para envio de arquivos, e não tem observabilidade: métricas, rastreamento de requisições entre serviços, registros estruturados que um sistema de monitoramento consiga ler. O exercício 5 dá o primeiro passo nessa direção.
E não tem nada do que fica acima do HTTP: conversão automática entre JSON e objetos, validação declarativa de dados, injeção de dependências, transações com banco de dados, páginas geradas a partir de modelos. Esse é exatamente o território dos frameworks, e é por isso que a Trilha A começa logo depois desta aula. Depois de construir o Farol, cada coisa que o Spring faz "por mágica" vai ter uma explicação concreta: em algum lugar, há um leitor de bytes, um roteador e uma thread por conexão.
Um ano de Java, e o Java de um ano
O curso acompanhou a linguagem numa versão específica, o Java 25, lançado em setembro de 2025 como versão de suporte de longo prazo. Durante o ano do curso, o calendário de lançamentos semestrais seguiu seu ritmo: o Java 26 saiu em março de 2026 e o Java 27, em setembro de 2026, ambos sem suporte de longo prazo. A escolha de permanecer no 25 foi deliberada, pelo motivo discutido no começo do curso: é a versão que as empresas vão usar por anos, e tudo o que ele ensina continua valendo nas versões seguintes, que acrescentam recursos sem quebrar os anteriores. O próximo LTS, pelo ritmo de dois anos, deve ser o Java 29, em setembro de 2027.
Algumas coisas vistas como "futuro" ao longo do curso continuam em andamento: a concorrência estruturada de "Tarefas em Família" segue em prévia; os tipos restritos a não nulos do Projeto Valhalla, mencionados em "Contratos à Vista", ainda não chegaram à linguagem; e o Projeto Leyden continua acrescentando otimizações de inicialização ao cache AOT. Acompanhar essas mudanças é parte do trabalho de quem programa em Java, e as fontes desta aula são os lugares para isso.
As cinco aulas do Farol
A Fase 9 foi um único projeto, construído em cinco aulas. Em "O Projeto Final Começa", você espiou os bytes que um cliente HTTP envia, escreveu um servidor ingênuo de trinta linhas, derrubou-o de três formas diferentes e desenhou a arquitetura em camadas que resolveria cada problema. Em "A Primeira Pedra", construiu o leitor de requisições sobre bytes, com limites em cada entrada, respostas de erro precisas e defesas contra o contrabando de requisições e a injeção de cabeçalhos, e um servidor que nenhum cliente derrubava. Em "A Segunda Pedra", trocou os textos por tipos, com enums, records e hierarquias seladas, desmontou o alvo com a decodificação correta e construiu um roteador que responde 404, 405 e OPTIONS como a RFC manda. Em "A Terceira Pedra", deu a cada conexão uma thread virtual, implementou as conexões persistentes com dois prazos, limitou as conexões com um Semaphore, desligou com ordem e mediu tudo com um cliente de carga escrito à mão. E em "A Pedra de Acabamento", serviu um site com cache e com duas camadas de defesa, testou o sistema inteiro com o cliente HTTP do JDK e o empacotou num Java sob medida.
O que a Fase 9 ensinou, além do HTTP, foi o ciclo de trabalho de um projeto real: construir o mínimo, atacá-lo, transformar cada falha num requisito e cada requisito num teste, medir antes de acreditar, e voltar ao código quando a medição surpreende. Foi assim que o cliente de carga descobriu que ignorava o Connection: close, que o teste com ulimit revelou o laço que girava em falso, e que a medição da imagem expôs o lançador que engolia os sinais.
Com a Fase 9, termina o núcleo do curso: 52 aulas que vão da primeira linha de código a um servidor concorrente, seguro e testado, sem nenhuma biblioteca externa. Quem chegou até aqui sabe Java de verdade, e sabe o que acontece por baixo de cada abstração que os frameworks oferecem.
Os próximos passos
As trilhas de extensão continuam a partir daqui, e podem ser feitas na ordem que você preferir. A Trilha A, de web, APIs e bancos de dados, é a mais próxima do mercado de trabalho: começa com a próxima aula, "Java no Servidor", passa por SQL, pelo Spring Boot, pelo JPA e pelos bancos PostgreSQL, MySQL e MongoDB, e termina com um site completo, as Trilhas da Ilha, publicado num servidor. A Trilha B leva o Java para o desktop, com o JavaFX, e para o Android, e depende da API construída na Trilha A. A Trilha C entra no mundo dos jogos com a libGDX, onde o game loop e a pressão sobre o coletor de lixo trazem de volta, com outra cara, as lições da Fase 6.
E o Farol pode continuar crescendo. Os exercícios desta aula são o começo de uma lista: HTTPS, corpos em partes, métricas, compressão, HTTP/2. Cada um deles é um pequeno projeto, e cada um vai exigir voltar à RFC, escrever o teste primeiro e medir no fim, exatamente como foi feito aqui.
Fontes e leituras recomendadas
- Dev.java: o portal oficial de aprendizado do Java, com tutoriais atualizados para as versões mais recentes e a seção "Inside Java" com notícias da equipe da linguagem. https://dev.java/
- OpenJDK, índice de JEPs: a lista de todas as propostas de melhoria do Java, com o estado de cada uma. É o lugar para acompanhar o que entra em cada versão. https://openjdk.org/jeps/0
- The Java Version Almanac: uma referência que mostra, versão por versão, as APIs novas, removidas e alteradas, com comparações entre quaisquer duas versões. https://javaalmanac.io/
- Inside Java: o blog e o podcast dos engenheiros da Oracle que trabalham na plataforma, onde as mudanças de cada versão são explicadas por quem as fez. https://inside.java/
- Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017): para quem quer ir além de um servidor e entender sistemas distribuídos, bancos de dados e replicação. Uma leitura natural depois do Farol e antes, ou junto, da Trilha A.
Exercícios
Exercício 1
Faça o Farol falar HTTPS usando apenas o JDK. Mostre como gerar um certificado de teste, como criar o soquete do servidor e que mudança mínima o Servidor precisa sofrer. Por que o aperto de mão do TLS não atrasa o aceitador?
Ver resposta
✓ Resposta: O certificado de teste é gerado com o keytool, que vem no JDK:
$ keytool -genkeypair -alias farol -keyalg EC -groupname secp256r1 -dname "CN=localhost" \
-ext "SAN=dns:localhost,ip:127.0.0.1" -validity 30 \
-storetype PKCS12 -keystore farol.p12 -storepass segredo123
O soquete seguro é criado a partir de um SSLContext com a chave:
static ServerSocket soqueteSeguro(int porta, String arquivo, char[] senha) throws Exception {
KeyStore chaves = KeyStore.getInstance("PKCS12");
try (InputStream in = new FileInputStream(arquivo)) { chaves.load(in, senha); }
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(chaves, senha);
SSLContext contexto = SSLContext.getInstance("TLS");
contexto.init(kmf.getKeyManagers(), null, null);
SSLServerSocket s = (SSLServerSocket) contexto.getServerSocketFactory().createServerSocket(porta);
s.setEnabledProtocols(new String[]{"TLSv1.3", "TLSv1.2"});
return s;
}
Um servidor de teste com esse soquete respondeu ao curl -k https://localhost:8443/seguro com TLS ok: GET /seguro HTTP/1.1 via TLSv1.3; o -k diz ao curl para aceitar um certificado autoassinado. A mudança no Servidor é mínima porque SSLServerSocket é um ServerSocket, e os Sockets que ele devolve são SSLSockets, com os mesmos fluxos de entrada e saída: basta um segundo construtor que receba o ServerSocket pronto em vez de criá-lo, e o leitor, o escritor e a Conexao não mudam uma linha. É o polimorfismo de "Um Tipo, Várias Formas" trabalhando. O aperto de mão do TLS não acontece no accept(): ele é feito de forma preguiçosa, na primeira operação de leitura ou escrita do SSLSocket, e essa operação acontece dentro da thread virtual da conexão. O aceitador só aceita e distribui, e um cliente lento no aperto de mão ocupa apenas a sua própria thread virtual, sujeita ao mesmo prazo de leitura das outras.
Exercício 2
Escreva um método que leia um corpo no formato em partes (chunked, RFC 9112, seção 7.1), com um tamanho máximo, e descreva onde ele entraria no LeitorDeRequisicao. Há uma armadilha no tamanho das partes: qual?
Ver resposta
✓ Resposta: Cada parte começa com uma linha com o tamanho em hexadecimal, opcionalmente seguido de extensões depois de um ponto e vírgula, depois vêm os bytes da parte e um CRLF; uma parte de tamanho zero encerra o corpo, e depois dela podem vir cabeçalhos finais, terminados por uma linha vazia.
static byte[] lerEmPartes(InputStream in, long maximo) throws IOException {
var corpo = new ByteArrayOutputStream();
while (true) {
String cabecaDaParte = linha(in);
int pontoEVirgula = cabecaDaParte.indexOf(';'); // extensões de parte: ignoradas
String hexa = (pontoEVirgula < 0 ? cabecaDaParte : cabecaDaParte.substring(0, pontoEVirgula)).strip();
if (hexa.isEmpty() || hexa.length() > 8 || !hexa.chars().allMatch(ch -> Character.digit(ch, 16) >= 0))
throw new IOException("tamanho de parte inválido: '" + cabecaDaParte + "'");
long tamanho = Long.parseLong(hexa, 16); // até 8 dígitos: cabe num long, nem sempre num int
if (tamanho == 0) break; // a parte final
if (corpo.size() + tamanho > maximo) throw new IOException("corpo passou de " + maximo + " bytes");
byte[] parte = in.readNBytes((int) tamanho);
if (parte.length < tamanho) throw new EOFException("parte incompleta");
corpo.write(parte);
if (!linha(in).isEmpty()) throw new IOException("parte sem CRLF no fim");
}
while (!linha(in).isEmpty()) { } // cabeçalhos finais (trailers): ignorados
return corpo.toByteArray();
}
Com a entrada 7\r\nComprar\r\n5\r\n pão\r\n0\r\n\r\n seguida de uma segunda requisição, o método devolveu "Comprar pão" e deixou a linha GET /seguinte HTTP/1.1 intacta para a próxima leitura. A armadilha apareceu no primeiro teste: a versão inicial usava Integer.parseInt(hexa, 16), e uma parte de tamanho ffffffff, com oito dígitos válidos, lançou NumberFormatException, porque 4.294.967.295 não cabe num int. Um atacante poderia derrubar a requisição com uma exceção não prevista, ou, com código menos cuidadoso, causar um estouro para número negativo. Com Long.parseLong e o limite verificado antes de ler, a parte gigante é recusada com "corpo passou de 1024 bytes", sem alocar nada. No LeitorDeRequisicao, o método substituiria o throw ErroDeProtocolo.tamanhoObrigatorio(...) quando Transfer-Encoding for exatamente chunked, usando a mesma EntradaHttp para as linhas e os bytes, com as IOExceptions de formato convertidas em ErroDeProtocolo com 400 e o excesso em 413. A regra de recusar Transfer-Encoding junto com Content-Length continua valendo.
Exercício 3
Percorra o caminho de uma requisição GET /css/estilo.css pelo Farol, da chegada dos bytes ao envio da resposta, citando cada classe e o que ela faz.
Ver resposta
✓ Resposta: O aceitador, na thread de plataforma farol-aceitador do Servidor, recebe o Socket do accept(), obtém uma licença do Semaphore e entrega uma nova Conexao a uma thread virtual. A Conexao define o prazo ocioso e chama LeitorDeRequisicao.aguardarProxima(), que usa mark e reset do BufferedInputStream dentro da EntradaHttp para esperar o primeiro byte. Com o prazo de leitura, chama ler(): a EntradaHttp lê a linha de pedido e os cabeçalhos até o CRLF, o leitor valida o método (virando Metodo.GET), decodifica o alvo num Alvo com os segmentos [css, estilo.css], valida a versão e o Host, constrói os Cabecalhos e, sem Content-Length, usa Corpo.VAZIO. A Conexao chama o manipulador, que é o Roteador: o buscar testa cada Padrao, só o /{*arquivo} casa, com o parâmetro arquivo = "css/estilo.css", e o switch sobre a Busca chama o ArquivosEstaticos com a requisição preenchida por comParametros. O localizar filtra os nomes, resolve o caminho dentro da raiz e confirma com toRealPath e startsWith; o manipulador lê os atributos, monta ETag e Last-Modified, verifica If-None-Match e devolve uma Resposta com Conteudo.Arquivo e o tipo text/css; charset=utf-8, vindo de TiposDeMidia. A Conexao decide que a conexão continua, porque é HTTP/1.1 sem Connection: close, e chama o EscritorDeResposta, que escreve a linha de estado e os cabeçalhos, pega o tipo e o tamanho do Conteudo.Arquivo pelo switch exaustivo e copia o arquivo com transferTo pelo LimiteDeSaida. A Conexao registra a linha no Registro e volta a esperar a próxima requisição. São dezoito tipos do Farol, cada um com uma responsabilidade, e nenhuma conhece mais do que a camada abaixo dela.
Exercício 4
O Farol atende cada conexão numa thread virtual e usa entrada e saída bloqueantes. Servidores como o Netty, base de muitos frameworks, usam um pequeno número de threads e entrada e saída não bloqueante, com seletores. Compare os dois modelos e diga por que o Farol, escrito em 2026, pôde usar o mais simples.
Ver resposta
✓ Resposta: No modelo não bloqueante, usado pelo Netty desde 2004, um punhado de threads, tipicamente uma por núcleo, vigia milhares de sockets com um seletor do sistema operacional (epoll no Linux, exposto no Java pelo java.nio.channels.Selector), e só processa um socket quando ele tem dados prontos. Nenhuma thread fica parada esperando, e o servidor escala para centenas de milhares de conexões com poucas threads de plataforma. O preço é o estilo: o processamento de uma requisição é quebrado em pedaços executados à medida que os dados chegam, com estado guardado manualmente entre eles e callbacks ou futuros encadeados, o mesmo problema de "Encadeando o que Ainda Não Aconteceu", só que um nível abaixo. No modelo de uma thread por conexão, o código é sequencial: ler a linha, ler os cabeçalhos, chamar o manipulador, escrever, e cada read() bloqueia. Com threads de plataforma, esse modelo não escalava além de alguns milhares de conexões, e era por isso que os servidores de alto desempenho eram escritos no modelo não bloqueante. As threads virtuais mudaram a conta: por baixo, quando uma thread virtual bloqueia num socket, a JVM a desmonta e registra o socket num seletor interno, fazendo exatamente o que o Netty faz, mas escondido. O Farol obtém a escalabilidade do modelo não bloqueante com o código do modelo bloqueante, e foi isso que permitiu escrevê-lo em cinco aulas, com 5.000 conexões simultâneas e um código que se lê de cima para baixo. O modelo não bloqueante ainda leva vantagem em cenários extremos de desempenho e memória por conexão, e continua sendo a base de muita infraestrutura, mas deixou de ser obrigatório.
Exercício 5
Escreva um decorador ComMetricas que conte as respostas por código de estado e responda, em /metricas, um texto no formato usado pelo Prometheus, como farol_respostas{status="200"} 1000. Ele precisa ser seguro entre threads: por quê, e com quais estruturas?
Ver resposta
✓ Resposta:
static final class ComMetricas implements Manipulador {
private final Manipulador interno;
private final Map<Integer, LongAdder> porStatus = new ConcurrentSkipListMap<>();
ComMetricas(Manipulador interno) { this.interno = interno; }
@Override public Resposta atender(Requisicao req) throws Exception {
if (req.alvo().caminho().equals("/metricas")) return Resposta.texto(relatorio());
Resposta r;
try {
r = interno.atender(req);
} catch (Exception e) {
contar(500); // o servidor vai transformar a exceção num 500
throw e;
}
contar(r.status().codigo());
return r;
}
private void contar(int codigo) { porStatus.computeIfAbsent(codigo, k -> new LongAdder()).increment(); }
private String relatorio() {
return porStatus.entrySet().stream()
.map(e -> "farol_respostas{status=\"" + e.getKey() + "\"} " + e.getValue().sum())
.collect(Collectors.joining("\n", "", "\n"));
}
}
Envolvendo um roteador e disparando, em threads virtuais simultâneas, mil requisições a uma rota que responde 200, vinte a um caminho inexistente e três a uma rota que lança uma exceção, o /metricas mostrou farol_respostas{status="200"} 1000, {status="404"} 20 e {status="500"} 3, sem nenhuma contagem perdida. A segurança entre threads é obrigatória porque o mesmo decorador é chamado ao mesmo tempo por todas as threads virtuais das conexões. As estruturas são as de "Ferramentas de Precisão": o computeIfAbsent de um mapa concorrente garante que um só contador de cada código fique no mapa e seja devolvido a todas as threads, e o LongAdder absorve os incrementos disputados sem perder nenhum. O mapa é um ConcurrentSkipListMap para que o relatório saia ordenado por código. Ele tem um detalhe que a documentação avisa: no computeIfAbsent dele, a função não roda de forma atômica, e numa disputa várias threads podem criar o seu LongAdder para o mesmo código. Só um entra no mapa, e é esse que todas recebem de volta, então nenhuma contagem se perde: em vinte rodadas com as 1.023 requisições largadas ao mesmo tempo, foram criados de 7 a 46 contadores para os três códigos, e os totais saíram certos todas as vezes. Um ConcurrentHashMap roda a função uma única vez, mas não mantém as chaves em ordem. As exceções são contadas como 500 e relançadas, para que o servidor continue tratando-as como antes: o decorador observa, mas não muda o comportamento, como o whenComplete do CompletableFuture. Com o servidor recebendo new ComMetricas(rotas) em vez de rotas, nenhuma rota precisa mudar, e o formato do texto é o que o Prometheus, um dos sistemas de monitoramento mais usados, sabe coletar.