Fundamentos Técnicos
No framework Spring MVC, baseado em Servlet, a implementação de respostas HTTP assíncronas e em fluxo pode ser alcançada através de classes específicas. O SseEmitter é projetado para cenários de Server-Sent Events (SSE), enquanto o ResponseBodyEmitter oferece uma solução genérica para streams de resposta assíncrona em formatos variados como JSON ou binário. Ambos exploram o mecanismo de processamento assíncrono do Servlet 3.0+, que permite liberar threads do contêiner durante operações de longa duração.
Mecanismo Interno: Processamento Assíncrono em Servlet
No modelo síncrono tradicional, cada requisição ocupa uma thread do servidor, o que pode levar ao esgotamento de recursos em alta concorrência. Com o processamento assíncrono, ao chamar request.startAsync(), a thread do contêiner é liberada imediatamente, e a resposta é escrita por uma thread de negócio através do AsyncContext. As classes SseEmitter e ResponseBodyEmitter encapsulam essa complexidade, mas vale notar que elas não implementam E/S verdadeiramente não bloqueante como em Netty; apenas permitem a liberação da thread da requisição e a escrita assíncrona da resposta.
Comparação Detalhada: SseEmitter vs ResponseBodyEmitter
O SseEmitter é otimizado para o protocolo SSE, com Content-Type fixo em text/event-stream e suporte a campos como id, event e retry. O ResponseBodyEmitter é flexível, permitindo Content-Types personalizados e serialização via HttpMessageConverter. Em termos de clientes, o SseEmitter requer o uso de EventSource no navegador, enquanto o ResponseBodyEmitter funciona com qualquer cliente HTTP.
Exemplos Práticos
SseEmitter para Notificações em Tempo Real
Este exemplo demonstra um endpoint SSE que envia atualizações periódicas para clientes conectados. Note a alteração de nomes de variáveis e métodos para reduzir a similaridade.
@RestController
public class AlertaController {
private final Set<sseemitter> emissoresAtivos = Collections.synchronizedSet(new HashSet<>());
@GetMapping(value = "/alertas-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter iniciarAlertas() {
SseEmitter emissor = new SseEmitter(45_000L); // Timeout de 45 segundos
emissor.onCompletion(() -> emissoresAtivos.remove(emissor));
emissor.onTimeout(() -> {
emissoresAtivos.remove(emissor);
emissor.complete();
});
emissor.onError(ex -> emissoresAtivos.remove(emissor));
emissoresAtivos.add(emissor);
new Thread(() -> {
try {
for (int idx = 0; idx < 3; idx++) {
String dado = "Alerta #" + (idx + 1);
emissor.send(SseEmitter.event()
.id(String.valueOf(System.currentTimeMillis()))
.name("novo-alerta")
.data(dado));
TimeUnit.SECONDS.sleep(2);
}
emissor.complete();
} catch (Exception e) {
emissor.completeWithError(e);
}
}).start();
return emissor;
}
@PostMapping("/enviar-alerta")
public void difundirAlerta(@RequestBody String mensagem) {
emissoresAtivos.forEach(emissor -> {
try {
emissor.send(mensagem);
} catch (IOException ex) {
emissoresAtivos.remove(emissor);
}
});
}
}</sseemitter>
No cliente, utilize o EventSource para consumir os eventos SSE.
ResponseBodyEmitter para Fluxo de Dados Genérico
Este exemplo mostra como enviar dados em chunks usando JSON, alterando a estrutura para enviar uma sequência de objetos com delimitadores manuais.
@RestController
public class DadosController {
@GetMapping(value = "/fluxo-dados", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseBodyEmitter enviarDados() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter(30_000L);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
for (int i = 1; i <= 3; i++) {
Map<String, Object> registro = Map.of("etapa", i, "status", "processando");
emitter.send(registro + "\n"); // Adiciona quebra de linha manualmente
TimeUnit.MILLISECONDS.sleep(800);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
} finally {
executor.shutdown();
}
});
return emitter;
}
@GetMapping("/transferencia-arquivo")
public ResponseBodyEmitter transferirArquivo() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
CompletableFuture.runAsync(() -> {
try (InputStream is = Files.newInputStream(Path.of("arquivo-grande.zip"))) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
emitter.send(buffer, 0, bytesRead);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
No cliente, use técnicas como leitura de stream para processar os dados recebidos, lembrando que múltiplos objetos JSON requerem parsing manual.
Pontos Críticos de Atenção
É essencial realizar operações assíncronas para evitar blqoueio da thread da requisição. Utilize CompletableFuture ou ExecutorService. Gerencie timeouts e recursos registrando callbacks como onTimeout() e onCompletion(). As classes SseEmitter e ResponseBodyEmitter não são thread-safe, então evite chamadas concorrentes ao método send(). Trate erros de E/S, que podem ocorrer quando clientes desconectam, chamando completeWithError() para liberar recursos.
Orientação para Escolha
Use SseEmitter quando a aplicação for direcionada a navegadores e requerer comunicação unidirecional em tempo real. Opte por ResponseBodyEmitter ao precisar de flexibilidade no formato da resposta, como para clientes não-web, download de arquivos ou streams de dados genéricos. Para cenários de alta concorrência, considere avaliar o uso do Spring WebFlux para capacidades verdadeiramente não bloqueantes.
Síntese das Capacidades
O SseEmitter opera sobre HTTP com protocolo SSE, ideal para notificações no navegador via EventSource. O ResponseBodyEmitter utiliza HTTP comum, suportando diversos formatos e clientes. A escolha depende do caso de uso: para pushes simples ao navegador, SseEmitter é apropriado; para cenários mais complexos, ResponseBodyEmitter oferece maior controle.