O contrato começa no mapping
Uma rota é mais do que um caminho. A declaração da classe pode definir /shipments, enquanto o método acrescenta /{id} e restringe a operação a GET. O pedido GET /shipments/42 pode corresponder; POST para o mesmo caminho não adquire esse contrato por partilhar a URL.
Antes de rever a lógica de negócio, escreve o método, o caminho efetivo e as condições relevantes. Inclui prefixos de contexto ou proxy quando existirem. Os exemplos desta aula assumem que o pedido já chegou ao MVC e que nenhuma camada de segurança ou customização altera a resolução.
Cada argumento tem uma origem
PathVariable obtém um valor capturado no caminho. RequestParam resolve parâmetros Servlet, incluindo query e formulário. RequestBody pede a leitura da mensagem por um conversor compatível. Trocar estas anotações muda o contrato de entrada; não é uma forma equivalente de escrever o mesmo endpoint.
Para /quotes/BOX-A?units=3, sku vem do caminho e units da query. O int exige conversão numérica, mas não significa que o valor seja positivo. A próxima aula acrescenta restrições explícitas. Ler corretamente um valor ainda não valida a regra que o negócio quer impor.
Ausência, repetição e formulários
Um RequestParam sem opção de ausência nem valor predefinido é obrigatório. Decide se a ausência é erro, se deve produzir um valor predefinido ou se tem significado próprio representado por Optional. Não deixes essa decisão dependente de uma suposição sobre o valor inicial de um tipo primitivo.
Se o cliente envia tag=urgent&tag=fragile, usa uma coleção para conservar a multiplicidade. Um mapa de valores escalares não exprime a mesma necessidade. Em formulários Servlet, o acesso aos parâmetros pode consumir o corpo durante o parsing; não planeies lê-lo depois livremente como se fosse um novo corpo JSON.
Entrada e saída negociadas
Content-Type identifica a representação enviada. Accept exprime as representações que o cliente aceita receber. Um POST pode enviar JSON e aceitar JSON, mas os dois cabeçalhos continuam a responder a perguntas diferentes. consumes restringe a entrada e produces a saída. Alterar Accept não corrige um corpo identificado incorretamente como text/plain.
Há uma regra importante na composição: consumes e produces definidos no método substituem os valores da classe para esse método. Uma classe que consome JSON pode ter um método que consome texto; não se exige um corpo que seja simultaneamente os dois formatos.
Estado, cabeçalhos e representação
ResponseEntity permite declarar o estado, os cabeçalhos e o corpo. Se a criação já terminou, created(uri).body(dto) exprime 201 com Location e um DTO. É ainda necessário um conversor capaz de escrever esse DTO no formato negociado. Um literal "201" devolvido no corpo não tem o mesmo significado.
O builder não executa a criação. Se a operação apenas foi aceite para processamento, o contrato deve descrever esse estado real. Também é possível falhar ao escrever a resposta depois de trabalho já realizado; a confirmação de efeitos e a política de repetição precisam de desenho próprio.
Ler a fase de rejeição
Num registo ilustrativo, PUT /shipments recebe 405 e o método POST de criação não é chamado. A primeira diferença a corrigir é o método HTTP. Noutro pedido, o caminho e método correspondem, mas text/plain encontra um contrato que só aceita JSON: a rejeição normal é 415. Quando o conflito está no Accept e na saída disponível, é 406.
Estes estados ajudam a formular uma hipótese; não substituem o tipo de exceção e a configuração real. Não atribuas uma espera por SQL a um pedido rejeitado antes do handler. Confirma também que o estado observado não foi produzido por um gateway ou handler personalizado.
Leitura guiada do controlador
No exemplo, considera três pedidos: units=3, units ausente e units=three. O primeiro permite construir QuoteInput; o segundo falha a obrigatoriedade; o terceiro falha a conversão numérica. A assinatura não contém uma restrição que impeça units=-2. Esse pedido pode ser legível e continuar a ser inadequado ao negócio.
Compara também Accept: application/xml com a declaração JSON. Se não existe outro handler aplicável, a preferência de saída não pode ser satisfeita. O exercício consiste em escolher e justificar estas interpretações; não exige executar o código nem demonstra um serviço de cotações completo.
// Separate source file: QuoteController.java, inside the application scan root.
// Servlet Spring MVC; compatible JSON converter available.
package com.acme.shipping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(path = "/quotes", produces = "application/json")
public class QuoteController {
public record QuoteInput(String sku, int units) {}
@GetMapping("/{sku}")
public QuoteInput inspect(
@PathVariable("sku") String sku,
@RequestParam("units") int units) {
return new QuoteInput(sku, units);
}
}
Interpreta GET /quotes/BOX-A?units=3 com Accept: application/json: o caminho fornece BOX-A e a query fornece 3. O DTO apenas mostra os argumentos. Não calcula preço, não reserva stock e ainda não impõe units positivo.
Armadilhas comuns
Confundir Accept com Content-Type; tratar parâmetros da query como corpo JSON; perder valores repetidos; anunciar criação apenas por escolher 201; concluir que todo o 400 corresponde à mesma falha.
Tópicos relacionados: Validação, erros e contratos de resposta · Contexto da aplicação e dependências · Âmbito de testes e evidência de integração
Identifica primeiro o pedido que pode chegar ao método, depois os dados que podem ser convertidos e finalmente a resposta que pode ser representada. O mapping não comprova autorização, persistência ou entrega.
Referência: Request mapping conditions · Spring Boot 4.1.1 / Spring Framework 7.0.9 / Java 25